@lemoncloud/chatic-sockets-lib 0.3.1 → 0.3.3
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/gateways/join-gateway.d.ts +15 -0
- package/dist/client-socket-v2/gateways/join-gateway.js +12 -0
- package/dist/client-socket-v2/index.d.ts +4 -0
- package/dist/client-socket-v2/index.js +3 -0
- package/dist/client-socket-v2/plans/channel-sync-plan.d.ts +14 -10
- package/dist/client-socket-v2/plans/channel-sync-plan.js +6 -2
- package/dist/client-socket-v2/plans/chat-sync-plan.d.ts +63 -0
- package/dist/client-socket-v2/plans/chat-sync-plan.js +166 -0
- package/dist/client-socket-v2/plans/join-sync-plan.d.ts +37 -0
- package/dist/client-socket-v2/plans/join-sync-plan.js +71 -0
- package/dist/client-socket-v2/plans/place-sync-plan.d.ts +14 -10
- package/dist/client-socket-v2/plans/place-sync-plan.js +5 -2
- package/dist/client-socket-v2/plans/profile-sync-plan.d.ts +14 -10
- package/dist/client-socket-v2/plans/profile-sync-plan.js +5 -2
- package/dist/client-socket-v2/types.d.ts +12 -0
- package/dist/lib/channel/types.d.ts +1 -2
- package/dist/lib/chat/types.d.ts +6 -1
- package/dist/lib/chat/views.d.ts +62 -0
- package/dist/lib/chat/views.js +2 -0
- package/dist/lib/join/contracts.d.ts +32 -0
- package/dist/lib/join/contracts.js +7 -0
- package/dist/lib/join/types.d.ts +31 -0
- package/dist/lib/join/types.js +2 -0
- package/dist/lib/socket-actions.d.ts +19 -1
- package/dist/lib/socket-actions.js +13 -0
- package/dist/lib/socket-inputs.d.ts +1 -0
- package/package.json +1 -1
- package/dist/modules/chat/model.d.ts +0 -107
- package/dist/modules/chat/model.js +0 -28
- package/dist/modules/chat/types.d.ts +0 -65
- package/dist/modules/chat/types.js +0 -55
- package/dist/modules/chat/views.d.ts +0 -50
- package/dist/modules/chat/views.js +0 -23
- package/dist/modules/sockets/model.d.ts +0 -206
- package/dist/modules/sockets/model.js +0 -28
- package/dist/modules/sockets/types.d.ts +0 -100
- package/dist/modules/sockets/types.js +0 -86
- package/dist/modules/sockets/views.d.ts +0 -67
- package/dist/modules/sockets/views.js +0 -23
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { JoinGetInput, JoinUpdateInput } from '../../lib/join/types';
|
|
2
|
+
import type { ClientSocketV2 } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* join 도메인 게이트웨이.
|
|
5
|
+
* - 응답은 @lemoncloud/chatic-socials-api 소유(pass-through)이므로 제네릭 T로 호출부에서 주입한다.
|
|
6
|
+
* - 미주입 시 unknown. 각 메서드 주석의 $socials.<View> 가 실제 응답 타입.
|
|
7
|
+
* - 입력의 `id` 는 Join.id이며 클라이언트가 보관한 값을 그대로 전달한다.
|
|
8
|
+
*/
|
|
9
|
+
export interface JoinGateway {
|
|
10
|
+
/** $socials.JoinView */
|
|
11
|
+
get<T = unknown>(data: JoinGetInput): Promise<T>;
|
|
12
|
+
/** $socials.JoinView */
|
|
13
|
+
update<T = unknown>(data: JoinUpdateInput): Promise<T>;
|
|
14
|
+
}
|
|
15
|
+
export declare const createJoinGateway: (client: ClientSocketV2) => JoinGateway;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createJoinGateway = void 0;
|
|
4
|
+
const create_domain_gateway_1 = require("./create-domain-gateway");
|
|
5
|
+
const createJoinGateway = (client) => {
|
|
6
|
+
const gateway = (0, create_domain_gateway_1.createDomainGateway)('join', client);
|
|
7
|
+
return {
|
|
8
|
+
get: (data) => gateway.request('get', data),
|
|
9
|
+
update: (data) => gateway.request('update', data),
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
exports.createJoinGateway = createJoinGateway;
|
|
@@ -18,13 +18,17 @@ export * from './socket-runtime';
|
|
|
18
18
|
export * from './create-device-runtime';
|
|
19
19
|
export * from './plans/device-sync-plan';
|
|
20
20
|
export * from './plans/channel-sync-plan';
|
|
21
|
+
export * from './plans/chat-sync-plan';
|
|
22
|
+
export type { ChatSyncType, ChatSyncData, ChatSyncMessage } from '../lib/chat/types';
|
|
21
23
|
export * from './plans/place-sync-plan';
|
|
24
|
+
export * from './plans/join-sync-plan';
|
|
22
25
|
export * from './plans/profile-sync-plan';
|
|
23
26
|
export * from './gateways/create-domain-gateway';
|
|
24
27
|
export * from './gateways/device-gateway';
|
|
25
28
|
export * from './gateways/channel-gateway';
|
|
26
29
|
export * from './gateways/auth-gateway';
|
|
27
30
|
export * from './gateways/chat-gateway';
|
|
31
|
+
export * from './gateways/join-gateway';
|
|
28
32
|
export * from './gateways/cloud-gateway';
|
|
29
33
|
export * from './gateways/user-gateway';
|
|
30
34
|
export * from './gateways/place-gateway';
|
|
@@ -36,13 +36,16 @@ __exportStar(require("./socket-runtime"), exports);
|
|
|
36
36
|
__exportStar(require("./create-device-runtime"), exports);
|
|
37
37
|
__exportStar(require("./plans/device-sync-plan"), exports);
|
|
38
38
|
__exportStar(require("./plans/channel-sync-plan"), exports);
|
|
39
|
+
__exportStar(require("./plans/chat-sync-plan"), exports);
|
|
39
40
|
__exportStar(require("./plans/place-sync-plan"), exports);
|
|
41
|
+
__exportStar(require("./plans/join-sync-plan"), exports);
|
|
40
42
|
__exportStar(require("./plans/profile-sync-plan"), exports);
|
|
41
43
|
__exportStar(require("./gateways/create-domain-gateway"), exports);
|
|
42
44
|
__exportStar(require("./gateways/device-gateway"), exports);
|
|
43
45
|
__exportStar(require("./gateways/channel-gateway"), exports);
|
|
44
46
|
__exportStar(require("./gateways/auth-gateway"), exports);
|
|
45
47
|
__exportStar(require("./gateways/chat-gateway"), exports);
|
|
48
|
+
__exportStar(require("./gateways/join-gateway"), exports);
|
|
46
49
|
__exportStar(require("./gateways/cloud-gateway"), exports);
|
|
47
50
|
__exportStar(require("./gateways/user-gateway"), exports);
|
|
48
51
|
__exportStar(require("./gateways/place-gateway"), exports);
|
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
import type { ChannelView } from '../../modules/chat/views';
|
|
2
1
|
import type { SocketMessage } from '../../lib/types';
|
|
3
|
-
import type { DomainSyncContext, DomainSyncPlan
|
|
2
|
+
import type { DomainSyncContext, DomainSyncPlan } from '../types';
|
|
3
|
+
import type { SyncableView, SyncBackoffOptions, SyncFailureInfo, SyncTargetDescriptor } from '../types';
|
|
4
4
|
export interface ChannelSyncTarget extends SyncTargetDescriptor {
|
|
5
5
|
type: 'channel';
|
|
6
6
|
}
|
|
7
|
-
export interface ChannelSyncSnapshot {
|
|
7
|
+
export interface ChannelSyncSnapshot<TView extends SyncableView = SyncableView> {
|
|
8
8
|
id?: string;
|
|
9
9
|
updatedAt?: number;
|
|
10
|
-
view?:
|
|
10
|
+
view?: TView;
|
|
11
11
|
}
|
|
12
|
-
export interface ChannelSyncPlanOptions {
|
|
12
|
+
export interface ChannelSyncPlanOptions<TView extends SyncableView = SyncableView> {
|
|
13
13
|
intervalMs?: number;
|
|
14
14
|
idleBackoff?: SyncBackoffOptions;
|
|
15
15
|
resetSnapshotOnConnected?: boolean;
|
|
16
|
-
onUpdate?: (target: ChannelSyncTarget, view:
|
|
17
|
-
onRemove?: (target: ChannelSyncTarget, previous?: ChannelSyncSnapshot) => void;
|
|
16
|
+
onUpdate?: (target: ChannelSyncTarget, view: TView, previous?: ChannelSyncSnapshot<TView>) => void;
|
|
17
|
+
onRemove?: (target: ChannelSyncTarget, previous?: ChannelSyncSnapshot<TView>) => void;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Single-channel sync: polls `channel.get`, applies on `updatedAt` change, re-pulls on a `channel.sync` nudge.
|
|
21
|
+
* - Generic over the view type: inject the concrete view at the call site,
|
|
22
|
+
* e.g. `new ChannelSyncPlan<$socials.ChannelView>({ onUpdate })`.
|
|
23
|
+
*/
|
|
24
|
+
export declare class ChannelSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<ChannelSyncTarget> {
|
|
21
25
|
private readonly options;
|
|
22
26
|
readonly domain = "channel";
|
|
23
27
|
readonly idleBackoff: SyncBackoffOptions;
|
|
24
|
-
constructor(options?: ChannelSyncPlanOptions);
|
|
28
|
+
constructor(options?: ChannelSyncPlanOptions<TView>);
|
|
25
29
|
supports: (target: SyncTargetDescriptor) => target is ChannelSyncTarget;
|
|
26
30
|
getKey: (target: ChannelSyncTarget) => string;
|
|
27
31
|
getIntervalMs: (target: ChannelSyncTarget) => number;
|
|
@@ -10,7 +10,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ChannelSyncPlan = void 0;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Single-channel sync: polls `channel.get`, applies on `updatedAt` change, re-pulls on a `channel.sync` nudge.
|
|
15
|
+
* - Generic over the view type: inject the concrete view at the call site,
|
|
16
|
+
* e.g. `new ChannelSyncPlan<$socials.ChannelView>({ onUpdate })`.
|
|
17
|
+
*/
|
|
14
18
|
class ChannelSyncPlan {
|
|
15
19
|
constructor(options = {}) {
|
|
16
20
|
var _a;
|
|
@@ -30,7 +34,7 @@ class ChannelSyncPlan {
|
|
|
30
34
|
return;
|
|
31
35
|
const prev = ctx.readSnapshot(target);
|
|
32
36
|
const input = { id: target.id };
|
|
33
|
-
const view = yield ctx.client.request('channel.get', input);
|
|
37
|
+
const view = (yield ctx.client.request('channel.get', input));
|
|
34
38
|
const nextUpdatedAt = typeof (view === null || view === void 0 ? void 0 : view.updatedAt) === 'number' ? view.updatedAt : undefined;
|
|
35
39
|
const prevUpdatedAt = typeof (prev === null || prev === void 0 ? void 0 : prev.updatedAt) === 'number' ? prev.updatedAt : undefined;
|
|
36
40
|
if (prevUpdatedAt === nextUpdatedAt && (prev === null || prev === void 0 ? void 0 : prev.view))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { SocketMessage } from '../../lib/types';
|
|
2
|
+
import type { DomainSyncContext, DomainSyncPlan } from '../types';
|
|
3
|
+
import type { SyncBackoffOptions, SyncFailureInfo, SyncTargetDescriptor } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Minimal message shape the chat window requires: ordering key (`chatNo`) + channel scope (`channelId`).
|
|
6
|
+
* - The plan is generic over the message type; inject the concrete view at the call site,
|
|
7
|
+
* e.g. `new ChatSyncPlan<$socials.ChatView>({ onApply })`.
|
|
8
|
+
*/
|
|
9
|
+
export interface ChatMessageLike {
|
|
10
|
+
chatNo?: number;
|
|
11
|
+
channelId?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Sync target for one channel's chat messages. */
|
|
14
|
+
export interface ChatSyncTarget extends SyncTargetDescriptor {
|
|
15
|
+
type: 'chat';
|
|
16
|
+
}
|
|
17
|
+
/** Applied message window. `lastNo` is the top (next expected is `lastNo + 1`); messages older than `minNo` are left for the app to lazy-load. */
|
|
18
|
+
export interface ChatSyncSnapshot<TMessage extends ChatMessageLike = ChatMessageLike> {
|
|
19
|
+
id: string;
|
|
20
|
+
lastNo: number;
|
|
21
|
+
minNo: number;
|
|
22
|
+
messages: TMessage[];
|
|
23
|
+
}
|
|
24
|
+
export interface ChatSyncPlanOptions<TMessage extends ChatMessageLike = ChatMessageLike> {
|
|
25
|
+
/** Max messages fetched in one catch-up; anything older is left for the app to lazy-load. Default 50. */
|
|
26
|
+
cap?: number;
|
|
27
|
+
/** Max messages kept in the snapshot window to bound memory; the app holds full history via `onApply`. Default 500. */
|
|
28
|
+
maxMessages?: number;
|
|
29
|
+
idleBackoff?: SyncBackoffOptions;
|
|
30
|
+
/** Called after messages are applied, with the applied delta (ascending) and the new snapshot. */
|
|
31
|
+
onApply?: (target: ChatSyncTarget, applied: TMessage[], snapshot: ChatSyncSnapshot<TMessage>) => void;
|
|
32
|
+
/** Called when the engine stops the target. */
|
|
33
|
+
onRemove?: (target: ChatSyncTarget, previous?: ChatSyncSnapshot<TMessage>) => void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Per-channel chat message sync (append-only events).
|
|
37
|
+
* - `onTrigger` (chat.sync): applies the message payload directly, with no read round-trip on the normal path.
|
|
38
|
+
* - `onConnected`: catches up from `lastNo` to `channel.chatNo` (bounded by `cap`) after a reconnect.
|
|
39
|
+
* - `run`: no-op (event-driven, no polling).
|
|
40
|
+
*/
|
|
41
|
+
export declare class ChatSyncPlan<TMessage extends ChatMessageLike = ChatMessageLike> implements DomainSyncPlan<ChatSyncTarget> {
|
|
42
|
+
private readonly options;
|
|
43
|
+
readonly domain = "chat";
|
|
44
|
+
readonly idleBackoff: SyncBackoffOptions;
|
|
45
|
+
private readonly chains;
|
|
46
|
+
constructor(options?: ChatSyncPlanOptions<TMessage>);
|
|
47
|
+
supports: (target: SyncTargetDescriptor) => target is ChatSyncTarget;
|
|
48
|
+
getKey: (target: ChatSyncTarget) => string;
|
|
49
|
+
run: () => Promise<void>;
|
|
50
|
+
onConnected: (target: ChatSyncTarget, ctx: DomainSyncContext) => Promise<void>;
|
|
51
|
+
onTrigger: (target: ChatSyncTarget, message: SocketMessage<any>, ctx: DomainSyncContext) => Promise<void>;
|
|
52
|
+
onStopped: (target: ChatSyncTarget, _info: SyncFailureInfo, ctx: DomainSyncContext) => void;
|
|
53
|
+
updateLocalState: (target: ChatSyncTarget, snapshot: unknown, ctx: DomainSyncContext) => void;
|
|
54
|
+
/** Serialize read-modify-write per channel; the scheduler does not serialize onTrigger. */
|
|
55
|
+
private serialize;
|
|
56
|
+
private readSnap;
|
|
57
|
+
/** Append the contiguous `lastNo + 1 ..` run to the top of the window; gaps and duplicates are skipped. */
|
|
58
|
+
private appendContiguous;
|
|
59
|
+
/** Trim the window to `maxMessages`, dropping the oldest and advancing `minNo`. */
|
|
60
|
+
private clampWindow;
|
|
61
|
+
/** Reconnect/gap fill: collect newest-first, bound by `cap`, apply ascending, then set `lastNo` to `total`. */
|
|
62
|
+
private catchUp;
|
|
63
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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.ChatSyncPlan = void 0;
|
|
13
|
+
const num = (v, d = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : d);
|
|
14
|
+
/**
|
|
15
|
+
* Per-channel chat message sync (append-only events).
|
|
16
|
+
* - `onTrigger` (chat.sync): applies the message payload directly, with no read round-trip on the normal path.
|
|
17
|
+
* - `onConnected`: catches up from `lastNo` to `channel.chatNo` (bounded by `cap`) after a reconnect.
|
|
18
|
+
* - `run`: no-op (event-driven, no polling).
|
|
19
|
+
*/
|
|
20
|
+
class ChatSyncPlan {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
var _a;
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.domain = 'chat';
|
|
25
|
+
this.chains = new Map();
|
|
26
|
+
this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'chat';
|
|
27
|
+
this.getKey = (target) => { var _a; return `chat:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
|
|
28
|
+
this.run = () => __awaiter(this, void 0, void 0, function* () { return undefined; });
|
|
29
|
+
this.onConnected = (target, ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
30
|
+
if (!target.id)
|
|
31
|
+
return;
|
|
32
|
+
// Swallow errors so one channel's catch-up failure cannot break the scheduler's reconnect loop; recovered on the next message or reconnect.
|
|
33
|
+
yield this.serialize(target.id, () => this.catchUp(target, ctx)).catch(() => undefined);
|
|
34
|
+
});
|
|
35
|
+
this.onTrigger = (target, message, ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
36
|
+
var _b;
|
|
37
|
+
if (!target.id)
|
|
38
|
+
return;
|
|
39
|
+
const data = ((message === null || message === void 0 ? void 0 : message.data) || {});
|
|
40
|
+
const channelId = `${(_b = data === null || data === void 0 ? void 0 : data.channelId) !== null && _b !== void 0 ? _b : ''}`;
|
|
41
|
+
if (!channelId || channelId !== target.id)
|
|
42
|
+
return; // ignore other channels
|
|
43
|
+
const no = num(data === null || data === void 0 ? void 0 : data.chatNo, 0);
|
|
44
|
+
if (no <= 0)
|
|
45
|
+
return;
|
|
46
|
+
yield this.serialize(target.id, () => __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
const snap = this.readSnap(target, ctx);
|
|
48
|
+
if (no <= snap.lastNo)
|
|
49
|
+
return; // duplicate or old
|
|
50
|
+
if (no === snap.lastNo + 1) {
|
|
51
|
+
this.appendContiguous(target, ctx, snap, [data]); // apply payload directly, no feed
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
yield this.catchUp(target, ctx, snap); // gap: fill from the server
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
});
|
|
58
|
+
this.onStopped = (target, _info, ctx) => {
|
|
59
|
+
var _a, _b;
|
|
60
|
+
const prev = ctx.readSnapshot(target);
|
|
61
|
+
if (target.id)
|
|
62
|
+
this.chains.delete(target.id);
|
|
63
|
+
(_b = (_a = this.options).onRemove) === null || _b === void 0 ? void 0 : _b.call(_a, target, prev);
|
|
64
|
+
};
|
|
65
|
+
this.updateLocalState = (target, snapshot, ctx) => {
|
|
66
|
+
const prev = this.readSnap(target, ctx);
|
|
67
|
+
const patch = (snapshot || {});
|
|
68
|
+
ctx.writeSnapshot(target, Object.assign(Object.assign({}, prev), patch));
|
|
69
|
+
};
|
|
70
|
+
/** Reconnect/gap fill: collect newest-first, bound by `cap`, apply ascending, then set `lastNo` to `total`. */
|
|
71
|
+
this.catchUp = (target, ctx, snapIn) => __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
var _c, _d, _e;
|
|
73
|
+
if (!target.id)
|
|
74
|
+
return;
|
|
75
|
+
const snap = snapIn !== null && snapIn !== void 0 ? snapIn : this.readSnap(target, ctx);
|
|
76
|
+
const input = { id: target.id };
|
|
77
|
+
const channel = (yield ctx.client.request('channel.get', input));
|
|
78
|
+
const total = num(channel === null || channel === void 0 ? void 0 : channel.chatNo, 0);
|
|
79
|
+
if (total <= snap.lastNo)
|
|
80
|
+
return; // already latest
|
|
81
|
+
const cap = num(this.options.cap, 50) || 50;
|
|
82
|
+
const floor = Math.max(snap.lastNo, total - cap); // below this is left for lazy-load
|
|
83
|
+
const acc = [];
|
|
84
|
+
let cursor;
|
|
85
|
+
for (let page = 0; page < 64; page += 1) {
|
|
86
|
+
const res = (yield ctx.client.request('chat.feed', Object.assign({ channelId: target.id }, (cursor ? { cursorNo: cursor } : {}))));
|
|
87
|
+
const list = ((_c = res === null || res === void 0 ? void 0 : res.list) !== null && _c !== void 0 ? _c : []);
|
|
88
|
+
let stop = false;
|
|
89
|
+
for (const m of list) {
|
|
90
|
+
const mno = num(m.chatNo, 0);
|
|
91
|
+
if (mno <= floor) {
|
|
92
|
+
stop = true;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
acc.push(m);
|
|
96
|
+
}
|
|
97
|
+
const cn = num(res === null || res === void 0 ? void 0 : res.cursorNo, 0);
|
|
98
|
+
if (stop || cn <= 0 || !list.length)
|
|
99
|
+
break;
|
|
100
|
+
cursor = cn;
|
|
101
|
+
}
|
|
102
|
+
acc.sort((a, b) => num(a.chatNo, 0) - num(b.chatNo, 0));
|
|
103
|
+
if (floor === snap.lastNo) {
|
|
104
|
+
this.appendContiguous(target, ctx, snap, acc); // gap within cap: contiguous extend
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Large gap: reset to the newest window and set lastNo to total, which avoids a re-fetch loop.
|
|
108
|
+
const win = this.clampWindow(acc, floor + 1);
|
|
109
|
+
const next = { id: snap.id, lastNo: total, minNo: win.minNo, messages: win.messages };
|
|
110
|
+
ctx.writeSnapshot(target, next);
|
|
111
|
+
(_e = (_d = this.options).onApply) === null || _e === void 0 ? void 0 : _e.call(_d, target, acc, next);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
this.idleBackoff = (_a = options.idleBackoff) !== null && _a !== void 0 ? _a : { factor: 2, maxMs: 60000 };
|
|
115
|
+
}
|
|
116
|
+
// --- internals ---
|
|
117
|
+
/** Serialize read-modify-write per channel; the scheduler does not serialize onTrigger. */
|
|
118
|
+
serialize(key, task) {
|
|
119
|
+
var _a;
|
|
120
|
+
const prev = (_a = this.chains.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve();
|
|
121
|
+
const next = prev.then(task, task);
|
|
122
|
+
this.chains.set(key, next.then(() => undefined, () => undefined));
|
|
123
|
+
return next;
|
|
124
|
+
}
|
|
125
|
+
readSnap(target, ctx) {
|
|
126
|
+
var _a;
|
|
127
|
+
const prev = ctx.readSnapshot(target);
|
|
128
|
+
if (prev)
|
|
129
|
+
return prev;
|
|
130
|
+
return { id: `${(_a = target.id) !== null && _a !== void 0 ? _a : ''}`, lastNo: 0, minNo: 0, messages: [] };
|
|
131
|
+
}
|
|
132
|
+
/** Append the contiguous `lastNo + 1 ..` run to the top of the window; gaps and duplicates are skipped. */
|
|
133
|
+
appendContiguous(target, ctx, snap, ascending) {
|
|
134
|
+
var _a, _b;
|
|
135
|
+
const applied = [];
|
|
136
|
+
const messages = snap.messages.slice();
|
|
137
|
+
let lastNo = snap.lastNo;
|
|
138
|
+
let minNo = snap.minNo;
|
|
139
|
+
for (const m of ascending) {
|
|
140
|
+
const mno = num(m.chatNo, 0);
|
|
141
|
+
if (mno !== lastNo + 1)
|
|
142
|
+
continue;
|
|
143
|
+
messages.push(m);
|
|
144
|
+
lastNo = mno;
|
|
145
|
+
if (minNo === 0)
|
|
146
|
+
minNo = mno;
|
|
147
|
+
applied.push(m);
|
|
148
|
+
}
|
|
149
|
+
if (!applied.length)
|
|
150
|
+
return;
|
|
151
|
+
const win = this.clampWindow(messages, minNo);
|
|
152
|
+
const next = { id: snap.id, lastNo, minNo: win.minNo, messages: win.messages };
|
|
153
|
+
ctx.writeSnapshot(target, next);
|
|
154
|
+
(_b = (_a = this.options).onApply) === null || _b === void 0 ? void 0 : _b.call(_a, target, applied, next);
|
|
155
|
+
}
|
|
156
|
+
/** Trim the window to `maxMessages`, dropping the oldest and advancing `minNo`. */
|
|
157
|
+
clampWindow(messages, minNo) {
|
|
158
|
+
var _a;
|
|
159
|
+
const max = num(this.options.maxMessages, 500) || 500;
|
|
160
|
+
if (messages.length <= max)
|
|
161
|
+
return { messages, minNo };
|
|
162
|
+
const trimmed = messages.slice(messages.length - max);
|
|
163
|
+
return { messages: trimmed, minNo: num((_a = trimmed[0]) === null || _a === void 0 ? void 0 : _a.chatNo, minNo) };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.ChatSyncPlan = ChatSyncPlan;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SocketMessage } from '../../lib/types';
|
|
2
|
+
import type { DomainSyncContext, DomainSyncPlan } from '../types';
|
|
3
|
+
import type { SyncableView, SyncBackoffOptions, SyncFailureInfo, SyncTargetDescriptor } from '../types';
|
|
4
|
+
export interface JoinSyncTarget extends SyncTargetDescriptor {
|
|
5
|
+
type: 'join';
|
|
6
|
+
}
|
|
7
|
+
export interface JoinSyncSnapshot<TView extends SyncableView = SyncableView> {
|
|
8
|
+
id?: string;
|
|
9
|
+
updatedAt?: number;
|
|
10
|
+
view?: TView;
|
|
11
|
+
}
|
|
12
|
+
export interface JoinSyncPlanOptions<TView extends SyncableView = SyncableView> {
|
|
13
|
+
intervalMs?: number;
|
|
14
|
+
idleBackoff?: SyncBackoffOptions;
|
|
15
|
+
resetSnapshotOnConnected?: boolean;
|
|
16
|
+
onUpdate?: (target: JoinSyncTarget, view: TView, previous?: JoinSyncSnapshot<TView>) => void;
|
|
17
|
+
onRemove?: (target: JoinSyncTarget, previous?: JoinSyncSnapshot<TView>) => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Single-join sync: polls `join.get`, applies on `updatedAt` change, re-pulls on a `join.sync` nudge.
|
|
21
|
+
* - Generic over the view type: inject the concrete view at the call site,
|
|
22
|
+
* e.g. `new JoinSyncPlan<$socials.JoinView>({ onUpdate })`.
|
|
23
|
+
*/
|
|
24
|
+
export declare class JoinSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<JoinSyncTarget> {
|
|
25
|
+
private readonly options;
|
|
26
|
+
readonly domain = "join";
|
|
27
|
+
readonly idleBackoff: SyncBackoffOptions;
|
|
28
|
+
constructor(options?: JoinSyncPlanOptions<TView>);
|
|
29
|
+
supports: (target: SyncTargetDescriptor) => target is JoinSyncTarget;
|
|
30
|
+
getKey: (target: JoinSyncTarget) => string;
|
|
31
|
+
getIntervalMs: (target: JoinSyncTarget) => number;
|
|
32
|
+
onConnected: (target: JoinSyncTarget, ctx: DomainSyncContext) => void;
|
|
33
|
+
run: (target: JoinSyncTarget, ctx: DomainSyncContext) => Promise<void>;
|
|
34
|
+
onTrigger: (target: JoinSyncTarget, message: SocketMessage<any>, ctx: DomainSyncContext) => Promise<void>;
|
|
35
|
+
onStopped: (target: JoinSyncTarget, _info: SyncFailureInfo, ctx: DomainSyncContext) => void;
|
|
36
|
+
updateLocalState: (target: JoinSyncTarget, snapshot: unknown, ctx: DomainSyncContext) => void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
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.JoinSyncPlan = void 0;
|
|
13
|
+
/**
|
|
14
|
+
* Single-join sync: polls `join.get`, applies on `updatedAt` change, re-pulls on a `join.sync` nudge.
|
|
15
|
+
* - Generic over the view type: inject the concrete view at the call site,
|
|
16
|
+
* e.g. `new JoinSyncPlan<$socials.JoinView>({ onUpdate })`.
|
|
17
|
+
*/
|
|
18
|
+
class JoinSyncPlan {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
var _a;
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.domain = 'join';
|
|
23
|
+
this.supports = (target) => (target === null || target === void 0 ? void 0 : target.type) === 'join';
|
|
24
|
+
this.getKey = (target) => { var _a; return `join:${(_a = target === null || target === void 0 ? void 0 : target.id) !== null && _a !== void 0 ? _a : ''}`; };
|
|
25
|
+
// Default poll interval (10s); the scheduler backs this off x2 up to 60s while idle.
|
|
26
|
+
this.getIntervalMs = (target) => { var _a, _b; return (_b = (_a = target.intervalMs) !== null && _a !== void 0 ? _a : this.options.intervalMs) !== null && _b !== void 0 ? _b : 10000; };
|
|
27
|
+
this.onConnected = (target, ctx) => {
|
|
28
|
+
if (this.options.resetSnapshotOnConnected === false)
|
|
29
|
+
return;
|
|
30
|
+
ctx.writeSnapshot(target, undefined);
|
|
31
|
+
};
|
|
32
|
+
this.run = (target, ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
33
|
+
var _b, _c, _d, _e;
|
|
34
|
+
if (!target.id)
|
|
35
|
+
return;
|
|
36
|
+
const prev = ctx.readSnapshot(target);
|
|
37
|
+
const input = { id: target.id };
|
|
38
|
+
const view = (yield ctx.client.request('join.get', input));
|
|
39
|
+
const nextUpdatedAt = typeof (view === null || view === void 0 ? void 0 : view.updatedAt) === 'number' ? view.updatedAt : undefined;
|
|
40
|
+
const prevUpdatedAt = typeof (prev === null || prev === void 0 ? void 0 : prev.updatedAt) === 'number' ? prev.updatedAt : undefined;
|
|
41
|
+
if (prevUpdatedAt === nextUpdatedAt && (prev === null || prev === void 0 ? void 0 : prev.view))
|
|
42
|
+
return;
|
|
43
|
+
const next = {
|
|
44
|
+
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,
|
|
45
|
+
updatedAt: nextUpdatedAt,
|
|
46
|
+
view,
|
|
47
|
+
};
|
|
48
|
+
ctx.writeSnapshot(target, next);
|
|
49
|
+
(_e = (_d = this.options).onUpdate) === null || _e === void 0 ? void 0 : _e.call(_d, target, view, prev);
|
|
50
|
+
});
|
|
51
|
+
this.onTrigger = (target, message, ctx) => __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
const data = ((message === null || message === void 0 ? void 0 : message.data) || {});
|
|
53
|
+
if (target.id && (data === null || data === void 0 ? void 0 : data.id) && target.id !== data.id)
|
|
54
|
+
return;
|
|
55
|
+
yield this.run(target, ctx);
|
|
56
|
+
});
|
|
57
|
+
this.onStopped = (target, _info, ctx) => {
|
|
58
|
+
var _a, _b;
|
|
59
|
+
const prev = ctx.readSnapshot(target);
|
|
60
|
+
(_b = (_a = this.options).onRemove) === null || _b === void 0 ? void 0 : _b.call(_a, target, prev);
|
|
61
|
+
};
|
|
62
|
+
this.updateLocalState = (target, snapshot, ctx) => {
|
|
63
|
+
var _a, _b;
|
|
64
|
+
const prev = ctx.readSnapshot(target);
|
|
65
|
+
const patch = (snapshot || {});
|
|
66
|
+
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 }));
|
|
67
|
+
};
|
|
68
|
+
this.idleBackoff = (_a = options.idleBackoff) !== null && _a !== void 0 ? _a : { factor: 2, maxMs: 60000 };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.JoinSyncPlan = JoinSyncPlan;
|
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
import type * as $backend from '@lemoncloud/chatic-backend-api';
|
|
2
1
|
import type { SocketMessage } from '../../lib/types';
|
|
3
|
-
import type { DomainSyncContext, DomainSyncPlan
|
|
2
|
+
import type { DomainSyncContext, DomainSyncPlan } from '../types';
|
|
3
|
+
import type { SyncableView, SyncBackoffOptions, SyncFailureInfo, SyncTargetDescriptor } from '../types';
|
|
4
4
|
export interface PlaceSyncTarget extends SyncTargetDescriptor {
|
|
5
5
|
type: 'place';
|
|
6
6
|
}
|
|
7
|
-
export interface PlaceSyncSnapshot {
|
|
7
|
+
export interface PlaceSyncSnapshot<TView extends SyncableView = SyncableView> {
|
|
8
8
|
id?: string;
|
|
9
9
|
updatedAt?: number;
|
|
10
|
-
view?:
|
|
10
|
+
view?: TView;
|
|
11
11
|
}
|
|
12
|
-
export interface PlaceSyncPlanOptions {
|
|
12
|
+
export interface PlaceSyncPlanOptions<TView extends SyncableView = SyncableView> {
|
|
13
13
|
intervalMs?: number;
|
|
14
14
|
idleBackoff?: SyncBackoffOptions;
|
|
15
15
|
resetSnapshotOnConnected?: boolean;
|
|
16
|
-
onUpdate?: (target: PlaceSyncTarget, view:
|
|
17
|
-
onRemove?: (target: PlaceSyncTarget, previous?: PlaceSyncSnapshot) => void;
|
|
16
|
+
onUpdate?: (target: PlaceSyncTarget, view: TView, previous?: PlaceSyncSnapshot<TView>) => void;
|
|
17
|
+
onRemove?: (target: PlaceSyncTarget, previous?: PlaceSyncSnapshot<TView>) => void;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Single-place sync: polls `place.get`, applies on `updatedAt` change, re-pulls on a `place.sync` nudge.
|
|
21
|
+
* - Generic over the view type: inject the concrete view at the call site,
|
|
22
|
+
* e.g. `new PlaceSyncPlan<$backend.MySiteView>({ onUpdate })`.
|
|
23
|
+
*/
|
|
24
|
+
export declare class PlaceSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<PlaceSyncTarget> {
|
|
21
25
|
private readonly options;
|
|
22
26
|
readonly domain = "place";
|
|
23
27
|
readonly idleBackoff: SyncBackoffOptions;
|
|
24
|
-
constructor(options?: PlaceSyncPlanOptions);
|
|
28
|
+
constructor(options?: PlaceSyncPlanOptions<TView>);
|
|
25
29
|
supports: (target: SyncTargetDescriptor) => target is PlaceSyncTarget;
|
|
26
30
|
getKey: (target: PlaceSyncTarget) => string;
|
|
27
31
|
getIntervalMs: (target: PlaceSyncTarget) => number;
|
|
@@ -10,7 +10,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.PlaceSyncPlan = void 0;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Single-place sync: polls `place.get`, applies on `updatedAt` change, re-pulls on a `place.sync` nudge.
|
|
15
|
+
* - Generic over the view type: inject the concrete view at the call site,
|
|
16
|
+
* e.g. `new PlaceSyncPlan<$backend.MySiteView>({ onUpdate })`.
|
|
17
|
+
*/
|
|
14
18
|
class PlaceSyncPlan {
|
|
15
19
|
constructor(options = {}) {
|
|
16
20
|
var _a;
|
|
@@ -30,7 +34,6 @@ class PlaceSyncPlan {
|
|
|
30
34
|
return;
|
|
31
35
|
const prev = ctx.readSnapshot(target);
|
|
32
36
|
const input = { id: target.id };
|
|
33
|
-
// place.get 응답은 backend 소유(registry response=unknown) → MySiteView 로 단언
|
|
34
37
|
const view = (yield ctx.client.request('place.get', input));
|
|
35
38
|
const nextUpdatedAt = typeof (view === null || view === void 0 ? void 0 : view.updatedAt) === 'number' ? view.updatedAt : undefined;
|
|
36
39
|
const prevUpdatedAt = typeof (prev === null || prev === void 0 ? void 0 : prev.updatedAt) === 'number' ? prev.updatedAt : undefined;
|
|
@@ -1,27 +1,31 @@
|
|
|
1
|
-
import type { ProfileView } from '@lemoncloud/chatic-socials-api';
|
|
2
1
|
import type { SocketMessage } from '../../lib/types';
|
|
3
|
-
import type { DomainSyncContext, DomainSyncPlan
|
|
2
|
+
import type { DomainSyncContext, DomainSyncPlan } from '../types';
|
|
3
|
+
import type { SyncableView, SyncBackoffOptions, SyncFailureInfo, SyncTargetDescriptor } from '../types';
|
|
4
4
|
export interface ProfileSyncTarget extends SyncTargetDescriptor {
|
|
5
5
|
type: 'profile';
|
|
6
6
|
}
|
|
7
|
-
export interface ProfileSyncSnapshot {
|
|
7
|
+
export interface ProfileSyncSnapshot<TView extends SyncableView = SyncableView> {
|
|
8
8
|
id?: string;
|
|
9
9
|
updatedAt?: number;
|
|
10
|
-
view?:
|
|
10
|
+
view?: TView;
|
|
11
11
|
}
|
|
12
|
-
export interface ProfileSyncPlanOptions {
|
|
12
|
+
export interface ProfileSyncPlanOptions<TView extends SyncableView = SyncableView> {
|
|
13
13
|
intervalMs?: number;
|
|
14
14
|
idleBackoff?: SyncBackoffOptions;
|
|
15
15
|
resetSnapshotOnConnected?: boolean;
|
|
16
|
-
onUpdate?: (target: ProfileSyncTarget, view:
|
|
17
|
-
onRemove?: (target: ProfileSyncTarget, previous?: ProfileSyncSnapshot) => void;
|
|
16
|
+
onUpdate?: (target: ProfileSyncTarget, view: TView, previous?: ProfileSyncSnapshot<TView>) => void;
|
|
17
|
+
onRemove?: (target: ProfileSyncTarget, previous?: ProfileSyncSnapshot<TView>) => void;
|
|
18
18
|
}
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Single-profile sync: polls `profile.get`, applies on `updatedAt` change, re-pulls on a `profile.sync` nudge.
|
|
21
|
+
* - Generic over the view type: inject the concrete view at the call site,
|
|
22
|
+
* e.g. `new ProfileSyncPlan<$socials.ProfileView>({ onUpdate })`.
|
|
23
|
+
*/
|
|
24
|
+
export declare class ProfileSyncPlan<TView extends SyncableView = SyncableView> implements DomainSyncPlan<ProfileSyncTarget> {
|
|
21
25
|
private readonly options;
|
|
22
26
|
readonly domain = "profile";
|
|
23
27
|
readonly idleBackoff: SyncBackoffOptions;
|
|
24
|
-
constructor(options?: ProfileSyncPlanOptions);
|
|
28
|
+
constructor(options?: ProfileSyncPlanOptions<TView>);
|
|
25
29
|
supports: (target: SyncTargetDescriptor) => target is ProfileSyncTarget;
|
|
26
30
|
getKey: (target: ProfileSyncTarget) => string;
|
|
27
31
|
getIntervalMs: (target: ProfileSyncTarget) => number;
|
|
@@ -10,7 +10,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ProfileSyncPlan = void 0;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Single-profile sync: polls `profile.get`, applies on `updatedAt` change, re-pulls on a `profile.sync` nudge.
|
|
15
|
+
* - Generic over the view type: inject the concrete view at the call site,
|
|
16
|
+
* e.g. `new ProfileSyncPlan<$socials.ProfileView>({ onUpdate })`.
|
|
17
|
+
*/
|
|
14
18
|
class ProfileSyncPlan {
|
|
15
19
|
constructor(options = {}) {
|
|
16
20
|
var _a;
|
|
@@ -30,7 +34,6 @@ class ProfileSyncPlan {
|
|
|
30
34
|
return;
|
|
31
35
|
const prev = ctx.readSnapshot(target);
|
|
32
36
|
const input = { id: target.id };
|
|
33
|
-
// profile.get 응답은 socials 소유(registry response=unknown) → ProfileView 로 단언
|
|
34
37
|
const view = (yield ctx.client.request('profile.get', input));
|
|
35
38
|
const nextUpdatedAt = typeof (view === null || view === void 0 ? void 0 : view.updatedAt) === 'number' ? view.updatedAt : undefined;
|
|
36
39
|
const prevUpdatedAt = typeof (prev === null || prev === void 0 ? void 0 : prev.updatedAt) === 'number' ? prev.updatedAt : undefined;
|
|
@@ -117,6 +117,18 @@ export interface ClientSocketV2 {
|
|
|
117
117
|
destroy(): void;
|
|
118
118
|
}
|
|
119
119
|
export declare type SyncTargetType = string;
|
|
120
|
+
/**
|
|
121
|
+
* Minimal view shape a polling sync plan depends on — nothing else is read from the view.
|
|
122
|
+
* Concrete view types are owned by the external API, so plans stay generic over the view
|
|
123
|
+
* and the consumer injects the concrete type at the call site; this keeps the published
|
|
124
|
+
* `.d.ts` free of external-API dependencies.
|
|
125
|
+
*/
|
|
126
|
+
export interface SyncableView {
|
|
127
|
+
/** Stable identifier of the synced entity. */
|
|
128
|
+
id?: string;
|
|
129
|
+
/** Convergence axis — the plan re-applies the view only when this value changes. */
|
|
130
|
+
updatedAt?: number;
|
|
131
|
+
}
|
|
120
132
|
export interface SyncTargetDescriptor {
|
|
121
133
|
type: SyncTargetType;
|
|
122
134
|
id?: string;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { InferSocketRequest, SocketRequestMessage } from '../types';
|
|
2
|
-
import type { ChannelStereo } from '
|
|
3
|
-
import type { ChannelView } from '../../modules/chat/views';
|
|
2
|
+
import type { ChannelStereo, ChannelView } from '../chat/views';
|
|
4
3
|
export interface ChannelCreateRequestData {
|
|
5
4
|
stereo: ChannelStereo;
|
|
6
5
|
name?: string;
|