madoi-client
Version:
A Client library of Madoi Server for Distributed Sharing
2 lines (1 loc) • 15 kB
JavaScript
(function(i,M){typeof exports=="object"&&typeof module<"u"?M(exports):typeof define=="function"&&define.amd?define(["exports"],M):(i=typeof globalThis<"u"?globalThis:i||self,M(i.madoi={}))})(this,function(i){"use strict";var Z=Object.defineProperty;var x=(i,M,b)=>M in i?Z(i,M,{enumerable:!0,configurable:!0,writable:!0,value:b}):i[M]=b;var f=(i,M,b)=>x(i,typeof M!="symbol"?M+"":M,b);class M extends EventTarget{dispatchCustomEvent(v,e){return super.dispatchEvent(new CustomEvent(v,{detail:e}))}}const b={sender:"__PEER__",castType:"PEERTOSERVER",recipients:void 0},R={sender:"__PEER__",castType:"BROADCAST",recipients:void 0},O={sender:"__PEER__",recipients:void 0};function E(d=void 0){return{type:"Ping",...b,body:d}}function j(d){return{type:"EnterRoom",...b,...d}}function $(d){return{type:"LeaveRoom",...b,...d}}function P(d){return{type:"UpdateRoomProfile",...R,...d}}function g(d){return{type:"UpdatePeerProfile",...R,...d}}function C(d){return{type:"DefineFunction",...b,...d}}function _(d){return{type:"DefineObject",...b,...d}}function D(d,v){return{type:"InvokeFunction",castType:d,...O,...v}}function I(d){return{type:"UpdateObjectState",...b,...d}}function A(d,v){return{type:"InvokeMethod",castType:d,...O,...v}}function m(d){return(v,e,t)=>{v[e].madoiMethodConfig_={...v[e].madoiMethodConfig_?v[e].madoiMethodConfig_:{},...d}}}function L(d){return v=>{v.madoiClassConfig_={className:d}}}const w={serialized:!0};function k(d=w){return m({distributed:d})}function F(){return m({changeState:{}})}const T={maxInterval:5e3,minInterval:3e3};function N(d=T){return m({getState:d})}function H(){return m({setState:{}})}function B(){return m({hostOnly:{}})}function J(){return m({beforeEnterRoom:{}})}function z(){return m({enterRoomAllowed:{}})}function Q(){return m({enterRoomDenied:{}})}function G(){return m({leaveRoomDone:{}})}function q(){return m({roomProfileUpdated:{}})}function V(){return m({peerEntered:{}})}function W(){return m({peerLeaved:{}})}function K(){return m({peerProfileUpdated:{}})}function X(d){return m({userMessageArrived:{type:d}})}class Y extends M{constructor(e,t,s,n){super();f(this,"connecting",!1);f(this,"interimQueue");f(this,"distributedFuncs",new Map);f(this,"shareObjects",new Map);f(this,"shareOrNotifyMethods",new Map);f(this,"getStateMethods",new Map);f(this,"setStateMethods",new Map);f(this,"beforeEnterRoomMethods",new Map);f(this,"enterRoomAllowedMethods",new Map);f(this,"enterRoomDeniedMethods",new Map);f(this,"leaveRoomDoneMethods",new Map);f(this,"roomProfileUpdatedMethods",new Map);f(this,"peerEnteredMethods",new Map);f(this,"peerLeavedMethods",new Map);f(this,"peerProfileUpdatedMethods",new Map);f(this,"userMessageArrivedMethods",[]);f(this,"url");f(this,"ws",null);f(this,"room",{id:"",spec:{maxLog:1e3},profile:{}});f(this,"selfPeer",{id:"",order:-1,profile:{}});f(this,"peers",new Map);f(this,"currentSenderId",null);f(this,"systemMessageTypes",["Ping","Pong","EnterRoom","EnterRoomAllowed","EnterRoomDenied","LeaveRoom","LeaveRoomDone","UpdateRoomProfile","PeerArrived","PeerLeaved","UpdatePeerProfile","DefineFunction","DefineObject","InvokeFunction","UpdateObjectState","InvokeMethod"]);n&&(this.room={...this.room,...n}),s&&(this.selfPeer={...this.selfPeer,...s,order:-1}),this.interimQueue=new Array;const o=e.indexOf("?")!=-1?"&":"?";if(e.match(/^wss?:\/\//))this.url=`${e}${o}authToken=${t}`,this.room.id=e.split("rooms/")[1].split("?")[0];else{const r=document.querySelector("script[src$='madoi.js']").src.split("/",5),l=(r[0]=="http:"?"ws:":"wss:")+"//"+r[2]+"/"+r[3];this.url=`${l}/rooms/${e}${o}authToken=${t}`,this.room.id=e}this.ws=new WebSocket(this.url),this.ws.onopen=r=>this.handleOnOpen(r),this.ws.onclose=r=>this.handleOnClose(r),this.ws.onerror=r=>this.handleOnError(r),this.ws.onmessage=r=>this.handleOnMessage(r),setInterval(()=>{this.saveStates()},1e3),setInterval(()=>{this.sendPing()},3e4)}getRoomId(){return this.room.id}getRoomProfile(){var e;return(e=this.room)==null?void 0:e.profile}setRoomProfile(e,t){const s={};s[e]=t,this.sendMessage(P({updates:s}))}removeRoomProfile(e){this.sendMessage(P({deletes:[e]}))}getSelfPeerId(){var e;return(e=this.selfPeer)==null?void 0:e.id}getSelfPeerProfile(){return this.selfPeer.profile}updateSelfPeerProfile(e,t){this.selfPeer.profile[e]=t;const s={};s[e]=t,this.sendMessage(g({updates:s}));const n={updates:s,peerId:this.selfPeer.id};for(const[o,r]of this.peerProfileUpdatedMethods)r(n,this);this.dispatchCustomEvent("peerProfileUpdated",n)}removeSelfPeerProfile(e){delete this.selfPeer.profile[e],this.sendMessage(g({deletes:[e]}));const t={deletes:[e],peerId:this.selfPeer.id};for(const[s,n]of this.peerProfileUpdatedMethods)n(t,this);this.dispatchCustomEvent("peerProfileUpdated",t)}isMessageProcessing(){return this.currentSenderId!==null}getCurrentSender(){return this.currentSenderId===null?null:this.peers.get(this.currentSenderId)}isCurrentSenderSelf(){return this.currentSenderId===this.selfPeer.id}close(){var e;(e=this.ws)==null||e.close(),this.ws=null}sendPing(){var e;(e=this.ws)==null||e.send(JSON.stringify(E()))}handleOnOpen(e){var t;this.connecting=!0;for(const[s,n]of this.beforeEnterRoomMethods)n(this.selfPeer.profile,this);this.doSendMessage(j({room:this.room,selfPeer:this.selfPeer}));for(let s of this.interimQueue)(t=this.ws)==null||t.send(JSON.stringify(s));this.interimQueue=[]}handleOnClose(e){console.debug(`websocket closed because: ${e.reason}.`),this.connecting=!1,this.ws=null}handleOnError(e){}handleOnMessage(e){const t=JSON.parse(e.data);this.currentSenderId=t.sender;try{this.data(t)}finally{this.currentSenderId=null}}data(e){if(e.type!="Pong")if(e.type==="EnterRoomAllowed"){const t=e;for(const[s,n]of this.enterRoomAllowedMethods)n(t,this);this.room=e.room,this.selfPeer.order=e.selfPeer.order,this.peers.set(t.selfPeer.id,{...t.selfPeer,profile:this.selfPeer.profile});for(const s of t.otherPeers)this.peers.set(s.id,s);if(this.dispatchCustomEvent("enterRoomAllowed",t),e.histories)for(const s of e.histories)this.data(s)}else if(e.type==="EnterRoomDenied"){const s=e;for(const[n,o]of this.enterRoomDeniedMethods)o(s,this);this.dispatchCustomEvent("enterRoomDenied",s)}else if(e.type=="LeaveRoomDone"){for(const[t,s]of this.leaveRoomDoneMethods)s(this);this.dispatchCustomEvent("leaveRoomDone")}else if(e.type==="UpdateRoomProfile"){const t=e;if(t.updates)for(const[n,o]of Object.entries(t.updates))this.room.profile[n]=o;if(t.deletes)for(const n of t.deletes)delete this.room.profile[n];const s={updates:t.updates,deletes:t.deletes};for(const[n,o]of this.roomProfileUpdatedMethods)o(s,this);this.dispatchCustomEvent("roomProfileUpdated",s)}else if(e.type==="PeerEntered"){const t=e;this.peers.set(t.peer.id,t.peer);for(const[s,n]of this.peerEnteredMethods)n(t,this);this.dispatchCustomEvent("peerEntered",t)}else if(e.type==="PeerLeaved"){const t=e;this.peers.delete(e.peerId);for(const[s,n]of this.peerLeavedMethods)n(t,this);this.dispatchCustomEvent("peerLeaved",t)}else if(e.type==="UpdatePeerProfile"){const t=this.peers.get(e.sender);if(e.sender&&t){if(e.updates)for(const[n,o]of Object.entries(e.updates))t.profile[n]=o;if(e.deletes)for(const n of e.deletes)delete t.profile[n];const s={...e,peerId:e.sender};for(const[n,o]of this.peerProfileUpdatedMethods)o(s,this);this.dispatchCustomEvent("peerProfileUpdated",s)}}else if(e.type==="InvokeFunction"){const t=`${e.funcId}`,s=this.distributedFuncs.get(t);if(s===void 0){console.warn("no suitable function for ",e);return}const n=this.applyInvocation(s.original,e.args);n instanceof Promise&&n.then(()=>{var o;(o=s.resolve)==null||o.apply(null,arguments)}).catch(()=>{var o;(o=s.reject)==null||o.apply(null,arguments)})}else if(e.type==="UpdateObjectState"){const t=this.setStateMethods.get(e.objId);t&&t(e.state,e.objRevision);const s=this.shareObjects.get(e.objId);s&&(s.revision=e.objRevision,s.update=0)}else if(e.type==="InvokeMethod"){const t=this.shareObjects.get(e.objId);if(t===void 0){console.error(`Object not found for id: ${e.objId}.`,e);return}const s=`${e.objId}:${e.methodId}`,n=this.shareOrNotifyMethods.get(s);if(n===void 0){console.error(`Method not found for id: ${s}.`,e);return}n.config.distributed&&(n.config.distributed.serialized&&t.revision+1!==e.serverObjRevision&&console.error(`Found inconsistency. serverObjRevision must be ${t.revision+1} but ${e.serverObjRevision}.`,e),t.revision++,t.update++);const o=this.applyInvocation(n.original,e.args);o instanceof Promise&&o.then(function(){var r;(r=n.resolve)==null||r.apply(null,arguments)}).catch(function(){var r;(r=n.reject)==null||r.apply(null,arguments)})}else if(e.type){const t=e;for(const s of this.userMessageArrivedMethods)s.config.type===e.type&&s.method(t,this);this.dispatchEvent(new CustomEvent(e.type,{detail:e}))}else console.warn("Unknown message type.",e)}isSystemMessageType(e){return e in this.systemMessageTypes}send(e,t,s="BROADCAST"){this.ws&&this.sendMessage({type:e,sender:this.selfPeer.id,castType:s,recipients:void 0,content:t})}unicast(e,t,s){this.sendMessage({type:e,sender:this.selfPeer.id,castType:"UNICAST",recipients:[s],content:t})}multicast(e,t,s){this.sendMessage({type:e,sender:this.selfPeer.id,castType:"MULTICAST",recipients:s,content:t})}broadcast(e,t){this.sendMessage({type:e,sender:this.selfPeer.id,castType:"BROADCAST",recipients:void 0,content:t})}othercast(e,t){this.sendMessage({type:e,sender:this.selfPeer.id,castType:"OTHERCAST",recipients:void 0,content:t})}sendMessage(e){if(this.isSystemMessageType(e.type))throw new Error("システムメッセージは送信できません。");this.doSendMessage(e)}addReceiver(e,t){if(this.isSystemMessageType(e))throw new Error("システムメッセージのレシーバは登録できません。");this.addEventListener(e,t)}removeReceiver(e,t){this.removeEventListener(e,t)}replacer(e,t){return t instanceof Map?Object.fromEntries(t):t}doSendMessage(e){var t;this.connecting?(t=this.ws)==null||t.send(JSON.stringify(e,this.replacer)):this.interimQueue.push(e)}registerFunction(e,t={distributed:w}){if(t.hostOnly)return this.addHostOnlyFunction(e,t);if(t.distributed||t.changeState){const s=e.name,n=this.distributedFuncs.size,o=this.createFunctionProxy(e,t,n),r=function(){return o.apply(null,arguments)};return this.doSendMessage(C({definition:{funcId:n,name:s,config:t}})),r}return e}register(e,t=[]){if(!this.ws)return e;const s=e;if(s.madoiObjectId_)return console.warn("Ignore object registration because it's already registered."),e;let n=s.constructor.name;s.__proto__.constructor.madoiClassConfig_&&(n=s.__proto__.constructor.madoiClassConfig_.className);const o=this.shareObjects.size,r={instance:s,revision:0,update:0};this.shareObjects.set(o,r),s.madoiObjectId_=o;const l=new Array,u=new Array,y=new Map;Object.getOwnPropertyNames(Object.getPrototypeOf(s)).forEach(h=>{const c=s[h];if(typeof c!="function"||!c.madoiMethodConfig_)return;const p=c.madoiMethodConfig_,a=l.length;y.set(h,a),l.push(c),u.push({methodId:a,name:h,config:p}),console.debug(`add config ${n}.${h}=${JSON.stringify(p)} from decorator`)});for(const h of t){const c=h.method,p=h,a=c.name,S=y.get(a);if(typeof S>"u"){p.distributed&&(p.distributed={...w,...p.distributed}),p.getState&&(p.getState={...T,...p.distributed});const U=l.length;y.set(a,U),l.push(c),u.push({methodId:U,name:h.method.name,config:p}),console.debug(`add config ${n}.${a}=${JSON.stringify(h)} from argument`)}else u[S].config={...u[S].config,...h},console.debug(`merge config ${n}.${a}=${JSON.stringify(h)} from argument`)}for(let h=0;h<l.length;h++){const c=l[h],p=u[h],a=p.config;a.distributed||a.changeState?s[p.name]=this.createMethodProxy(c.bind(s),a,o,p.methodId):a.hostOnly?s[p.name]=this.addHostOnlyFunction(c.bind(s),p.config,o):a.getState?this.getStateMethods.set(o,{method:c.bind(s),config:a.getState,firstObjModified:-1,lastObjModified:-1}):a.setState?this.setStateMethods.set(o,c.bind(s)):a.beforeEnterRoom?this.beforeEnterRoomMethods.set(o,c.bind(s)):a.enterRoomAllowed?this.enterRoomAllowedMethods.set(o,c.bind(s)):a.enterRoomDenied?this.enterRoomDeniedMethods.set(o,c.bind(s)):a.leaveRoomDone?this.leaveRoomDoneMethods.set(o,c.bind(s)):a.peerEntered?this.peerEnteredMethods.set(o,c.bind(s)):a.peerProfileUpdated?this.peerProfileUpdatedMethods.set(o,c.bind(s)):a.peerLeaved?this.peerLeavedMethods.set(o,c.bind(s)):a.userMessageArrived&&this.userMessageArrivedMethods.push({method:c.bind(s),config:a.userMessageArrived})}return this.doSendMessage(_({definition:{objId:o,className:n,methods:u}})),e}createFunctionProxy(e,t,s){const n=`${s}`,o={original:e,config:t};this.distributedFuncs.set(n,o),o.promise=new Promise((l,u)=>{o.resolve=l,o.reject=u});const r=this;return function(){if(r.ws===null){if(e)return e.apply(null,arguments)}else{let l=null,u="BROADCAST";return t.distributed&&!t.distributed.serialized&&(l=e.apply(null,arguments),u="OTHERCAST"),r.sendMessage(D(u,{funcId:s,args:Array.from(arguments)})),l??o.promise}}}createMethodProxy(e,t,s,n){const o=`${s}:${n}`,r={original:e,config:t};this.shareOrNotifyMethods.set(o,r),r.promise=new Promise((u,y)=>{r.resolve=u,r.reject=y});const l=this;return function(){if(l.ws===null){if(e)return e.apply(null,[...arguments,l])}else{let u=null,y="BROADCAST";const h=l.shareObjects.get(s),c=h.revision;return t.distributed&&!t.distributed.serialized&&(u=e.apply(null,[...arguments,l]),y="OTHERCAST"),t.changeState&&(h.revision++,h.update++),l.sendMessage(A(y,{objId:s,objRevision:c,methodId:n,args:Array.from(arguments)})),u??r.promise}}}addHostOnlyFunction(e,t,s){const n=this;return function(){n.isSelfPeerHost()&&(t.changeState&&s!==void 0&&n.objectChanged(s),e.apply(null,[...arguments,n]))}}objectChanged(e){const t=this.shareObjects.get(e);t.revision++,t.update++;const s=this.getStateMethods.get(e);if(!s)return;const n=performance.now();s.firstObjModified==-1&&(s.firstObjModified=n),s.lastObjModified=n}saveStates(){if(!(!this.ws||!this.connecting)&&this.isSelfPeerHost())for(let[e,t]of this.shareObjects){if(t.update==0)continue;const s=this.getStateMethods.get(e);if(!s)continue;const n=s.config,o=performance.now(),r=s.firstObjModified,l=s.lastObjModified;(o-l>=(n.minInterval||0)||o-r>=(n.maxInterval||0))&&(this.doSendMessage(I({objId:e,objRevision:t.revision,state:s.method(this)})),s.firstObjModified=-1,s.lastObjModified=-1,t.update=0,console.debug(`state saved: ${e}`))}}applyInvocation(e,t){return e.apply(null,t)}isSelfPeerHost(){for(const e of this.peers.values())if(e.order<this.selfPeer.order)return!1;return!0}}i.BeforeEnterRoom=J,i.ChangeState=F,i.ClassName=L,i.Distributed=k,i.EnterRoomAllowed=z,i.EnterRoomDenied=Q,i.GetState=N,i.HostOnly=B,i.LeaveRoomDone=G,i.Madoi=Y,i.PeerEntered=V,i.PeerLeaved=W,i.PeerProfileUpdated=K,i.RoomProfileUpdated=q,i.SetState=H,i.UserMessageArrived=X,i.newDefineFunction=C,i.newDefineObject=_,i.newEnterRoom=j,i.newInvokeFunction=D,i.newInvokeMethod=A,i.newLeaveRoom=$,i.newPing=E,i.newUpdateObjectState=I,i.newUpdatePeerProfile=g,i.newUpdateRoomProfile=P,Object.defineProperty(i,Symbol.toStringTag,{value:"Module"})});