@lemoncloud/chatic-sockets-lib 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/client-socket-v2/common.d.ts +4 -0
- package/dist/client-socket-v2/common.js +21 -0
- package/dist/client-socket-v2/connection-rotation-controller.d.ts +26 -0
- package/dist/client-socket-v2/connection-rotation-controller.js +79 -0
- package/dist/client-socket-v2/create-client-socket-v2.d.ts +2 -0
- package/dist/client-socket-v2/create-client-socket-v2.js +164 -0
- package/dist/client-socket-v2/create-device-runtime.d.ts +19 -0
- package/dist/client-socket-v2/create-device-runtime.js +53 -0
- package/dist/client-socket-v2/domain-sync-plan.d.ts +1 -0
- package/dist/client-socket-v2/domain-sync-plan.js +2 -0
- package/dist/client-socket-v2/gateways/create-domain-gateway.d.ts +6 -0
- package/dist/client-socket-v2/gateways/create-domain-gateway.js +19 -0
- package/dist/client-socket-v2/gateways/device-gateway.d.ts +8 -0
- package/dist/client-socket-v2/gateways/device-gateway.js +13 -0
- package/dist/client-socket-v2/index.d.ts +23 -0
- package/dist/client-socket-v2/index.js +41 -0
- package/dist/client-socket-v2/keep-alive-loop.d.ts +29 -0
- package/dist/client-socket-v2/keep-alive-loop.js +93 -0
- package/dist/client-socket-v2/message-router.d.ts +9 -0
- package/dist/client-socket-v2/message-router.js +45 -0
- package/dist/client-socket-v2/pending-request-store.d.ts +23 -0
- package/dist/client-socket-v2/pending-request-store.js +85 -0
- package/dist/client-socket-v2/plans/device-sync-plan.d.ts +30 -0
- package/dist/client-socket-v2/plans/device-sync-plan.js +64 -0
- package/dist/client-socket-v2/reconnect-controller.d.ts +32 -0
- package/dist/client-socket-v2/reconnect-controller.js +120 -0
- package/dist/client-socket-v2/shared-timer-scheduler.d.ts +17 -0
- package/dist/client-socket-v2/shared-timer-scheduler.js +72 -0
- package/dist/client-socket-v2/socket-runtime.d.ts +32 -0
- package/dist/client-socket-v2/socket-runtime.js +52 -0
- package/dist/client-socket-v2/socket-transport.d.ts +18 -0
- package/dist/client-socket-v2/socket-transport.js +124 -0
- package/dist/client-socket-v2/sync-scheduler.d.ts +36 -0
- package/dist/client-socket-v2/sync-scheduler.js +189 -0
- package/dist/client-socket-v2/types.d.ts +135 -0
- package/dist/client-socket-v2/types.js +2 -0
- package/dist/lib/device/contracts.d.ts +36 -0
- package/dist/lib/device/contracts.js +7 -0
- package/dist/lib/device/types.d.ts +55 -0
- package/dist/lib/device/types.js +2 -0
- package/dist/lib/sockets/packets.d.ts +67 -0
- package/dist/lib/sockets/packets.js +2 -0
- package/dist/lib/types.d.ts +122 -0
- package/dist/lib/types.js +11 -0
- package/package.json +31 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SocketRuntime = void 0;
|
|
13
|
+
const reconnect_controller_1 = require("./reconnect-controller");
|
|
14
|
+
const connection_rotation_controller_1 = require("./connection-rotation-controller");
|
|
15
|
+
const keep_alive_loop_1 = require("./keep-alive-loop");
|
|
16
|
+
const shared_timer_scheduler_1 = require("./shared-timer-scheduler");
|
|
17
|
+
const sync_scheduler_1 = require("./sync-scheduler");
|
|
18
|
+
class SocketRuntime {
|
|
19
|
+
constructor(options) {
|
|
20
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.start = () => __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
this.keepAlive.start();
|
|
24
|
+
this.rotation.start();
|
|
25
|
+
yield this.reconnect.start();
|
|
26
|
+
});
|
|
27
|
+
this.stop = () => __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
this.rotation.stop();
|
|
29
|
+
this.keepAlive.stop();
|
|
30
|
+
yield this.reconnect.stop();
|
|
31
|
+
});
|
|
32
|
+
this.startSync = (target) => this.scheduler.start(target);
|
|
33
|
+
this.stopSync = (target) => this.scheduler.stop(target);
|
|
34
|
+
this.stopAllSync = () => this.scheduler.stopAll();
|
|
35
|
+
this.listSyncTargets = () => this.scheduler.list();
|
|
36
|
+
this.updateLocalSnapshot = (target, snapshot) => this.scheduler.updateLocalSnapshot(target, snapshot);
|
|
37
|
+
this.timerScheduler = (_b = (_a = options.timerScheduler) !== null && _a !== void 0 ? _a : options.client.timerScheduler) !== null && _b !== void 0 ? _b : new shared_timer_scheduler_1.InMemorySharedTimerScheduler();
|
|
38
|
+
this.scheduler =
|
|
39
|
+
(_c = options.scheduler) !== null && _c !== void 0 ? _c : new sync_scheduler_1.DomainSyncScheduler({
|
|
40
|
+
client: options.client,
|
|
41
|
+
plans: (_d = options.syncPlans) !== null && _d !== void 0 ? _d : [],
|
|
42
|
+
timerScheduler: this.timerScheduler,
|
|
43
|
+
});
|
|
44
|
+
this.keepAlive =
|
|
45
|
+
(_e = options.keepAlive) !== null && _e !== void 0 ? _e : new keep_alive_loop_1.KeepAliveLoop(Object.assign({ client: options.client, timerScheduler: this.timerScheduler }, (options.keepAliveOptions || {})));
|
|
46
|
+
this.reconnect =
|
|
47
|
+
(_f = options.reconnect) !== null && _f !== void 0 ? _f : new reconnect_controller_1.AutoReconnectController(Object.assign({ client: options.client, timerScheduler: this.timerScheduler }, (options.reconnectOptions || {})));
|
|
48
|
+
this.rotation =
|
|
49
|
+
(_g = options.rotation) !== null && _g !== void 0 ? _g : new connection_rotation_controller_1.ConnectionRotationController(Object.assign({ client: options.client, reconnect: this.reconnect, timerScheduler: this.timerScheduler }, (options.rotationOptions || {})));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.SocketRuntime = SocketRuntime;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ClientSocketState, SocketFactoryContext, SocketLike, SocketTransport, SocketTransportEventMap } from './types';
|
|
2
|
+
export declare class WebSocketTransport implements SocketTransport {
|
|
3
|
+
readonly url: string;
|
|
4
|
+
readonly protocols?: string | string[];
|
|
5
|
+
private readonly socketFactory;
|
|
6
|
+
private _state;
|
|
7
|
+
private socket?;
|
|
8
|
+
private unbinders;
|
|
9
|
+
private readonly listeners;
|
|
10
|
+
constructor(url: string, protocols?: string | string[], socketFactory?: (context: SocketFactoryContext) => SocketLike);
|
|
11
|
+
get state(): ClientSocketState;
|
|
12
|
+
on: <TType extends keyof SocketTransportEventMap>(type: TType, listener: (event: SocketTransportEventMap[TType]) => void) => (() => void);
|
|
13
|
+
connect: () => Promise<void>;
|
|
14
|
+
disconnect: (code?: number, reason?: string) => Promise<void>;
|
|
15
|
+
send: (raw: string) => void;
|
|
16
|
+
private emit;
|
|
17
|
+
private cleanupSocket;
|
|
18
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.WebSocketTransport = void 0;
|
|
13
|
+
const common_1 = require("./common");
|
|
14
|
+
const OPEN_STATE = 1;
|
|
15
|
+
const bindSocketListener = (socket, type, listener) => {
|
|
16
|
+
if (socket.addEventListener) {
|
|
17
|
+
socket.addEventListener(type, listener);
|
|
18
|
+
return () => { var _a; return (_a = socket.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(socket, type, listener); };
|
|
19
|
+
}
|
|
20
|
+
const key = `on${type}`;
|
|
21
|
+
const prev = socket[key];
|
|
22
|
+
socket[key] = listener;
|
|
23
|
+
return () => {
|
|
24
|
+
if (socket[key] === listener)
|
|
25
|
+
socket[key] = prev !== null && prev !== void 0 ? prev : null;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
class WebSocketTransport {
|
|
29
|
+
constructor(url, protocols, socketFactory = context => new globalThis.WebSocket(context.url, context.protocols)) {
|
|
30
|
+
this.url = url;
|
|
31
|
+
this.protocols = protocols;
|
|
32
|
+
this.socketFactory = socketFactory;
|
|
33
|
+
this._state = 'idle';
|
|
34
|
+
this.unbinders = [];
|
|
35
|
+
this.listeners = new Map();
|
|
36
|
+
this.on = (type, listener) => {
|
|
37
|
+
var _a;
|
|
38
|
+
const bucket = (_a = this.listeners.get(type)) !== null && _a !== void 0 ? _a : new Set();
|
|
39
|
+
bucket.add(listener);
|
|
40
|
+
this.listeners.set(type, bucket);
|
|
41
|
+
return () => {
|
|
42
|
+
bucket.delete(listener);
|
|
43
|
+
if (!bucket.size)
|
|
44
|
+
this.listeners.delete(type);
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
this.connect = () => __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
if (this._state === 'connected')
|
|
49
|
+
return;
|
|
50
|
+
if (this._state === 'connecting')
|
|
51
|
+
return;
|
|
52
|
+
this.cleanupSocket();
|
|
53
|
+
this._state = 'connecting';
|
|
54
|
+
const socket = this.socketFactory({ url: this.url, protocols: this.protocols });
|
|
55
|
+
this.socket = socket;
|
|
56
|
+
yield new Promise((resolve, reject) => {
|
|
57
|
+
let connectSettled = false;
|
|
58
|
+
const settleConnect = (handler) => {
|
|
59
|
+
if (connectSettled)
|
|
60
|
+
return;
|
|
61
|
+
connectSettled = true;
|
|
62
|
+
handler();
|
|
63
|
+
};
|
|
64
|
+
this.unbinders = [
|
|
65
|
+
bindSocketListener(socket, 'open', () => {
|
|
66
|
+
this._state = 'connected';
|
|
67
|
+
this.emit('open', undefined);
|
|
68
|
+
settleConnect(() => resolve());
|
|
69
|
+
}),
|
|
70
|
+
bindSocketListener(socket, 'close', event => {
|
|
71
|
+
this._state = 'closed';
|
|
72
|
+
this.emit('close', {
|
|
73
|
+
code: event === null || event === void 0 ? void 0 : event.code,
|
|
74
|
+
reason: event === null || event === void 0 ? void 0 : event.reason,
|
|
75
|
+
wasClean: event === null || event === void 0 ? void 0 : event.wasClean,
|
|
76
|
+
});
|
|
77
|
+
settleConnect(() => resolve());
|
|
78
|
+
}),
|
|
79
|
+
bindSocketListener(socket, 'error', event => {
|
|
80
|
+
var _a;
|
|
81
|
+
const error = (_a = event === null || event === void 0 ? void 0 : event.error) !== null && _a !== void 0 ? _a : new Error(`socket transport error`);
|
|
82
|
+
this.emit('error', { error });
|
|
83
|
+
if (this._state !== 'connected')
|
|
84
|
+
this._state = 'closed';
|
|
85
|
+
settleConnect(() => reject(error));
|
|
86
|
+
}),
|
|
87
|
+
bindSocketListener(socket, 'message', event => {
|
|
88
|
+
this.emit('message', { data: (0, common_1.asString)(event === null || event === void 0 ? void 0 : event.data, '') });
|
|
89
|
+
}),
|
|
90
|
+
];
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
this.disconnect = (code, reason) => __awaiter(this, void 0, void 0, function* () {
|
|
94
|
+
if (!this.socket) {
|
|
95
|
+
this._state = 'closed';
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
this._state = 'closing';
|
|
99
|
+
const socket = this.socket;
|
|
100
|
+
socket.close(code, reason);
|
|
101
|
+
this.cleanupSocket();
|
|
102
|
+
this._state = 'closed';
|
|
103
|
+
this.emit('close', { code, reason, wasClean: true });
|
|
104
|
+
});
|
|
105
|
+
this.send = (raw) => {
|
|
106
|
+
if (!this.socket || this.socket.readyState !== undefined && this.socket.readyState !== OPEN_STATE) {
|
|
107
|
+
throw new Error(`503 SOCKET NOT CONNECTED - WebSocketTransport.send()`);
|
|
108
|
+
}
|
|
109
|
+
this.socket.send(raw);
|
|
110
|
+
};
|
|
111
|
+
this.emit = (type, event) => {
|
|
112
|
+
var _a;
|
|
113
|
+
(_a = this.listeners.get(type)) === null || _a === void 0 ? void 0 : _a.forEach(listener => listener(event));
|
|
114
|
+
};
|
|
115
|
+
this.cleanupSocket = () => {
|
|
116
|
+
this.unbinders.splice(0).forEach(unbind => unbind());
|
|
117
|
+
this.socket = undefined;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
get state() {
|
|
121
|
+
return this._state;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
exports.WebSocketTransport = WebSocketTransport;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ClientSocketV2, DomainSyncPlan, SharedTimerScheduler, SyncScheduler, SyncTargetDescriptor } from './types';
|
|
2
|
+
export interface SyncSchedulerOptions {
|
|
3
|
+
client: ClientSocketV2;
|
|
4
|
+
plans: DomainSyncPlan<any>[];
|
|
5
|
+
now?: () => number;
|
|
6
|
+
timerScheduler?: SharedTimerScheduler;
|
|
7
|
+
timerPrefix?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class DomainSyncScheduler implements SyncScheduler {
|
|
10
|
+
private readonly options;
|
|
11
|
+
private readonly plans;
|
|
12
|
+
private readonly targets;
|
|
13
|
+
private readonly snapshots;
|
|
14
|
+
private readonly now;
|
|
15
|
+
private readonly timerScheduler?;
|
|
16
|
+
private readonly timerPrefix;
|
|
17
|
+
private readonly unsubs;
|
|
18
|
+
constructor(options: SyncSchedulerOptions);
|
|
19
|
+
start: (target: SyncTargetDescriptor) => void;
|
|
20
|
+
stop: (target: SyncTargetDescriptor) => void;
|
|
21
|
+
stopAll: () => void;
|
|
22
|
+
list: () => SyncTargetDescriptor[];
|
|
23
|
+
updateLocalSnapshot: (target: SyncTargetDescriptor, snapshot: unknown) => void;
|
|
24
|
+
destroy: () => void;
|
|
25
|
+
private resolvePlan;
|
|
26
|
+
private findEntry;
|
|
27
|
+
private buildContext;
|
|
28
|
+
private handleConnected;
|
|
29
|
+
private stopAllTimers;
|
|
30
|
+
private clearTimer;
|
|
31
|
+
private scheduleNow;
|
|
32
|
+
private scheduleNext;
|
|
33
|
+
private toTimerKey;
|
|
34
|
+
private runEntry;
|
|
35
|
+
private handleTrigger;
|
|
36
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DomainSyncScheduler = void 0;
|
|
13
|
+
class DomainSyncScheduler {
|
|
14
|
+
constructor(options) {
|
|
15
|
+
var _a, _b;
|
|
16
|
+
this.options = options;
|
|
17
|
+
this.targets = new Map();
|
|
18
|
+
this.snapshots = new Map();
|
|
19
|
+
this.unsubs = [];
|
|
20
|
+
this.start = (target) => {
|
|
21
|
+
const plan = this.resolvePlan(target);
|
|
22
|
+
const key = plan.getKey(target);
|
|
23
|
+
const entry = this.targets.get(key);
|
|
24
|
+
if (entry) {
|
|
25
|
+
entry.target = Object.assign(Object.assign({}, entry.target), target);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.targets.set(key, { target, plan });
|
|
29
|
+
if (this.options.client.state === 'connected')
|
|
30
|
+
this.scheduleNow(key);
|
|
31
|
+
};
|
|
32
|
+
this.stop = (target) => {
|
|
33
|
+
const entry = this.findEntry(target);
|
|
34
|
+
if (!entry)
|
|
35
|
+
return;
|
|
36
|
+
this.clearTimer(entry);
|
|
37
|
+
this.targets.delete(entry.plan.getKey(entry.target));
|
|
38
|
+
this.snapshots.delete(entry.plan.getKey(entry.target));
|
|
39
|
+
};
|
|
40
|
+
this.stopAll = () => {
|
|
41
|
+
this.stopAllTimers();
|
|
42
|
+
this.targets.clear();
|
|
43
|
+
this.snapshots.clear();
|
|
44
|
+
};
|
|
45
|
+
this.list = () => [...this.targets.values()].map(entry => (Object.assign({}, entry.target)));
|
|
46
|
+
this.updateLocalSnapshot = (target, snapshot) => {
|
|
47
|
+
var _a, _b, _c;
|
|
48
|
+
const entry = (_a = this.findEntry(target)) !== null && _a !== void 0 ? _a : (() => {
|
|
49
|
+
const plan = this.resolvePlan(target);
|
|
50
|
+
const key = plan.getKey(target);
|
|
51
|
+
return { target, plan, key };
|
|
52
|
+
})();
|
|
53
|
+
const key = 'key' in entry ? entry.key : entry.plan.getKey(entry.target);
|
|
54
|
+
this.snapshots.set(key, snapshot);
|
|
55
|
+
(_c = (_b = entry.plan).updateLocalState) === null || _c === void 0 ? void 0 : _c.call(_b, entry.target, snapshot, this.buildContext());
|
|
56
|
+
};
|
|
57
|
+
this.destroy = () => {
|
|
58
|
+
this.stopAllTimers();
|
|
59
|
+
this.unsubs.splice(0).forEach(unsub => unsub());
|
|
60
|
+
};
|
|
61
|
+
this.resolvePlan = (target) => {
|
|
62
|
+
var _a;
|
|
63
|
+
const plan = this.plans.find(candidate => candidate.supports(target));
|
|
64
|
+
if (!plan)
|
|
65
|
+
throw new Error(`404 NOT FOUND - sync plan[${(_a = target === null || target === void 0 ? void 0 : target.type) !== null && _a !== void 0 ? _a : '-'}]`);
|
|
66
|
+
return plan;
|
|
67
|
+
};
|
|
68
|
+
this.findEntry = (target) => {
|
|
69
|
+
for (const entry of this.targets.values()) {
|
|
70
|
+
if (!entry.plan.supports(target))
|
|
71
|
+
continue;
|
|
72
|
+
if (entry.plan.getKey(entry.target) === entry.plan.getKey(target))
|
|
73
|
+
return entry;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
};
|
|
77
|
+
this.buildContext = () => ({
|
|
78
|
+
client: this.options.client,
|
|
79
|
+
now: this.now,
|
|
80
|
+
readSnapshot: target => {
|
|
81
|
+
const plan = this.resolvePlan(target);
|
|
82
|
+
return this.snapshots.get(plan.getKey(target));
|
|
83
|
+
},
|
|
84
|
+
writeSnapshot: (target, snapshot) => {
|
|
85
|
+
const plan = this.resolvePlan(target);
|
|
86
|
+
const key = plan.getKey(target);
|
|
87
|
+
if (snapshot === undefined) {
|
|
88
|
+
this.snapshots.delete(key);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.snapshots.set(key, snapshot);
|
|
92
|
+
},
|
|
93
|
+
requestResync: (target) => __awaiter(this, void 0, void 0, function* () {
|
|
94
|
+
const entry = this.findEntry(target);
|
|
95
|
+
const key = entry ? entry.plan.getKey(entry.target) : this.resolvePlan(target).getKey(target);
|
|
96
|
+
yield this.runEntry(key);
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
this.handleConnected = () => __awaiter(this, void 0, void 0, function* () {
|
|
100
|
+
var _c, _d;
|
|
101
|
+
const ctx = this.buildContext();
|
|
102
|
+
for (const [key, entry] of this.targets.entries()) {
|
|
103
|
+
yield ((_d = (_c = entry.plan).onConnected) === null || _d === void 0 ? void 0 : _d.call(_c, entry.target, ctx));
|
|
104
|
+
this.scheduleNow(key);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
this.stopAllTimers = () => {
|
|
108
|
+
this.targets.forEach(entry => this.clearTimer(entry));
|
|
109
|
+
};
|
|
110
|
+
this.clearTimer = (entry) => {
|
|
111
|
+
var _a;
|
|
112
|
+
(_a = this.timerScheduler) === null || _a === void 0 ? void 0 : _a.cancel(this.toTimerKey(entry.plan.getKey(entry.target)));
|
|
113
|
+
if (entry.timer)
|
|
114
|
+
clearTimeout(entry.timer);
|
|
115
|
+
entry.timer = undefined;
|
|
116
|
+
};
|
|
117
|
+
this.scheduleNow = (key) => {
|
|
118
|
+
const entry = this.targets.get(key);
|
|
119
|
+
if (!entry)
|
|
120
|
+
return;
|
|
121
|
+
this.clearTimer(entry);
|
|
122
|
+
if (this.timerScheduler) {
|
|
123
|
+
this.timerScheduler.schedule(this.toTimerKey(key), 0, () => this.runEntry(key));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
entry.timer = setTimeout(() => void this.runEntry(key), 0);
|
|
127
|
+
};
|
|
128
|
+
this.scheduleNext = (key) => {
|
|
129
|
+
var _a, _b, _c, _d;
|
|
130
|
+
const entry = this.targets.get(key);
|
|
131
|
+
if (!entry)
|
|
132
|
+
return;
|
|
133
|
+
this.clearTimer(entry);
|
|
134
|
+
if (this.options.client.state !== 'connected')
|
|
135
|
+
return;
|
|
136
|
+
const intervalMs = (_d = (_a = entry.target.intervalMs) !== null && _a !== void 0 ? _a : (_c = (_b = entry.plan).getIntervalMs) === null || _c === void 0 ? void 0 : _c.call(_b, entry.target)) !== null && _d !== void 0 ? _d : 5000;
|
|
137
|
+
if (this.timerScheduler) {
|
|
138
|
+
this.timerScheduler.schedule(this.toTimerKey(key), intervalMs, () => this.runEntry(key));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
entry.timer = setTimeout(() => void this.runEntry(key), intervalMs);
|
|
142
|
+
};
|
|
143
|
+
this.toTimerKey = (key) => `${this.timerPrefix}${key}`;
|
|
144
|
+
this.runEntry = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
145
|
+
const entry = this.targets.get(key);
|
|
146
|
+
if (!entry || this.options.client.state !== 'connected')
|
|
147
|
+
return;
|
|
148
|
+
if (entry.inFlight)
|
|
149
|
+
return;
|
|
150
|
+
const ctx = this.buildContext();
|
|
151
|
+
entry.inFlight = Promise.resolve(entry.plan.run(entry.target, ctx))
|
|
152
|
+
.catch(() => undefined)
|
|
153
|
+
.then(() => {
|
|
154
|
+
entry.inFlight = undefined;
|
|
155
|
+
this.scheduleNext(key);
|
|
156
|
+
});
|
|
157
|
+
yield entry.inFlight;
|
|
158
|
+
});
|
|
159
|
+
this.handleTrigger = (message) => __awaiter(this, void 0, void 0, function* () {
|
|
160
|
+
var _e;
|
|
161
|
+
const type = `${(_e = message === null || message === void 0 ? void 0 : message.type) !== null && _e !== void 0 ? _e : ''}`;
|
|
162
|
+
if (!type.endsWith('.sync'))
|
|
163
|
+
return;
|
|
164
|
+
const targets = [...this.targets.values()];
|
|
165
|
+
const ctx = this.buildContext();
|
|
166
|
+
yield Promise.all(targets.map((entry) => __awaiter(this, void 0, void 0, function* () {
|
|
167
|
+
if (!entry.plan.onTrigger)
|
|
168
|
+
return;
|
|
169
|
+
if (!type.startsWith(`${entry.target.type}.`))
|
|
170
|
+
return;
|
|
171
|
+
yield entry.plan.onTrigger(entry.target, message, ctx);
|
|
172
|
+
})));
|
|
173
|
+
});
|
|
174
|
+
this.plans = [...(options.plans || [])];
|
|
175
|
+
this.now = (_a = options.now) !== null && _a !== void 0 ? _a : (() => Date.now());
|
|
176
|
+
this.timerScheduler = options.timerScheduler;
|
|
177
|
+
this.timerPrefix = (_b = options.timerPrefix) !== null && _b !== void 0 ? _b : 'sync:';
|
|
178
|
+
this.unsubs.push(options.client.onState(event => {
|
|
179
|
+
if (event.next === 'connected')
|
|
180
|
+
void this.handleConnected();
|
|
181
|
+
if (event.next === 'closing' || event.next === 'closed')
|
|
182
|
+
this.stopAllTimers();
|
|
183
|
+
}));
|
|
184
|
+
this.unsubs.push(options.client.onMessage(({ message }) => {
|
|
185
|
+
void this.handleTrigger(message);
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
exports.DomainSyncScheduler = DomainSyncScheduler;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ResolveSocketPacketType, SocketMessage, SocketPacketInputType, SocketPacketRequestData, SocketPacketResponseData, SocketPacketType } from '../lib/types';
|
|
2
|
+
import type { DeviceBootstrapInput } from '../lib/device/types';
|
|
3
|
+
export declare type ClientSocketState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
4
|
+
export interface SocketLike {
|
|
5
|
+
readonly readyState?: number;
|
|
6
|
+
send(data: string): void;
|
|
7
|
+
close(code?: number, reason?: string): void;
|
|
8
|
+
addEventListener?(type: 'open' | 'close' | 'error' | 'message', listener: (event?: any) => void): void;
|
|
9
|
+
removeEventListener?(type: 'open' | 'close' | 'error' | 'message', listener: (event?: any) => void): void;
|
|
10
|
+
onopen?: ((event?: any) => void) | null;
|
|
11
|
+
onclose?: ((event?: any) => void) | null;
|
|
12
|
+
onerror?: ((event?: any) => void) | null;
|
|
13
|
+
onmessage?: ((event?: any) => void) | null;
|
|
14
|
+
}
|
|
15
|
+
export interface SocketFactoryContext {
|
|
16
|
+
url: string;
|
|
17
|
+
protocols?: string | string[];
|
|
18
|
+
}
|
|
19
|
+
export interface SocketTransportEventMap {
|
|
20
|
+
open: void;
|
|
21
|
+
close: {
|
|
22
|
+
code?: number;
|
|
23
|
+
reason?: string;
|
|
24
|
+
wasClean?: boolean;
|
|
25
|
+
};
|
|
26
|
+
error: {
|
|
27
|
+
error?: unknown;
|
|
28
|
+
};
|
|
29
|
+
message: {
|
|
30
|
+
data: string;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export interface SocketTransport {
|
|
34
|
+
readonly url: string;
|
|
35
|
+
readonly protocols?: string | string[];
|
|
36
|
+
readonly state: ClientSocketState;
|
|
37
|
+
connect(): Promise<void>;
|
|
38
|
+
disconnect(code?: number, reason?: string): Promise<void>;
|
|
39
|
+
send(raw: string): void;
|
|
40
|
+
on<TType extends keyof SocketTransportEventMap>(type: TType, listener: (event: SocketTransportEventMap[TType]) => void): () => void;
|
|
41
|
+
}
|
|
42
|
+
export interface ClientSocketOptions {
|
|
43
|
+
url: string;
|
|
44
|
+
protocols?: string | string[];
|
|
45
|
+
device?: DeviceBootstrapInput | null;
|
|
46
|
+
socketFactory?: (context: SocketFactoryContext) => SocketLike;
|
|
47
|
+
aliases?: Partial<Record<SocketPacketInputType | string, SocketPacketType>>;
|
|
48
|
+
timerScheduler?: SharedTimerScheduler;
|
|
49
|
+
requestTimeoutMs?: number;
|
|
50
|
+
createMid?: () => string;
|
|
51
|
+
now?: () => number;
|
|
52
|
+
}
|
|
53
|
+
export interface ClientSocketStateEvent {
|
|
54
|
+
prev: ClientSocketState;
|
|
55
|
+
next: ClientSocketState;
|
|
56
|
+
}
|
|
57
|
+
export interface ClientSocketErrorEvent {
|
|
58
|
+
error: unknown;
|
|
59
|
+
phase: 'connect' | 'disconnect' | 'message' | 'request' | 'transport';
|
|
60
|
+
raw?: unknown;
|
|
61
|
+
}
|
|
62
|
+
export interface ClientSocketMessageEvent<T = any> {
|
|
63
|
+
message: SocketMessage<T>;
|
|
64
|
+
raw: string;
|
|
65
|
+
}
|
|
66
|
+
export interface ClientSocketV2 {
|
|
67
|
+
readonly state: ClientSocketState;
|
|
68
|
+
readonly deviceDefaults?: DeviceBootstrapInput;
|
|
69
|
+
readonly timerScheduler?: SharedTimerScheduler;
|
|
70
|
+
connect(): Promise<void>;
|
|
71
|
+
disconnect(code?: number, reason?: string): Promise<void>;
|
|
72
|
+
send<TType extends SocketPacketInputType>(type: TType, data?: SocketPacketRequestData<ResolveSocketPacketType<TType>>): void;
|
|
73
|
+
send<T = any>(message: SocketMessage<T>): void;
|
|
74
|
+
request<TType extends SocketPacketInputType>(type: TType, data?: SocketPacketRequestData<ResolveSocketPacketType<TType>>, options?: {
|
|
75
|
+
timeoutMs?: number;
|
|
76
|
+
}): Promise<SocketPacketResponseData<ResolveSocketPacketType<TType>>>;
|
|
77
|
+
onState(listener: (event: ClientSocketStateEvent) => void): () => void;
|
|
78
|
+
onError(listener: (event: ClientSocketErrorEvent) => void): () => void;
|
|
79
|
+
onMessage(listener: (event: ClientSocketMessageEvent) => void): () => void;
|
|
80
|
+
onType<T = any>(type: string, listener: (message: SocketMessage<T>) => void): () => void;
|
|
81
|
+
}
|
|
82
|
+
export declare type SyncTargetType = string;
|
|
83
|
+
export interface SyncTargetDescriptor {
|
|
84
|
+
type: SyncTargetType;
|
|
85
|
+
id?: string;
|
|
86
|
+
intervalMs?: number;
|
|
87
|
+
meta?: Record<string, unknown>;
|
|
88
|
+
}
|
|
89
|
+
export interface DomainSyncContext {
|
|
90
|
+
client: ClientSocketV2;
|
|
91
|
+
now(): number;
|
|
92
|
+
readSnapshot<T = unknown>(target: SyncTargetDescriptor): T | undefined;
|
|
93
|
+
writeSnapshot<T = unknown>(target: SyncTargetDescriptor, snapshot: T | undefined): void;
|
|
94
|
+
requestResync(target: SyncTargetDescriptor): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
export interface DomainSyncPlan<TTarget extends SyncTargetDescriptor = SyncTargetDescriptor> {
|
|
97
|
+
readonly domain: string;
|
|
98
|
+
supports(target: SyncTargetDescriptor): target is TTarget;
|
|
99
|
+
getKey(target: TTarget): string;
|
|
100
|
+
getIntervalMs?(target: TTarget): number;
|
|
101
|
+
onConnected?(target: TTarget, ctx: DomainSyncContext): Promise<void> | void;
|
|
102
|
+
run(target: TTarget, ctx: DomainSyncContext): Promise<void>;
|
|
103
|
+
onTrigger?(target: TTarget, message: SocketMessage<any>, ctx: DomainSyncContext): Promise<void>;
|
|
104
|
+
updateLocalState?(target: TTarget, snapshot: unknown, ctx: DomainSyncContext): void;
|
|
105
|
+
}
|
|
106
|
+
export interface SyncScheduler {
|
|
107
|
+
start(target: SyncTargetDescriptor): void;
|
|
108
|
+
stop(target: SyncTargetDescriptor): void;
|
|
109
|
+
stopAll(): void;
|
|
110
|
+
list(): SyncTargetDescriptor[];
|
|
111
|
+
updateLocalSnapshot(target: SyncTargetDescriptor, snapshot: unknown): void;
|
|
112
|
+
}
|
|
113
|
+
export interface KeepAliveLoopControl {
|
|
114
|
+
start(): void;
|
|
115
|
+
stop(): void;
|
|
116
|
+
}
|
|
117
|
+
export interface ReconnectController {
|
|
118
|
+
start(): Promise<void>;
|
|
119
|
+
stop(): Promise<void>;
|
|
120
|
+
restart(): Promise<void>;
|
|
121
|
+
}
|
|
122
|
+
export interface SharedTimerScheduler {
|
|
123
|
+
schedule(key: string, delayMs: number, task: () => void | Promise<void>): void;
|
|
124
|
+
cancel(key: string): void;
|
|
125
|
+
cancelAll(prefix?: string): void;
|
|
126
|
+
}
|
|
127
|
+
export interface ClientSocketRuntime {
|
|
128
|
+
start(): Promise<void>;
|
|
129
|
+
stop(): Promise<void>;
|
|
130
|
+
startSync(target: SyncTargetDescriptor): void;
|
|
131
|
+
stopSync(target: SyncTargetDescriptor): void;
|
|
132
|
+
stopAllSync(): void;
|
|
133
|
+
listSyncTargets(): SyncTargetDescriptor[];
|
|
134
|
+
updateLocalSnapshot(target: SyncTargetDescriptor, snapshot: unknown): void;
|
|
135
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lib/device/contracts.ts`
|
|
3
|
+
* - client-safe shared device contracts.
|
|
4
|
+
* - keep this file free from server-only runtime dependencies.
|
|
5
|
+
*/
|
|
6
|
+
export declare type DeviceStatus = '' | 'green' | 'red' | 'yellow';
|
|
7
|
+
export declare type DevicePlatform = '' | 'ios' | 'android' | 'web' | 'macos' | 'windows' | 'linux';
|
|
8
|
+
/**
|
|
9
|
+
* common device shape shared across websocket client/server contracts.
|
|
10
|
+
*/
|
|
11
|
+
export interface DeviceView {
|
|
12
|
+
id?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
platform?: DevicePlatform;
|
|
15
|
+
status?: DeviceStatus;
|
|
16
|
+
tick?: number;
|
|
17
|
+
posX?: number;
|
|
18
|
+
posY?: number;
|
|
19
|
+
lastActiveAt?: number;
|
|
20
|
+
connectedAt?: number;
|
|
21
|
+
disconnectedAt?: number;
|
|
22
|
+
connId?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* mutable device body accepted by `device.save`.
|
|
26
|
+
* - `tick` is server-managed and must be ignored when provided by clients.
|
|
27
|
+
*/
|
|
28
|
+
export interface DeviceBody extends Partial<DeviceView> {
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* client bootstrap seed.
|
|
32
|
+
* - `tick` is intentionally blocked because the client must never author it.
|
|
33
|
+
*/
|
|
34
|
+
export declare type DeviceSeed = Omit<DeviceBody, 'tick'> & {
|
|
35
|
+
tick?: never;
|
|
36
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { InferSocketError, InferSocketRequest, InferSocketResponse, SocketErrorMessage, SocketRequestMessage, SocketResponseMessage } from '../types';
|
|
2
|
+
import type { DeviceBody, DeviceSeed, DeviceView } from './contracts';
|
|
3
|
+
/**
|
|
4
|
+
* request payload for `device.read`
|
|
5
|
+
* - omit `id` to read the device linked to the current connection.
|
|
6
|
+
*/
|
|
7
|
+
export interface DeviceGetRequestData {
|
|
8
|
+
id?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DeviceSyncRequestData {
|
|
11
|
+
id?: string;
|
|
12
|
+
tick?: number;
|
|
13
|
+
}
|
|
14
|
+
declare module '../types' {
|
|
15
|
+
interface SocketPacketRegistry {
|
|
16
|
+
'device.save': {
|
|
17
|
+
request: DeviceBody;
|
|
18
|
+
response: DeviceView;
|
|
19
|
+
error: null;
|
|
20
|
+
};
|
|
21
|
+
'device.read': {
|
|
22
|
+
request: DeviceGetRequestData | null;
|
|
23
|
+
response: DeviceView;
|
|
24
|
+
error: null;
|
|
25
|
+
};
|
|
26
|
+
'device.sync': {
|
|
27
|
+
request: DeviceSyncRequestData | null;
|
|
28
|
+
response: null;
|
|
29
|
+
error: null;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export declare type DeviceSaveType = 'device.save';
|
|
34
|
+
export declare type DeviceSaveResponseData = InferSocketResponse<DeviceSaveType>;
|
|
35
|
+
export declare type DeviceSaveErrorData = InferSocketError<DeviceSaveType>;
|
|
36
|
+
export declare type DeviceSaveInput = InferSocketRequest<DeviceSaveType>;
|
|
37
|
+
export declare type DeviceBootstrapInput = DeviceSeed;
|
|
38
|
+
export declare type DeviceSaveRequestMessage = SocketRequestMessage<DeviceSaveType>;
|
|
39
|
+
export declare type DeviceSaveResponseMessage = SocketResponseMessage<DeviceSaveType>;
|
|
40
|
+
export declare type DeviceSaveErrorMessage = SocketErrorMessage<DeviceSaveType>;
|
|
41
|
+
export declare type DeviceGetType = 'device.read';
|
|
42
|
+
export declare type DeviceGetResponseData = InferSocketResponse<DeviceGetType>;
|
|
43
|
+
export declare type DeviceGetErrorData = InferSocketError<DeviceGetType>;
|
|
44
|
+
export declare type DeviceGetInput = InferSocketRequest<DeviceGetType>;
|
|
45
|
+
export declare type DeviceGetRequestMessage = SocketRequestMessage<DeviceGetType>;
|
|
46
|
+
export declare type DeviceGetResponseMessage = SocketResponseMessage<DeviceGetType>;
|
|
47
|
+
export declare type DeviceGetErrorMessage = SocketErrorMessage<DeviceGetType>;
|
|
48
|
+
export declare type DeviceSyncType = 'device.sync';
|
|
49
|
+
export declare type DeviceSyncResponseData = InferSocketResponse<DeviceSyncType>;
|
|
50
|
+
export declare type DeviceSyncErrorData = InferSocketError<DeviceSyncType>;
|
|
51
|
+
export declare type DeviceSyncInput = InferSocketRequest<DeviceSyncType>;
|
|
52
|
+
export declare type DeviceSyncRequestMessage = SocketRequestMessage<DeviceSyncType>;
|
|
53
|
+
export declare type DeviceSyncResponseMessage = SocketResponseMessage<DeviceSyncType>;
|
|
54
|
+
export declare type DeviceSyncErrorMessage = SocketErrorMessage<DeviceSyncType>;
|
|
55
|
+
export type { DeviceBody, DeviceSeed, DeviceView };
|