@lemoncloud/chatic-sockets-lib 0.2.0 → 0.3.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/dist/client-socket-v2/create-client-socket-v2.js +5 -0
- package/dist/client-socket-v2/gateways/auth-gateway.d.ts +6 -0
- package/dist/client-socket-v2/gateways/auth-gateway.js +11 -0
- package/dist/client-socket-v2/gateways/channel-gateway.d.ts +38 -0
- package/dist/client-socket-v2/gateways/channel-gateway.js +24 -0
- package/dist/client-socket-v2/gateways/chat-gateway.d.ts +22 -0
- package/dist/client-socket-v2/gateways/chat-gateway.js +16 -0
- package/dist/client-socket-v2/gateways/cloud-gateway.d.ts +18 -0
- package/dist/client-socket-v2/gateways/cloud-gateway.js +14 -0
- package/dist/client-socket-v2/gateways/place-gateway.d.ts +18 -0
- package/dist/client-socket-v2/gateways/place-gateway.js +14 -0
- package/dist/client-socket-v2/gateways/profile-gateway.d.ts +18 -0
- package/dist/client-socket-v2/gateways/profile-gateway.js +14 -0
- package/dist/client-socket-v2/gateways/user-gateway.d.ts +38 -0
- package/dist/client-socket-v2/gateways/user-gateway.js +18 -0
- package/dist/client-socket-v2/index.d.ts +13 -0
- package/dist/client-socket-v2/index.js +12 -0
- package/dist/client-socket-v2/plans/channel-sync-plan.d.ts +33 -0
- package/dist/client-socket-v2/plans/channel-sync-plan.js +66 -0
- package/dist/client-socket-v2/plans/device-sync-plan.d.ts +5 -1
- package/dist/client-socket-v2/plans/device-sync-plan.js +6 -3
- package/dist/client-socket-v2/plans/place-sync-plan.d.ts +33 -0
- package/dist/client-socket-v2/plans/place-sync-plan.js +67 -0
- package/dist/client-socket-v2/plans/profile-sync-plan.d.ts +33 -0
- package/dist/client-socket-v2/plans/profile-sync-plan.js +67 -0
- package/dist/client-socket-v2/socket-runtime.d.ts +5 -1
- package/dist/client-socket-v2/socket-runtime.js +5 -1
- package/dist/client-socket-v2/socket-transport.d.ts +14 -4
- package/dist/client-socket-v2/socket-transport.js +58 -60
- package/dist/client-socket-v2/sync-scheduler.d.ts +15 -1
- package/dist/client-socket-v2/sync-scheduler.js +89 -12
- package/dist/client-socket-v2/types.d.ts +38 -7
- package/dist/lib/channel/types.d.ts +191 -0
- package/dist/lib/channel/types.js +2 -0
- package/dist/lib/chat/types.d.ts +79 -0
- package/dist/lib/chat/types.js +2 -0
- package/dist/lib/cloud/types.d.ts +54 -0
- package/dist/lib/cloud/types.js +2 -0
- package/dist/lib/device/contracts.d.ts +2 -0
- package/dist/lib/device/types.d.ts +1 -0
- package/dist/lib/place/types.d.ts +53 -0
- package/dist/lib/place/types.js +2 -0
- package/dist/lib/profile/types.d.ts +55 -0
- package/dist/lib/profile/types.js +2 -0
- package/dist/lib/socket-actions.d.ts +291 -0
- package/dist/lib/socket-actions.js +209 -0
- package/dist/lib/socket-inputs.d.ts +15 -0
- package/dist/lib/socket-inputs.js +8 -0
- package/dist/lib/sockets/types.d.ts +27 -0
- package/dist/lib/sockets/types.js +17 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/user/types.d.ts +102 -0
- package/dist/lib/user/types.js +2 -0
- package/dist/modules/chat/model.d.ts +107 -0
- package/dist/modules/chat/model.js +28 -0
- package/dist/modules/chat/types.d.ts +65 -0
- package/dist/modules/chat/types.js +55 -0
- package/dist/modules/chat/views.d.ts +50 -0
- package/dist/modules/chat/views.js +23 -0
- package/dist/modules/sockets/model.d.ts +204 -0
- package/dist/modules/sockets/model.js +28 -0
- package/dist/modules/sockets/types.d.ts +88 -0
- package/dist/modules/sockets/types.js +78 -0
- package/dist/modules/sockets/views.d.ts +67 -0
- package/dist/modules/sockets/views.js +23 -0
- package/package.json +4 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ProfileView } from '@lemoncloud/chatic-socials-api';
|
|
2
|
+
import type { SocketMessage } from '../../lib/types';
|
|
3
|
+
import type { DomainSyncContext, DomainSyncPlan, SyncBackoffOptions, SyncFailureInfo, SyncTargetDescriptor } from '../types';
|
|
4
|
+
export interface ProfileSyncTarget extends SyncTargetDescriptor {
|
|
5
|
+
type: 'profile';
|
|
6
|
+
}
|
|
7
|
+
export interface ProfileSyncSnapshot {
|
|
8
|
+
id?: string;
|
|
9
|
+
updatedAt?: number;
|
|
10
|
+
view?: ProfileView;
|
|
11
|
+
}
|
|
12
|
+
export interface ProfileSyncPlanOptions {
|
|
13
|
+
intervalMs?: number;
|
|
14
|
+
idleBackoff?: SyncBackoffOptions;
|
|
15
|
+
resetSnapshotOnConnected?: boolean;
|
|
16
|
+
onUpdate?: (target: ProfileSyncTarget, view: ProfileView, previous?: ProfileSyncSnapshot) => void;
|
|
17
|
+
onRemove?: (target: ProfileSyncTarget, previous?: ProfileSyncSnapshot) => void;
|
|
18
|
+
}
|
|
19
|
+
/** Single-profile sync: polls `profile.get`, applies on `updatedAt` change, re-pulls on a `profile.sync` nudge. */
|
|
20
|
+
export declare class ProfileSyncPlan implements DomainSyncPlan<ProfileSyncTarget> {
|
|
21
|
+
private readonly options;
|
|
22
|
+
readonly domain = "profile";
|
|
23
|
+
readonly idleBackoff: SyncBackoffOptions;
|
|
24
|
+
constructor(options?: ProfileSyncPlanOptions);
|
|
25
|
+
supports: (target: SyncTargetDescriptor) => target is ProfileSyncTarget;
|
|
26
|
+
getKey: (target: ProfileSyncTarget) => string;
|
|
27
|
+
getIntervalMs: (target: ProfileSyncTarget) => number;
|
|
28
|
+
onConnected: (target: ProfileSyncTarget, ctx: DomainSyncContext) => void;
|
|
29
|
+
run: (target: ProfileSyncTarget, ctx: DomainSyncContext) => Promise<void>;
|
|
30
|
+
onTrigger: (target: ProfileSyncTarget, message: SocketMessage<any>, ctx: DomainSyncContext) => Promise<void>;
|
|
31
|
+
onStopped: (target: ProfileSyncTarget, _info: SyncFailureInfo, ctx: DomainSyncContext) => void;
|
|
32
|
+
updateLocalState: (target: ProfileSyncTarget, snapshot: unknown, ctx: DomainSyncContext) => void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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.ProfileSyncPlan = void 0;
|
|
13
|
+
/** Single-profile sync: polls `profile.get`, applies on `updatedAt` change, re-pulls on a `profile.sync` nudge. */
|
|
14
|
+
class ProfileSyncPlan {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
var _a;
|
|
17
|
+
this.options = options;
|
|
18
|
+
this.domain = 'profile';
|
|
19
|
+
this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'profile';
|
|
20
|
+
this.getKey = (target) => { var _a; return `profile:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
|
|
21
|
+
this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 2000; };
|
|
22
|
+
this.onConnected = (target, ctx) => {
|
|
23
|
+
if (this.options.resetSnapshotOnConnected === false)
|
|
24
|
+
return;
|
|
25
|
+
ctx.writeSnapshot(target, undefined);
|
|
26
|
+
};
|
|
27
|
+
this.run = (target, ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
var _b, _c, _d, _e;
|
|
29
|
+
if (!target.id)
|
|
30
|
+
return;
|
|
31
|
+
const prev = ctx.readSnapshot(target);
|
|
32
|
+
const input = { id: target.id };
|
|
33
|
+
// profile.get 응답은 socials 소유(registry response=unknown) → ProfileView 로 단언
|
|
34
|
+
const view = (yield ctx.client.request('profile.get', input));
|
|
35
|
+
const nextUpdatedAt = typeof (view === null || view === void 0 ? void 0 : view.updatedAt) === 'number' ? view.updatedAt : undefined;
|
|
36
|
+
const prevUpdatedAt = typeof (prev === null || prev === void 0 ? void 0 : prev.updatedAt) === 'number' ? prev.updatedAt : undefined;
|
|
37
|
+
if (prevUpdatedAt === nextUpdatedAt && (prev === null || prev === void 0 ? void 0 : prev.view))
|
|
38
|
+
return;
|
|
39
|
+
const next = {
|
|
40
|
+
id: `${(_c = (_b = view === null || view === void 0 ? void 0 : view.id) !== null && _b !== void 0 ? _b : target.id) !== null && _c !== void 0 ? _c : ''}` || undefined,
|
|
41
|
+
updatedAt: nextUpdatedAt,
|
|
42
|
+
view,
|
|
43
|
+
};
|
|
44
|
+
ctx.writeSnapshot(target, next);
|
|
45
|
+
(_e = (_d = this.options).onUpdate) === null || _e === void 0 ? void 0 : _e.call(_d, target, view, prev);
|
|
46
|
+
});
|
|
47
|
+
this.onTrigger = (target, message, ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
48
|
+
const data = ((message === null || message === void 0 ? void 0 : message.data) || {});
|
|
49
|
+
if (target.id && (data === null || data === void 0 ? void 0 : data.id) && target.id !== data.id)
|
|
50
|
+
return;
|
|
51
|
+
yield this.run(target, ctx);
|
|
52
|
+
});
|
|
53
|
+
this.onStopped = (target, _info, ctx) => {
|
|
54
|
+
var _a, _b;
|
|
55
|
+
const prev = ctx.readSnapshot(target);
|
|
56
|
+
(_b = (_a = this.options).onRemove) === null || _b === void 0 ? void 0 : _b.call(_a, target, prev);
|
|
57
|
+
};
|
|
58
|
+
this.updateLocalState = (target, snapshot, ctx) => {
|
|
59
|
+
var _a, _b;
|
|
60
|
+
const prev = ctx.readSnapshot(target);
|
|
61
|
+
const patch = (snapshot || {});
|
|
62
|
+
ctx.writeSnapshot(target, Object.assign(Object.assign(Object.assign({}, prev), patch), { view: (_a = patch.view) !== null && _a !== void 0 ? _a : prev === null || prev === void 0 ? void 0 : prev.view, updatedAt: (_b = patch.updatedAt) !== null && _b !== void 0 ? _b : prev === null || prev === void 0 ? void 0 : prev.updatedAt }));
|
|
63
|
+
};
|
|
64
|
+
this.idleBackoff = (_a = options.idleBackoff) !== null && _a !== void 0 ? _a : { factor: 2, maxMs: 60000 };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.ProfileSyncPlan = ProfileSyncPlan;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AutoReconnectController } from './reconnect-controller';
|
|
2
2
|
import { ConnectionRotationController } from './connection-rotation-controller';
|
|
3
3
|
import { KeepAliveLoop } from './keep-alive-loop';
|
|
4
|
-
import
|
|
4
|
+
import { type SyncBackoffOptions } from './sync-scheduler';
|
|
5
|
+
import type { ClientSocketRuntime, ClientSocketV2, DomainSyncPlan, KeepAliveLoopControl, ReconnectController, SharedTimerScheduler, SyncFailurePolicy, SyncScheduler, SyncTargetDescriptor } from './types';
|
|
5
6
|
export interface ClientSocketRuntimeOptions {
|
|
6
7
|
client: ClientSocketV2;
|
|
7
8
|
scheduler?: SyncScheduler;
|
|
@@ -10,6 +11,9 @@ export interface ClientSocketRuntimeOptions {
|
|
|
10
11
|
rotation?: ConnectionRotationController;
|
|
11
12
|
timerScheduler?: SharedTimerScheduler;
|
|
12
13
|
syncPlans?: DomainSyncPlan<any>[];
|
|
14
|
+
syncBackoff?: SyncBackoffOptions;
|
|
15
|
+
syncIdleBackoff?: SyncBackoffOptions;
|
|
16
|
+
failurePolicy?: SyncFailurePolicy;
|
|
13
17
|
keepAliveOptions?: Omit<ConstructorParameters<typeof KeepAliveLoop>[0], 'client' | 'timerScheduler'>;
|
|
14
18
|
reconnectOptions?: Omit<ConstructorParameters<typeof AutoReconnectController>[0], 'client'>;
|
|
15
19
|
rotationOptions?: Omit<ConstructorParameters<typeof ConnectionRotationController>[0], 'client' | 'reconnect'>;
|
|
@@ -34,12 +34,16 @@ class SocketRuntime {
|
|
|
34
34
|
this.stopAllSync = () => this.scheduler.stopAll();
|
|
35
35
|
this.listSyncTargets = () => this.scheduler.list();
|
|
36
36
|
this.updateLocalSnapshot = (target, snapshot) => this.scheduler.updateLocalSnapshot(target, snapshot);
|
|
37
|
-
this.timerScheduler =
|
|
37
|
+
this.timerScheduler =
|
|
38
|
+
(_b = (_a = options.timerScheduler) !== null && _a !== void 0 ? _a : options.client.timerScheduler) !== null && _b !== void 0 ? _b : new shared_timer_scheduler_1.InMemorySharedTimerScheduler();
|
|
38
39
|
this.scheduler =
|
|
39
40
|
(_c = options.scheduler) !== null && _c !== void 0 ? _c : new sync_scheduler_1.DomainSyncScheduler({
|
|
40
41
|
client: options.client,
|
|
41
42
|
plans: (_d = options.syncPlans) !== null && _d !== void 0 ? _d : [],
|
|
42
43
|
timerScheduler: this.timerScheduler,
|
|
44
|
+
backoff: options.syncBackoff,
|
|
45
|
+
idleBackoff: options.syncIdleBackoff,
|
|
46
|
+
failurePolicy: options.failurePolicy,
|
|
43
47
|
});
|
|
44
48
|
this.keepAlive =
|
|
45
49
|
(_e = options.keepAlive) !== null && _e !== void 0 ? _e : (options.keepAliveOptions
|
|
@@ -3,13 +3,21 @@ export interface WebSocketTransportOptions {
|
|
|
3
3
|
/** connect() open 이벤트 대기 timeout. 만료 시 socket close + Promise reject. 0이면 무한 대기. 기본 10000 */
|
|
4
4
|
connectTimeoutMs?: number;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* `SocketTransport` adapter over lemon-model's owned WebSocket network.
|
|
8
|
+
*
|
|
9
|
+
* Raw network concerns (socket creation, OPEN send guard, actual close, raw event mapping) are owned by
|
|
10
|
+
* `OwnedWebSocketNetwork`. This adapter only translates that single-shot network into chatic's reconnectable,
|
|
11
|
+
* code-carrying, 4-event / 5-state lifecycle. The connect timeout stays here because it belongs to the
|
|
12
|
+
* reconnect/backoff policy, not the raw transport.
|
|
13
|
+
*/
|
|
6
14
|
export declare class WebSocketTransport implements SocketTransport {
|
|
7
15
|
readonly url: string;
|
|
8
16
|
readonly protocols?: string | string[];
|
|
9
|
-
private readonly socketFactory
|
|
17
|
+
private readonly socketFactory?;
|
|
10
18
|
private _state;
|
|
11
|
-
private
|
|
12
|
-
private
|
|
19
|
+
private network?;
|
|
20
|
+
private networkUnsubs;
|
|
13
21
|
private readonly listeners;
|
|
14
22
|
private readonly connectTimeoutMs;
|
|
15
23
|
private connecting?;
|
|
@@ -21,5 +29,7 @@ export declare class WebSocketTransport implements SocketTransport {
|
|
|
21
29
|
disconnect: (code?: number, reason?: string) => Promise<void>;
|
|
22
30
|
send: (raw: string) => void;
|
|
23
31
|
private emit;
|
|
24
|
-
|
|
32
|
+
/** bridge chatic's `SocketLike` factory to the owned network's `WebSocketClosable` factory */
|
|
33
|
+
private buildSocketFactory;
|
|
34
|
+
private cleanupNetwork;
|
|
25
35
|
}
|
|
@@ -10,30 +10,24 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.WebSocketTransport = void 0;
|
|
13
|
-
const
|
|
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
|
-
};
|
|
13
|
+
const lemon_model_1 = require("lemon-model");
|
|
28
14
|
const DEFAULT_CONNECT_TIMEOUT_MS = 10000;
|
|
15
|
+
/**
|
|
16
|
+
* `SocketTransport` adapter over lemon-model's owned WebSocket network.
|
|
17
|
+
*
|
|
18
|
+
* Raw network concerns (socket creation, OPEN send guard, actual close, raw event mapping) are owned by
|
|
19
|
+
* `OwnedWebSocketNetwork`. This adapter only translates that single-shot network into chatic's reconnectable,
|
|
20
|
+
* code-carrying, 4-event / 5-state lifecycle. The connect timeout stays here because it belongs to the
|
|
21
|
+
* reconnect/backoff policy, not the raw transport.
|
|
22
|
+
*/
|
|
29
23
|
class WebSocketTransport {
|
|
30
|
-
constructor(url, protocols, socketFactory
|
|
24
|
+
constructor(url, protocols, socketFactory, options) {
|
|
31
25
|
var _a;
|
|
32
26
|
this.url = url;
|
|
33
27
|
this.protocols = protocols;
|
|
34
28
|
this.socketFactory = socketFactory;
|
|
35
29
|
this._state = 'idle';
|
|
36
|
-
this.
|
|
30
|
+
this.networkUnsubs = [];
|
|
37
31
|
this.listeners = new Map();
|
|
38
32
|
this.on = (type, listener) => {
|
|
39
33
|
var _a;
|
|
@@ -51,17 +45,23 @@ class WebSocketTransport {
|
|
|
51
45
|
return;
|
|
52
46
|
if (this.connecting)
|
|
53
47
|
return this.connecting;
|
|
54
|
-
this.
|
|
48
|
+
this.cleanupNetwork();
|
|
55
49
|
this._state = 'connecting';
|
|
56
|
-
const
|
|
57
|
-
|
|
50
|
+
const network = (0, lemon_model_1.createOwnedWebSocketNetwork)({
|
|
51
|
+
url: this.url,
|
|
52
|
+
protocols: this.protocols,
|
|
53
|
+
socketFactory: this.buildSocketFactory(),
|
|
54
|
+
connectTimeoutMs: 0, // connect timeout is owned here (reconnect policy), not by the network
|
|
55
|
+
});
|
|
56
|
+
this.network = network;
|
|
58
57
|
this.connecting = new Promise((resolve, reject) => {
|
|
59
|
-
|
|
58
|
+
var _a;
|
|
59
|
+
let settled = false;
|
|
60
60
|
let timeoutTimer;
|
|
61
61
|
const settleConnect = (handler) => {
|
|
62
|
-
if (
|
|
62
|
+
if (settled)
|
|
63
63
|
return;
|
|
64
|
-
|
|
64
|
+
settled = true;
|
|
65
65
|
if (timeoutTimer)
|
|
66
66
|
clearTimeout(timeoutTimer);
|
|
67
67
|
this.connecting = undefined;
|
|
@@ -71,75 +71,73 @@ class WebSocketTransport {
|
|
|
71
71
|
this.cancelConnecting = error => settleConnect(() => reject(error));
|
|
72
72
|
if (this.connectTimeoutMs > 0) {
|
|
73
73
|
timeoutTimer = setTimeout(() => {
|
|
74
|
-
if (connectSettled)
|
|
75
|
-
return;
|
|
76
74
|
const error = new Error(`408 CONNECT TIMEOUT - WebSocketTransport.connect() after ${this.connectTimeoutMs}ms`);
|
|
77
75
|
settleConnect(() => reject(error));
|
|
78
|
-
|
|
79
|
-
socket.close(1000, 'connect-timeout');
|
|
80
|
-
}
|
|
81
|
-
catch (_a) {
|
|
82
|
-
/** ignore — best-effort cleanup */
|
|
83
|
-
}
|
|
76
|
+
network.close(1000, 'connect-timeout');
|
|
84
77
|
}, this.connectTimeoutMs);
|
|
85
78
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
this.emit('close', {
|
|
95
|
-
code: event === null || event === void 0 ? void 0 : event.code,
|
|
96
|
-
reason: event === null || event === void 0 ? void 0 : event.reason,
|
|
97
|
-
wasClean: event === null || event === void 0 ? void 0 : event.wasClean,
|
|
98
|
-
});
|
|
99
|
-
settleConnect(() => resolve());
|
|
100
|
-
}),
|
|
101
|
-
bindSocketListener(socket, 'error', event => {
|
|
79
|
+
const unsubOpen = (_a = network.onOpen) === null || _a === void 0 ? void 0 : _a.call(network, () => {
|
|
80
|
+
this._state = 'connected';
|
|
81
|
+
this.emit('open', undefined);
|
|
82
|
+
settleConnect(() => resolve());
|
|
83
|
+
});
|
|
84
|
+
this.networkUnsubs = [
|
|
85
|
+
network.onMessage(raw => this.emit('message', { data: raw })),
|
|
86
|
+
network.onError((event, context) => {
|
|
102
87
|
var _a;
|
|
103
|
-
|
|
88
|
+
if (context.scope === 'ownedWebSocket.close') {
|
|
89
|
+
const close = event;
|
|
90
|
+
this._state = 'closed';
|
|
91
|
+
this.emit('close', { code: close === null || close === void 0 ? void 0 : close.code, reason: close === null || close === void 0 ? void 0 : close.reason, wasClean: close === null || close === void 0 ? void 0 : close.wasClean });
|
|
92
|
+
settleConnect(() => resolve()); // close-before-open resolves connect (reconnect drives off state)
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const error = (_a = event === null || event === void 0 ? void 0 : event.error) !== null && _a !== void 0 ? _a : (event instanceof Error ? event : new Error(`socket transport error`));
|
|
104
96
|
this.emit('error', { error });
|
|
105
97
|
if (this._state !== 'connected')
|
|
106
98
|
this._state = 'closed';
|
|
107
99
|
settleConnect(() => reject(error));
|
|
108
100
|
}),
|
|
109
|
-
bindSocketListener(socket, 'message', event => {
|
|
110
|
-
this.emit('message', { data: (0, common_1.asString)(event === null || event === void 0 ? void 0 : event.data, '') });
|
|
111
|
-
}),
|
|
112
101
|
];
|
|
102
|
+
if (unsubOpen)
|
|
103
|
+
this.networkUnsubs.push(unsubOpen);
|
|
113
104
|
});
|
|
114
105
|
return this.connecting;
|
|
115
106
|
});
|
|
116
107
|
this.disconnect = (code, reason) => __awaiter(this, void 0, void 0, function* () {
|
|
117
108
|
var _b;
|
|
118
|
-
if (!this.
|
|
109
|
+
if (!this.network) {
|
|
119
110
|
this._state = 'closed';
|
|
120
111
|
return;
|
|
121
112
|
}
|
|
122
113
|
this._state = 'closing';
|
|
123
|
-
const
|
|
124
|
-
|
|
114
|
+
const network = this.network;
|
|
115
|
+
network.close(code, reason);
|
|
125
116
|
(_b = this.cancelConnecting) === null || _b === void 0 ? void 0 : _b.call(this, new Error(`499 CLIENT CLOSED REQUEST - disconnect during connecting`));
|
|
126
|
-
this.
|
|
117
|
+
this.cleanupNetwork();
|
|
127
118
|
this._state = 'closed';
|
|
128
119
|
this.emit('close', { code, reason, wasClean: true });
|
|
129
120
|
});
|
|
130
121
|
this.send = (raw) => {
|
|
131
|
-
if (!this.
|
|
122
|
+
if (!this.network || this.network.readyState !== 'open') {
|
|
132
123
|
throw new Error(`503 SOCKET NOT CONNECTED - WebSocketTransport.send()`);
|
|
133
124
|
}
|
|
134
|
-
this.
|
|
125
|
+
this.network.send(raw);
|
|
135
126
|
};
|
|
136
127
|
this.emit = (type, event) => {
|
|
137
128
|
var _a;
|
|
138
129
|
(_a = this.listeners.get(type)) === null || _a === void 0 ? void 0 : _a.forEach(listener => listener(event));
|
|
139
130
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
/** bridge chatic's `SocketLike` factory to the owned network's `WebSocketClosable` factory */
|
|
132
|
+
this.buildSocketFactory = () => {
|
|
133
|
+
const factory = this.socketFactory;
|
|
134
|
+
if (!factory)
|
|
135
|
+
return undefined;
|
|
136
|
+
return context => factory({ url: context.url, protocols: context.protocols });
|
|
137
|
+
};
|
|
138
|
+
this.cleanupNetwork = () => {
|
|
139
|
+
this.networkUnsubs.splice(0).forEach(unsub => unsub());
|
|
140
|
+
this.network = undefined;
|
|
143
141
|
};
|
|
144
142
|
this.connectTimeoutMs = (_a = options === null || options === void 0 ? void 0 : options.connectTimeoutMs) !== null && _a !== void 0 ? _a : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
145
143
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type { ClientSocketV2, DomainSyncPlan, SharedTimerScheduler, SyncScheduler, SyncTargetDescriptor } from './types';
|
|
1
|
+
import type { ClientSocketV2, DomainSyncPlan, SharedTimerScheduler, SyncBackoffOptions, SyncFailurePolicy, SyncScheduler, SyncTargetDescriptor } from './types';
|
|
2
|
+
export type { SyncBackoffOptions } from './types';
|
|
2
3
|
export interface SyncSchedulerOptions {
|
|
3
4
|
client: ClientSocketV2;
|
|
4
5
|
plans: DomainSyncPlan<any>[];
|
|
5
6
|
now?: () => number;
|
|
6
7
|
timerScheduler?: SharedTimerScheduler;
|
|
7
8
|
timerPrefix?: string;
|
|
9
|
+
backoff?: SyncBackoffOptions;
|
|
10
|
+
idleBackoff?: SyncBackoffOptions;
|
|
11
|
+
failurePolicy?: SyncFailurePolicy;
|
|
12
|
+
jitterRatio?: number;
|
|
13
|
+
random?: () => number;
|
|
8
14
|
}
|
|
9
15
|
export declare class DomainSyncScheduler implements SyncScheduler {
|
|
10
16
|
private readonly options;
|
|
@@ -14,6 +20,11 @@ export declare class DomainSyncScheduler implements SyncScheduler {
|
|
|
14
20
|
private readonly now;
|
|
15
21
|
private readonly timerScheduler?;
|
|
16
22
|
private readonly timerPrefix;
|
|
23
|
+
private readonly backoff;
|
|
24
|
+
private readonly idleBackoff?;
|
|
25
|
+
private readonly failurePolicy;
|
|
26
|
+
private readonly jitterRatio;
|
|
27
|
+
private readonly random;
|
|
17
28
|
private readonly unsubs;
|
|
18
29
|
constructor(options: SyncSchedulerOptions);
|
|
19
30
|
start: (target: SyncTargetDescriptor) => void;
|
|
@@ -30,7 +41,10 @@ export declare class DomainSyncScheduler implements SyncScheduler {
|
|
|
30
41
|
private clearTimer;
|
|
31
42
|
private scheduleNow;
|
|
32
43
|
private scheduleNext;
|
|
44
|
+
private computeInterval;
|
|
45
|
+
private jitter;
|
|
33
46
|
private toTimerKey;
|
|
34
47
|
private runEntry;
|
|
48
|
+
private handleRunFailure;
|
|
35
49
|
private handleTrigger;
|
|
36
50
|
}
|
|
@@ -10,9 +10,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.DomainSyncScheduler = void 0;
|
|
13
|
+
const defaultClassify = (error) => {
|
|
14
|
+
var _a;
|
|
15
|
+
const msg = error instanceof Error ? error.message : `${error !== null && error !== void 0 ? error : ''}`;
|
|
16
|
+
const code = (_a = /^\s*(\d{3})\b/.exec(msg)) === null || _a === void 0 ? void 0 : _a[1];
|
|
17
|
+
return code === '403' || code === '404' ? 'gone' : 'transient';
|
|
18
|
+
};
|
|
19
|
+
const DEFAULT_IDLE_BACKOFF = { factor: 2, maxMs: 60000 };
|
|
13
20
|
class DomainSyncScheduler {
|
|
14
21
|
constructor(options) {
|
|
15
|
-
var _a, _b;
|
|
22
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
16
23
|
this.options = options;
|
|
17
24
|
this.targets = new Map();
|
|
18
25
|
this.snapshots = new Map();
|
|
@@ -95,12 +102,17 @@ class DomainSyncScheduler {
|
|
|
95
102
|
const key = entry ? entry.plan.getKey(entry.target) : this.resolvePlan(target).getKey(target);
|
|
96
103
|
yield this.runEntry(key);
|
|
97
104
|
}),
|
|
105
|
+
stop: target => this.stop(target),
|
|
98
106
|
});
|
|
99
107
|
this.handleConnected = () => __awaiter(this, void 0, void 0, function* () {
|
|
100
|
-
var
|
|
108
|
+
var _k, _l;
|
|
101
109
|
const ctx = this.buildContext();
|
|
102
110
|
for (const [key, entry] of this.targets.entries()) {
|
|
103
|
-
|
|
111
|
+
// reconnect = fresh start
|
|
112
|
+
entry.failures = 0;
|
|
113
|
+
entry.goneStreak = 0;
|
|
114
|
+
entry.idleStreak = 0;
|
|
115
|
+
yield ((_l = (_k = entry.plan).onConnected) === null || _l === void 0 ? void 0 : _l.call(_k, entry.target, ctx));
|
|
104
116
|
this.scheduleNow(key);
|
|
105
117
|
}
|
|
106
118
|
});
|
|
@@ -133,13 +145,28 @@ class DomainSyncScheduler {
|
|
|
133
145
|
this.clearTimer(entry);
|
|
134
146
|
if (this.options.client.state !== 'connected')
|
|
135
147
|
return;
|
|
136
|
-
const
|
|
148
|
+
const baseMs = (_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;
|
|
149
|
+
const intervalMs = this.computeInterval(entry, baseMs);
|
|
137
150
|
if (this.timerScheduler) {
|
|
138
151
|
this.timerScheduler.schedule(this.toTimerKey(key), intervalMs, () => this.runEntry(key));
|
|
139
152
|
return;
|
|
140
153
|
}
|
|
141
154
|
entry.timer = setTimeout(() => void this.runEntry(key), intervalMs);
|
|
142
155
|
};
|
|
156
|
+
this.computeInterval = (entry, baseMs) => {
|
|
157
|
+
var _a, _b, _c, _d, _e, _f;
|
|
158
|
+
const failures = (_a = entry.failures) !== null && _a !== void 0 ? _a : 0;
|
|
159
|
+
if (failures > 0)
|
|
160
|
+
return this.jitter(Math.min(baseMs * Math.pow(this.backoff.factor, failures), this.backoff.maxMs));
|
|
161
|
+
const idle = (_c = (_b = entry.plan.idleBackoff) !== null && _b !== void 0 ? _b : this.idleBackoff) !== null && _c !== void 0 ? _c : DEFAULT_IDLE_BACKOFF;
|
|
162
|
+
const idleFactor = Math.max(1, (_d = idle === null || idle === void 0 ? void 0 : idle.factor) !== null && _d !== void 0 ? _d : 1);
|
|
163
|
+
const idleStreak = (_e = entry.idleStreak) !== null && _e !== void 0 ? _e : 0;
|
|
164
|
+
if (idleFactor > 1 && idleStreak > 0) {
|
|
165
|
+
return this.jitter(Math.min(baseMs * Math.pow(idleFactor, idleStreak), (_f = idle === null || idle === void 0 ? void 0 : idle.maxMs) !== null && _f !== void 0 ? _f : 60000));
|
|
166
|
+
}
|
|
167
|
+
return baseMs;
|
|
168
|
+
};
|
|
169
|
+
this.jitter = (ms) => this.jitterRatio > 0 ? Math.max(0, Math.round(ms * (1 + (this.random() * 2 - 1) * this.jitterRatio))) : ms;
|
|
143
170
|
this.toTimerKey = (key) => `${this.timerPrefix}${key}`;
|
|
144
171
|
this.runEntry = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
145
172
|
const entry = this.targets.get(key);
|
|
@@ -148,33 +175,83 @@ class DomainSyncScheduler {
|
|
|
148
175
|
if (entry.inFlight)
|
|
149
176
|
return;
|
|
150
177
|
const ctx = this.buildContext();
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
178
|
+
// no snapshot write during run = unchanged = idle
|
|
179
|
+
const before = this.snapshots.get(key);
|
|
180
|
+
entry.inFlight = Promise.resolve(entry.plan.run(entry.target, ctx)).then(() => {
|
|
181
|
+
var _a;
|
|
154
182
|
entry.inFlight = undefined;
|
|
183
|
+
entry.failures = 0;
|
|
184
|
+
entry.goneStreak = 0;
|
|
185
|
+
const changed = this.snapshots.get(key) !== before;
|
|
186
|
+
entry.idleStreak = changed ? 0 : ((_a = entry.idleStreak) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
155
187
|
this.scheduleNext(key);
|
|
188
|
+
}, error => {
|
|
189
|
+
entry.inFlight = undefined;
|
|
190
|
+
this.handleRunFailure(key, error, ctx);
|
|
156
191
|
});
|
|
157
192
|
yield entry.inFlight;
|
|
158
193
|
});
|
|
194
|
+
this.handleRunFailure = (key, error, ctx) => {
|
|
195
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
196
|
+
const entry = this.targets.get(key);
|
|
197
|
+
if (!entry)
|
|
198
|
+
return;
|
|
199
|
+
const plan = (_a = entry.plan.failurePolicy) !== null && _a !== void 0 ? _a : {};
|
|
200
|
+
const sched = this.failurePolicy;
|
|
201
|
+
const classify = (_c = (_b = plan.classify) !== null && _b !== void 0 ? _b : sched.classify) !== null && _c !== void 0 ? _c : defaultClassify;
|
|
202
|
+
const stopAfter = (_e = (_d = plan.stopAfter) !== null && _d !== void 0 ? _d : sched.stopAfter) !== null && _e !== void 0 ? _e : 2;
|
|
203
|
+
const decide = (_g = (_f = plan.decide) !== null && _f !== void 0 ? _f : sched.decide) !== null && _g !== void 0 ? _g : ((info) => (info.kind === 'gone' && info.goneStreak >= stopAfter ? 'stop' : 'retry'));
|
|
204
|
+
const kind = classify(error, entry.target);
|
|
205
|
+
entry.failures = ((_h = entry.failures) !== null && _h !== void 0 ? _h : 0) + 1;
|
|
206
|
+
entry.goneStreak = kind === 'gone' ? ((_j = entry.goneStreak) !== null && _j !== void 0 ? _j : 0) + 1 : 0;
|
|
207
|
+
entry.idleStreak = 0;
|
|
208
|
+
const info = {
|
|
209
|
+
target: Object.assign({}, entry.target),
|
|
210
|
+
error,
|
|
211
|
+
kind,
|
|
212
|
+
failures: entry.failures,
|
|
213
|
+
goneStreak: entry.goneStreak,
|
|
214
|
+
};
|
|
215
|
+
const decision = decide(info);
|
|
216
|
+
if (decision === 'stop') {
|
|
217
|
+
(_l = (_k = entry.plan).onStopped) === null || _l === void 0 ? void 0 : _l.call(_k, entry.target, info, ctx);
|
|
218
|
+
this.stop(entry.target);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (decision === 'keep') {
|
|
222
|
+
entry.failures = 0;
|
|
223
|
+
entry.goneStreak = 0;
|
|
224
|
+
}
|
|
225
|
+
this.scheduleNext(key);
|
|
226
|
+
};
|
|
159
227
|
this.handleTrigger = (message) => __awaiter(this, void 0, void 0, function* () {
|
|
160
|
-
var
|
|
161
|
-
const type = `${(
|
|
228
|
+
var _m;
|
|
229
|
+
const type = `${(_m = message === null || message === void 0 ? void 0 : message.type) !== null && _m !== void 0 ? _m : ''}`;
|
|
162
230
|
if (!type.endsWith('.sync'))
|
|
163
231
|
return;
|
|
164
|
-
const
|
|
232
|
+
const entries = [...this.targets.entries()];
|
|
165
233
|
const ctx = this.buildContext();
|
|
166
|
-
yield Promise.all(
|
|
234
|
+
yield Promise.all(entries.map(([key, entry]) => __awaiter(this, void 0, void 0, function* () {
|
|
167
235
|
if (!entry.plan.onTrigger)
|
|
168
236
|
return;
|
|
169
237
|
if (!type.startsWith(`${entry.target.type}.`))
|
|
170
238
|
return;
|
|
171
|
-
|
|
239
|
+
// nudge resets idle backoff (best-effort)
|
|
240
|
+
entry.idleStreak = 0;
|
|
241
|
+
yield entry.plan.onTrigger(entry.target, message, ctx).catch(() => undefined);
|
|
242
|
+
if (!entry.inFlight)
|
|
243
|
+
this.scheduleNext(key);
|
|
172
244
|
})));
|
|
173
245
|
});
|
|
174
246
|
this.plans = [...(options.plans || [])];
|
|
175
247
|
this.now = (_a = options.now) !== null && _a !== void 0 ? _a : (() => Date.now());
|
|
176
248
|
this.timerScheduler = options.timerScheduler;
|
|
177
249
|
this.timerPrefix = (_b = options.timerPrefix) !== null && _b !== void 0 ? _b : 'sync:';
|
|
250
|
+
this.backoff = { factor: Math.max(1, (_d = (_c = options.backoff) === null || _c === void 0 ? void 0 : _c.factor) !== null && _d !== void 0 ? _d : 2), maxMs: (_f = (_e = options.backoff) === null || _e === void 0 ? void 0 : _e.maxMs) !== null && _f !== void 0 ? _f : 30000 };
|
|
251
|
+
this.idleBackoff = options.idleBackoff;
|
|
252
|
+
this.failurePolicy = (_g = options.failurePolicy) !== null && _g !== void 0 ? _g : {};
|
|
253
|
+
this.jitterRatio = Math.max(0, (_h = options.jitterRatio) !== null && _h !== void 0 ? _h : 0.1);
|
|
254
|
+
this.random = (_j = options.random) !== null && _j !== void 0 ? _j : Math.random;
|
|
178
255
|
this.unsubs.push(options.client.onState(event => {
|
|
179
256
|
if (event.next === 'connected')
|
|
180
257
|
void this.handleConnected();
|
|
@@ -2,20 +2,27 @@ import type { ResolveSocketPacketType, SocketMessage, SocketPacketInputType, Soc
|
|
|
2
2
|
import type { DeviceBootstrapInput } from '../lib/device/types';
|
|
3
3
|
export declare type ClientSocketState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
4
4
|
export interface SocketLike {
|
|
5
|
-
readonly readyState
|
|
5
|
+
readonly readyState: number;
|
|
6
6
|
send(data: string): void;
|
|
7
7
|
close(code?: number, reason?: string): void;
|
|
8
|
-
addEventListener
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
onerror?: ((event?: any) => void) | null;
|
|
13
|
-
onmessage?: ((event?: any) => void) | null;
|
|
8
|
+
addEventListener(type: 'open' | 'close' | 'error' | 'message', listener: (event?: any) => void, options?: {
|
|
9
|
+
once?: boolean;
|
|
10
|
+
}): void;
|
|
11
|
+
removeEventListener(type: 'open' | 'close' | 'error' | 'message', listener: (event?: any) => void): void;
|
|
14
12
|
}
|
|
15
13
|
export interface SocketFactoryContext {
|
|
16
14
|
url: string;
|
|
17
15
|
protocols?: string | string[];
|
|
18
16
|
}
|
|
17
|
+
/** context for the pre-parse raw ownership filter */
|
|
18
|
+
export interface ClientSocketInboundFilterContext {
|
|
19
|
+
raw: string;
|
|
20
|
+
}
|
|
21
|
+
/** context for the post-parse message ownership filter */
|
|
22
|
+
export interface ClientSocketMessageFilterContext<T = any> {
|
|
23
|
+
raw: string;
|
|
24
|
+
message: SocketMessage<T>;
|
|
25
|
+
}
|
|
19
26
|
export interface SocketTransportEventMap {
|
|
20
27
|
open: void;
|
|
21
28
|
close: {
|
|
@@ -64,6 +71,8 @@ export interface ClientSocketOptions {
|
|
|
64
71
|
protocols?: string | string[];
|
|
65
72
|
device?: DeviceBootstrapInput | null;
|
|
66
73
|
socketFactory?: (context: SocketFactoryContext) => SocketLike;
|
|
74
|
+
shouldHandleRaw?: (context: ClientSocketInboundFilterContext) => boolean;
|
|
75
|
+
shouldHandleMessage?: (context: ClientSocketMessageFilterContext) => boolean;
|
|
67
76
|
aliases?: Partial<Record<SocketPacketInputType | string, SocketPacketType>>;
|
|
68
77
|
timerScheduler?: SharedTimerScheduler;
|
|
69
78
|
requestTimeoutMs?: number;
|
|
@@ -114,21 +123,43 @@ export interface SyncTargetDescriptor {
|
|
|
114
123
|
intervalMs?: number;
|
|
115
124
|
meta?: Record<string, unknown>;
|
|
116
125
|
}
|
|
126
|
+
export declare type SyncFailureKind = 'gone' | 'transient';
|
|
127
|
+
export declare type SyncFailureDecision = 'stop' | 'keep' | 'retry';
|
|
128
|
+
export interface SyncFailureInfo {
|
|
129
|
+
target: SyncTargetDescriptor;
|
|
130
|
+
error: unknown;
|
|
131
|
+
kind: SyncFailureKind;
|
|
132
|
+
failures: number;
|
|
133
|
+
goneStreak: number;
|
|
134
|
+
}
|
|
135
|
+
export interface SyncFailurePolicy {
|
|
136
|
+
classify?(error: unknown, target: SyncTargetDescriptor): SyncFailureKind;
|
|
137
|
+
decide?(info: SyncFailureInfo): SyncFailureDecision;
|
|
138
|
+
stopAfter?: number;
|
|
139
|
+
}
|
|
140
|
+
export interface SyncBackoffOptions {
|
|
141
|
+
factor?: number;
|
|
142
|
+
maxMs?: number;
|
|
143
|
+
}
|
|
117
144
|
export interface DomainSyncContext {
|
|
118
145
|
client: ClientSocketV2;
|
|
119
146
|
now(): number;
|
|
120
147
|
readSnapshot<T = unknown>(target: SyncTargetDescriptor): T | undefined;
|
|
121
148
|
writeSnapshot<T = unknown>(target: SyncTargetDescriptor, snapshot: T | undefined): void;
|
|
122
149
|
requestResync(target: SyncTargetDescriptor): Promise<void>;
|
|
150
|
+
stop(target: SyncTargetDescriptor): void;
|
|
123
151
|
}
|
|
124
152
|
export interface DomainSyncPlan<TTarget extends SyncTargetDescriptor = SyncTargetDescriptor> {
|
|
125
153
|
readonly domain: string;
|
|
154
|
+
readonly failurePolicy?: SyncFailurePolicy;
|
|
155
|
+
readonly idleBackoff?: SyncBackoffOptions;
|
|
126
156
|
supports(target: SyncTargetDescriptor): target is TTarget;
|
|
127
157
|
getKey(target: TTarget): string;
|
|
128
158
|
getIntervalMs?(target: TTarget): number;
|
|
129
159
|
onConnected?(target: TTarget, ctx: DomainSyncContext): Promise<void> | void;
|
|
130
160
|
run(target: TTarget, ctx: DomainSyncContext): Promise<void>;
|
|
131
161
|
onTrigger?(target: TTarget, message: SocketMessage<any>, ctx: DomainSyncContext): Promise<void>;
|
|
162
|
+
onStopped?(target: TTarget, info: SyncFailureInfo, ctx: DomainSyncContext): void;
|
|
132
163
|
updateLocalState?(target: TTarget, snapshot: unknown, ctx: DomainSyncContext): void;
|
|
133
164
|
}
|
|
134
165
|
export interface SyncScheduler {
|