@pmate/chat-sdk 0.12.0-next.2 → 0.12.0-next.21
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 +62 -3
- package/lib/runtime/batchedReadIndex.d.ts +11 -2
- package/lib/runtime/batchedReadIndex.js +21 -1
- package/lib/runtime/chatClient.d.ts +8 -0
- package/lib/runtime/chatClient.js +8 -2
- package/lib/runtime/chatSession.d.ts +79 -7
- package/lib/runtime/chatSession.js +439 -30
- package/lib/runtime/conversationRead.d.ts +13 -0
- package/lib/runtime/conversationRead.js +34 -0
- package/lib/runtime/createChat.d.ts +26 -0
- package/lib/runtime/createChat.js +42 -0
- package/lib/runtime/durableSender.d.ts +28 -4
- package/lib/runtime/durableSender.js +158 -35
- package/lib/runtime/index.d.ts +2 -0
- package/lib/runtime/index.js +2 -0
- package/lib/runtime/messageClient.d.ts +39 -6
- package/lib/runtime/messageClient.js +448 -69
- package/lib/runtime/messageReaders.d.ts +12 -1
- package/lib/runtime/messageReaders.js +64 -3
- package/lib/runtime/messageStore.d.ts +37 -2
- package/lib/runtime/messageStore.js +150 -10
- package/lib/runtime/readCounts.d.ts +15 -1
- package/lib/runtime/readCounts.js +85 -7
- package/lib/runtime/readIndex.d.ts +26 -3
- package/lib/runtime/readIndex.js +75 -6
- package/lib/runtime/readSync.js +12 -4
- package/lib/runtime/readingClient.d.ts +15 -0
- package/lib/runtime/readingClient.js +83 -12
- package/lib/runtime/recordCache.d.ts +9 -0
- package/lib/runtime/recordCache.js +62 -5
- package/lib/runtime/responseCache.d.ts +44 -0
- package/lib/runtime/responseCache.js +173 -0
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ChatSession } from "./chatSession.js";
|
|
2
|
+
import { SocialReadClient } from "./socialReadClient.js";
|
|
3
|
+
import { chatTeamId } from "./teamScope.js";
|
|
4
|
+
import { dmThreadId, groupThreadId, isValidChatTeamId } from "./thread.js";
|
|
5
|
+
/** Standard browser entry: storage, paging, realtime and reading share one identity. */
|
|
6
|
+
export function createChat(options) {
|
|
7
|
+
const { appId, accountId, profileId, teamId, token, onError } = options;
|
|
8
|
+
if (!isValidChatTeamId(teamId))
|
|
9
|
+
throw new Error("INVALID_CHAT_TEAM");
|
|
10
|
+
const endpoint = (options.endpoint ?? "https://hub.pmate.chat").replace(/\/+$/, "");
|
|
11
|
+
const http = endpoint.replace(/^ws/, "http"), ws = endpoint.replace(/^http/, "ws");
|
|
12
|
+
const reader = new SocialReadClient({ appId, profileId, token, hubHttpUrl: http, fetch: options.fetch });
|
|
13
|
+
const page = async (threadId, signal, cursor = {}) => {
|
|
14
|
+
const result = await reader.messagePage({ threadId, teamId, limit: 100, ...cursor }, signal);
|
|
15
|
+
return { ...result, messages: result.messages.map(message => ({ ...message, id: message.messageId, timestamp: message.serverCreatedAt })) };
|
|
16
|
+
};
|
|
17
|
+
return new ChatSession({
|
|
18
|
+
sharedConnection: options.connection,
|
|
19
|
+
activeThreadId: options.threadId,
|
|
20
|
+
messages: { scope: { appId, accountId, profileId, teamId }, transport: { load: (thread, signal) => page(thread, signal), changes: (thread, after, signal) => page(thread, signal, { after }), older: (thread, before, signal) => page(thread, signal, { before }) }, onError },
|
|
21
|
+
reading: { appId, profileId, sessionToken: token, hubHttpUrl: http, fetch: options.fetch, onError },
|
|
22
|
+
connection: { appId, profileId, teamId, hubUrl: ws, clientInstanceId: options.clientInstanceId, fetchImpl: options.fetch,
|
|
23
|
+
session: { token, identity: { accountId, app: appId, profileId } }, onNotification: options.onNotification, onReach: options.onReach },
|
|
24
|
+
decode: message => decodeIncoming(message, { appId, profileId, teamId }), onError,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function decodeIncoming(message, scope) {
|
|
28
|
+
if (message.opcode !== 1 || !message.body || typeof message.body !== "object")
|
|
29
|
+
return;
|
|
30
|
+
const body = message.body;
|
|
31
|
+
const invitation = body.kind === "contact" && ["friendRequest", "requestFriend"].includes(String(body.action));
|
|
32
|
+
if ((!invitation && body.kind !== "message") || (invitation ? body.payload?.appId : body.content?.appId) !== scope.appId || chatTeamId(body) !== scope.teamId)
|
|
33
|
+
return;
|
|
34
|
+
const threadKind = invitation ? "dm" : body.threadKind;
|
|
35
|
+
if (threadKind !== "dm" && threadKind !== "group")
|
|
36
|
+
return;
|
|
37
|
+
if (threadKind === "dm" && message.from !== scope.profileId && message.to !== scope.profileId)
|
|
38
|
+
return;
|
|
39
|
+
const threadId = threadKind === "dm" ? dmThreadId(scope.appId, message.from, message.to, scope.teamId) : groupThreadId(scope.appId, message.to, scope.teamId);
|
|
40
|
+
return { appId: scope.appId, teamId: scope.teamId, threadId, threadKind, messageId: message.hash, id: message.hash,
|
|
41
|
+
timestamp: message.t, serverCreatedAt: message.t, clientCreatedAt: message.t, from: message.from, to: message.to, body: message.body, recipients: message.recipients };
|
|
42
|
+
}
|
|
@@ -8,13 +8,30 @@ export type OutgoingMessage = {
|
|
|
8
8
|
message: ChatRuntimeMessage;
|
|
9
9
|
status: "sending" | "failed" | "sent";
|
|
10
10
|
confirmed?: boolean;
|
|
11
|
+
accessVersion?: number;
|
|
12
|
+
};
|
|
13
|
+
export type SenderScopeAccess = {
|
|
14
|
+
version: number;
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type DurableSenderOptions = {
|
|
18
|
+
indexedDB?: IDBFactory;
|
|
19
|
+
name?: string;
|
|
20
|
+
access?: () => Promise<SenderScopeAccess>;
|
|
11
21
|
};
|
|
12
22
|
/** Persists the exact wire message before sending; retries preserve its hash. */
|
|
13
23
|
export declare class DurableChatSender {
|
|
14
24
|
private client;
|
|
15
25
|
private onError;
|
|
26
|
+
private options;
|
|
16
27
|
private db;
|
|
17
28
|
private outbox;
|
|
29
|
+
private epochs;
|
|
30
|
+
private version;
|
|
31
|
+
private withdrawn;
|
|
32
|
+
private revoking?;
|
|
33
|
+
private channel?;
|
|
34
|
+
private accepted;
|
|
18
35
|
private scope;
|
|
19
36
|
private identity;
|
|
20
37
|
private values;
|
|
@@ -22,12 +39,19 @@ export declare class DurableChatSender {
|
|
|
22
39
|
private listeners;
|
|
23
40
|
private running;
|
|
24
41
|
private closed;
|
|
42
|
+
private closing;
|
|
43
|
+
private empty;
|
|
25
44
|
private offConnection;
|
|
26
45
|
private ready;
|
|
27
|
-
constructor(client: ChatClient, scope: MessageScope, onError: (error: unknown) => void, options?:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
constructor(client: ChatClient, scope: MessageScope, onError: (error: unknown) => void, options?: DurableSenderOptions);
|
|
47
|
+
private readAccess;
|
|
48
|
+
private restore;
|
|
49
|
+
private checkAccess;
|
|
50
|
+
private withEpoch;
|
|
51
|
+
private deleteRows;
|
|
52
|
+
/** Stop disclosure immediately; durable invalidation is coordinated separately. */
|
|
53
|
+
withdraw(): void;
|
|
54
|
+
revoke(targetVersion?: number): Promise<void>;
|
|
31
55
|
initialize(): Promise<void>;
|
|
32
56
|
subscribe(listener: () => void): () => void;
|
|
33
57
|
snapshot(threadId?: string): readonly OutgoingMessage[];
|
|
@@ -5,8 +5,15 @@ import { messageScopeKey } from "./messageStore.js";
|
|
|
5
5
|
export class DurableChatSender {
|
|
6
6
|
client;
|
|
7
7
|
onError;
|
|
8
|
+
options;
|
|
8
9
|
db;
|
|
9
10
|
outbox;
|
|
11
|
+
epochs;
|
|
12
|
+
version = 0;
|
|
13
|
+
withdrawn = false;
|
|
14
|
+
revoking;
|
|
15
|
+
channel;
|
|
16
|
+
accepted = new Set();
|
|
10
17
|
scope;
|
|
11
18
|
identity;
|
|
12
19
|
values = new Map();
|
|
@@ -14,30 +21,129 @@ export class DurableChatSender {
|
|
|
14
21
|
listeners = new Set();
|
|
15
22
|
running = new Map();
|
|
16
23
|
closed = false;
|
|
24
|
+
closing = false;
|
|
25
|
+
empty = [];
|
|
17
26
|
offConnection;
|
|
18
27
|
ready;
|
|
19
28
|
constructor(client, scope, onError, options = {}) {
|
|
20
29
|
this.client = client;
|
|
21
30
|
this.onError = onError;
|
|
31
|
+
this.options = options;
|
|
22
32
|
this.scope = messageScopeKey(scope);
|
|
23
33
|
this.identity = { ...scope };
|
|
24
34
|
this.db = new Dexie(options.name ?? "pmate-chat-outbox", { indexedDB: options.indexedDB ?? globalThis.indexedDB, IDBKeyRange: globalThis.IDBKeyRange });
|
|
25
35
|
this.db.version(1).stores({ outbox: "[scope+hash],scope,[scope+threadId]" });
|
|
36
|
+
this.db.version(2).stores({ scopeEpochs: "scope" });
|
|
26
37
|
this.outbox = this.db.table("outbox");
|
|
27
|
-
this.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
this.epochs = this.db.table("scopeEpochs");
|
|
39
|
+
this.db.on("versionchange", () => { this.db.close({ disableAutoOpen: false }); return false; });
|
|
40
|
+
this.ready = this.restore();
|
|
41
|
+
void this.ready.catch(() => { });
|
|
42
|
+
if (typeof BroadcastChannel !== "undefined") {
|
|
43
|
+
this.channel = new BroadcastChannel(`pmate-chat-outbox:${this.scope}:${this.db.name}`);
|
|
44
|
+
this.channel.onmessage = event => { if (Number.isSafeInteger(event.data?.version) && event.data.version > this.version)
|
|
45
|
+
this.withdraw(); };
|
|
46
|
+
}
|
|
33
47
|
this.offConnection = client.watchConnection(state => {
|
|
34
48
|
if (state === "connected")
|
|
35
49
|
void this.resume().catch(onError);
|
|
36
50
|
});
|
|
37
51
|
}
|
|
52
|
+
async readAccess() {
|
|
53
|
+
const access = await this.options.access?.();
|
|
54
|
+
if (access && (!Number.isSafeInteger(access.version) || access.version < 0 || typeof access.allowed !== "boolean"))
|
|
55
|
+
throw new Error("INVALID_OUTBOX_ACCESS");
|
|
56
|
+
return access;
|
|
57
|
+
}
|
|
58
|
+
async restore() {
|
|
59
|
+
const access = await this.readAccess();
|
|
60
|
+
const loaded = await this.db.transaction("rw", this.outbox, this.epochs, async () => {
|
|
61
|
+
const current = (await this.epochs.get(this.scope))?.version ?? 0;
|
|
62
|
+
const version = access?.version ?? current;
|
|
63
|
+
if (version < current)
|
|
64
|
+
throw new Error("OUTBOX_SCOPE_CHANGED");
|
|
65
|
+
if (version > current) {
|
|
66
|
+
await this.epochs.put({ scope: this.scope, version });
|
|
67
|
+
await this.deleteRows();
|
|
68
|
+
}
|
|
69
|
+
return { version, advanced: version > current, records: access?.allowed === false ? [] : await this.outbox.where("scope").equals(this.scope).toArray() };
|
|
70
|
+
});
|
|
71
|
+
this.version = loaded.version;
|
|
72
|
+
if (loaded.advanced)
|
|
73
|
+
this.channel?.postMessage({ version: this.version });
|
|
74
|
+
if (this.closed || this.withdrawn)
|
|
75
|
+
return;
|
|
76
|
+
const current = await this.readAccess();
|
|
77
|
+
if (current && (!current.allowed || current.version !== this.version))
|
|
78
|
+
return;
|
|
79
|
+
loaded.records.filter(record => !record.confirmed && (record.accessVersion ?? 0) === this.version).forEach(record => this.values.set(record.hash, record));
|
|
80
|
+
this.emit();
|
|
81
|
+
}
|
|
82
|
+
async checkAccess() {
|
|
83
|
+
if (this.closed)
|
|
84
|
+
throw new Error("CHAT_SENDER_CLOSED");
|
|
85
|
+
if (this.withdrawn)
|
|
86
|
+
throw new Error("OUTBOX_ACCESS_REVOKED");
|
|
87
|
+
const access = await this.readAccess();
|
|
88
|
+
if (access && access.version !== this.version) {
|
|
89
|
+
this.withdraw();
|
|
90
|
+
throw new Error("OUTBOX_SCOPE_CHANGED");
|
|
91
|
+
}
|
|
92
|
+
if (access && !access.allowed)
|
|
93
|
+
throw new Error("OUTBOX_ACCESS_REVOKED");
|
|
94
|
+
}
|
|
95
|
+
async withEpoch(mode, operation) {
|
|
96
|
+
await this.checkAccess();
|
|
97
|
+
try {
|
|
98
|
+
return await this.db.transaction(mode, this.outbox, this.epochs, async () => {
|
|
99
|
+
if (this.closed || this.withdrawn)
|
|
100
|
+
throw new Error("OUTBOX_ACCESS_REVOKED");
|
|
101
|
+
if (((await this.epochs.get(this.scope))?.version ?? 0) !== this.version)
|
|
102
|
+
throw new Error("OUTBOX_SCOPE_CHANGED");
|
|
103
|
+
return operation();
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (String(error).includes("OUTBOX_SCOPE_CHANGED"))
|
|
108
|
+
this.withdraw();
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
deleteRows() { return this.outbox.where("[scope+hash]").between([this.scope, Dexie.minKey], [this.scope, Dexie.maxKey], true, true).delete(); }
|
|
113
|
+
/** Stop disclosure immediately; durable invalidation is coordinated separately. */
|
|
114
|
+
withdraw() {
|
|
115
|
+
this.withdrawn = true;
|
|
116
|
+
this.values.clear();
|
|
117
|
+
this.emit();
|
|
118
|
+
}
|
|
119
|
+
revoke(targetVersion) {
|
|
120
|
+
if (this.closed || this.closing)
|
|
121
|
+
return Promise.reject(new Error("CHAT_SENDER_CLOSED"));
|
|
122
|
+
if (this.revoking)
|
|
123
|
+
return this.revoking;
|
|
124
|
+
this.withdraw();
|
|
125
|
+
return this.revoking = (async () => {
|
|
126
|
+
await this.ready.catch(error => { if (!String(error).includes("OUTBOX_SCOPE_CHANGED"))
|
|
127
|
+
throw error; });
|
|
128
|
+
const version = await this.db.transaction("rw", this.outbox, this.epochs, async () => {
|
|
129
|
+
const current = (await this.epochs.get(this.scope))?.version ?? 0;
|
|
130
|
+
const target = targetVersion ?? this.version + 1;
|
|
131
|
+
if (!Number.isSafeInteger(target) || target < 0)
|
|
132
|
+
throw new Error("INVALID_OUTBOX_EPOCH");
|
|
133
|
+
if (current >= target)
|
|
134
|
+
return current;
|
|
135
|
+
await this.epochs.put({ scope: this.scope, version: target });
|
|
136
|
+
await this.deleteRows();
|
|
137
|
+
return target;
|
|
138
|
+
});
|
|
139
|
+
this.channel?.postMessage({ version });
|
|
140
|
+
})();
|
|
141
|
+
}
|
|
38
142
|
initialize() { return this.ready; }
|
|
39
143
|
subscribe(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; }
|
|
40
144
|
snapshot(threadId) {
|
|
145
|
+
if (this.closed || this.closing || this.withdrawn)
|
|
146
|
+
return this.empty;
|
|
41
147
|
let snapshot = this.snapshots.get(threadId);
|
|
42
148
|
if (!snapshot) {
|
|
43
149
|
snapshot = [...this.values.values()].filter(record => threadId === undefined || record.threadId === threadId);
|
|
@@ -50,7 +156,8 @@ export class DurableChatSender {
|
|
|
50
156
|
/** Resolves after the exact wire message is durable, independently of network delivery. */
|
|
51
157
|
async enqueue(threadId, input) {
|
|
52
158
|
const message = await this.persistOutgoing(threadId, input);
|
|
53
|
-
|
|
159
|
+
if (!this.closed && !this.closing && !this.withdrawn)
|
|
160
|
+
void this.retry(message.hash).catch(error => this.onError(error));
|
|
54
161
|
return message;
|
|
55
162
|
}
|
|
56
163
|
/** Compatibility API: resolves only after transport delivery. */
|
|
@@ -58,23 +165,27 @@ export class DurableChatSender {
|
|
|
58
165
|
const message = await this.persistOutgoing(threadId, input);
|
|
59
166
|
return this.retry(message.hash);
|
|
60
167
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
168
|
+
persistOutgoing(threadId, input) {
|
|
169
|
+
if (this.closed || this.closing || this.withdrawn)
|
|
170
|
+
return Promise.reject(new Error("OUTBOX_ACCESS_REVOKED"));
|
|
171
|
+
const pending = (async () => {
|
|
172
|
+
await this.ready;
|
|
173
|
+
await this.checkAccess();
|
|
174
|
+
const message = this.client.prepareChat(input.to, withChatTeam(input.body, this.identity.teamId), input.recipients);
|
|
175
|
+
if (message.from !== this.identity.profileId)
|
|
176
|
+
throw new Error("OUTGOING_MESSAGE_IDENTITY_MISMATCH");
|
|
177
|
+
const record = { scope: this.scope, threadId, hash: message.hash, message, status: "sending", accessVersion: this.version };
|
|
178
|
+
await this.withEpoch("rw", async () => { await this.outbox.put(record); });
|
|
179
|
+
await this.checkAccess();
|
|
180
|
+
this.values.set(record.hash, record);
|
|
181
|
+
this.emit();
|
|
182
|
+
return message;
|
|
183
|
+
})().finally(() => this.accepted.delete(pending));
|
|
184
|
+
this.accepted.add(pending);
|
|
185
|
+
return pending;
|
|
75
186
|
}
|
|
76
187
|
retry(hash) {
|
|
77
|
-
if (this.closed)
|
|
188
|
+
if (this.closed || this.closing || this.withdrawn)
|
|
78
189
|
return Promise.reject(new Error("CHAT_SENDER_CLOSED"));
|
|
79
190
|
const running = this.running.get(hash);
|
|
80
191
|
if (running)
|
|
@@ -85,8 +196,8 @@ export class DurableChatSender {
|
|
|
85
196
|
}
|
|
86
197
|
async transmit(hash) {
|
|
87
198
|
await this.ready;
|
|
88
|
-
const record = await this.outbox.get([this.scope, hash]);
|
|
89
|
-
if (!record || this.closed)
|
|
199
|
+
const record = await this.withEpoch("r", async () => this.outbox.get([this.scope, hash]));
|
|
200
|
+
if (!record || this.closed || this.withdrawn)
|
|
90
201
|
throw new Error("OUTGOING_MESSAGE_UNAVAILABLE");
|
|
91
202
|
if (record.status === "sent" || record.confirmed)
|
|
92
203
|
return record.message;
|
|
@@ -96,6 +207,7 @@ export class DurableChatSender {
|
|
|
96
207
|
if (!latest || latest.confirmed || latest.status === "sent")
|
|
97
208
|
return latest?.message ?? record.message;
|
|
98
209
|
}
|
|
210
|
+
await this.checkAccess();
|
|
99
211
|
if (this.values.get(hash)?.status !== "sending") {
|
|
100
212
|
this.values.set(hash, sending);
|
|
101
213
|
this.emit();
|
|
@@ -104,7 +216,7 @@ export class DurableChatSender {
|
|
|
104
216
|
const message = await this.client.sendPrepared(record.message);
|
|
105
217
|
const sent = { ...record, message, status: "sent" };
|
|
106
218
|
const committed = await this.save(sent);
|
|
107
|
-
if (!this.closed) {
|
|
219
|
+
if (!this.closed && !this.withdrawn) {
|
|
108
220
|
if (committed?.confirmed || !committed)
|
|
109
221
|
this.values.delete(hash);
|
|
110
222
|
else
|
|
@@ -114,7 +226,7 @@ export class DurableChatSender {
|
|
|
114
226
|
return message;
|
|
115
227
|
}
|
|
116
228
|
catch (error) {
|
|
117
|
-
if (!this.closed) {
|
|
229
|
+
if (!this.closed && !this.withdrawn) {
|
|
118
230
|
const failed = { ...record, status: "failed" };
|
|
119
231
|
const committed = await this.save(failed);
|
|
120
232
|
if (committed?.confirmed || committed?.status === "sent") {
|
|
@@ -132,7 +244,7 @@ export class DurableChatSender {
|
|
|
132
244
|
}
|
|
133
245
|
}
|
|
134
246
|
async save(record) {
|
|
135
|
-
return this.
|
|
247
|
+
return this.withEpoch("rw", async () => {
|
|
136
248
|
const previous = await this.outbox.get([this.scope, record.hash]);
|
|
137
249
|
if (!previous || previous.confirmed || previous.status === "sent")
|
|
138
250
|
return previous;
|
|
@@ -143,10 +255,13 @@ export class DurableChatSender {
|
|
|
143
255
|
/** Confirm only after the authoritative message has been durably merged.
|
|
144
256
|
* A tombstone prevents another tab's late failure from recreating the outbox. */
|
|
145
257
|
async confirm(hashes) {
|
|
258
|
+
hashes = [...new Set(hashes)];
|
|
259
|
+
if (!hashes.length)
|
|
260
|
+
return;
|
|
146
261
|
await this.ready;
|
|
147
|
-
await this.
|
|
262
|
+
await this.withEpoch("rw", async () => {
|
|
148
263
|
const previous = await this.outbox.bulkGet(hashes.map(hash => [this.scope, hash]));
|
|
149
|
-
const confirmed = previous.filter((record) => Boolean(record)).map(record => ({ ...record, status: "sent", confirmed: true }));
|
|
264
|
+
const confirmed = previous.filter((record) => Boolean(record && !record.confirmed)).map(record => ({ ...record, status: "sent", confirmed: true }));
|
|
150
265
|
if (confirmed.length)
|
|
151
266
|
await this.outbox.bulkPut(confirmed);
|
|
152
267
|
});
|
|
@@ -158,12 +273,12 @@ export class DurableChatSender {
|
|
|
158
273
|
}
|
|
159
274
|
async resume() {
|
|
160
275
|
await this.ready;
|
|
161
|
-
if (this.closed)
|
|
276
|
+
if (this.closed || this.closing || this.withdrawn)
|
|
162
277
|
return;
|
|
163
278
|
// Only interrupted, unconfirmed sends resume automatically; failed sends require retry.
|
|
164
279
|
for (const record of this.values.values())
|
|
165
280
|
if (record.status === "sending") {
|
|
166
|
-
if (this.closed)
|
|
281
|
+
if (this.closed || this.withdrawn)
|
|
167
282
|
return;
|
|
168
283
|
try {
|
|
169
284
|
await this.retry(record.hash);
|
|
@@ -174,11 +289,19 @@ export class DurableChatSender {
|
|
|
174
289
|
}
|
|
175
290
|
}
|
|
176
291
|
async close() {
|
|
177
|
-
this.
|
|
292
|
+
this.closing = true;
|
|
178
293
|
this.offConnection();
|
|
179
294
|
this.listeners.clear();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
295
|
+
this.channel?.close();
|
|
296
|
+
try {
|
|
297
|
+
await this.ready;
|
|
298
|
+
await Promise.allSettled([...this.accepted, ...this.running.values(), ...(this.revoking ? [this.revoking] : [])]);
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
this.closed = true;
|
|
302
|
+
this.values.clear();
|
|
303
|
+
this.snapshots.clear();
|
|
304
|
+
this.db.close();
|
|
305
|
+
}
|
|
183
306
|
}
|
|
184
307
|
}
|
package/lib/runtime/index.d.ts
CHANGED
package/lib/runtime/index.js
CHANGED
|
@@ -15,6 +15,8 @@ export type MessageTransport<T> = {
|
|
|
15
15
|
export type MessageSnapshot<T> = {
|
|
16
16
|
messages: readonly T[];
|
|
17
17
|
hasEarlier: boolean;
|
|
18
|
+
resetVersion: number;
|
|
19
|
+
complete: boolean;
|
|
18
20
|
};
|
|
19
21
|
export type MessageClientOptions<T extends StoredMessage> = {
|
|
20
22
|
scope: MessageScope;
|
|
@@ -25,12 +27,17 @@ export type MessageClientOptions<T extends StoredMessage> = {
|
|
|
25
27
|
batchSize?: number;
|
|
26
28
|
writeWindowMs?: number;
|
|
27
29
|
syncWindowMs?: number;
|
|
30
|
+
transactionBytes?: number;
|
|
31
|
+
queueBytes?: number;
|
|
32
|
+
freshnessMs?: number;
|
|
28
33
|
};
|
|
29
34
|
/** Local-first message facade; hosts never select a cache or network source. */
|
|
30
35
|
export declare class MessageClient<T extends StoredMessage> {
|
|
31
36
|
private options;
|
|
32
37
|
private storage;
|
|
33
38
|
private watches;
|
|
39
|
+
private accessListeners;
|
|
40
|
+
private allListeners;
|
|
34
41
|
private reads;
|
|
35
42
|
private syncing;
|
|
36
43
|
private paging;
|
|
@@ -44,27 +51,53 @@ export declare class MessageClient<T extends StoredMessage> {
|
|
|
44
51
|
private closing;
|
|
45
52
|
private revoked;
|
|
46
53
|
private batchSize;
|
|
54
|
+
private transactionBytes;
|
|
55
|
+
private queueBytes;
|
|
56
|
+
private pendingBytes;
|
|
57
|
+
private pendingRecords;
|
|
58
|
+
private legacyImports;
|
|
59
|
+
private revokingScope?;
|
|
60
|
+
private scopeReady?;
|
|
61
|
+
private scopeVersion;
|
|
62
|
+
private scopeAllowed;
|
|
63
|
+
private scopeDenied;
|
|
64
|
+
private enqueuing;
|
|
65
|
+
private ensureScope;
|
|
47
66
|
constructor(options: MessageClientOptions<T>);
|
|
67
|
+
/** Shared guard for sibling caches; it never authorizes a denied old instance. */
|
|
68
|
+
subscribeAccessRevoked(listener: () => void): () => void;
|
|
69
|
+
scopeAccess(): Promise<{
|
|
70
|
+
version: number;
|
|
71
|
+
allowed: boolean;
|
|
72
|
+
}>;
|
|
48
73
|
getSnapshot(thread: string): MessageSnapshot<T> | undefined;
|
|
49
74
|
subscribe(thread: string, listener: () => void): () => void;
|
|
50
|
-
|
|
75
|
+
subscribeAll(listener: (threadId: string) => void): () => void;
|
|
76
|
+
/** Close the connection gap for saved windows with four bounded background lanes. */
|
|
77
|
+
restore(since: number, threadId?: string): Promise<void>;
|
|
78
|
+
read(thread: string, limit?: number, refresh?: boolean): Promise<MessageSnapshot<T>>;
|
|
51
79
|
/** Application connection/wake events invalidate, rather than fetching directly. */
|
|
52
80
|
invalidate(thread: string): void;
|
|
81
|
+
private fallback;
|
|
82
|
+
/** Migration batches remain separate from authoritative message coverage. */
|
|
83
|
+
importLegacy(thread: string, messages: T[]): Promise<void>;
|
|
53
84
|
private reload;
|
|
54
85
|
/** Events are visible only after their atomic batch commits. */
|
|
55
86
|
receive(thread: string, messages: T[]): Promise<void>;
|
|
56
87
|
private persist;
|
|
88
|
+
private messageBytes;
|
|
89
|
+
private release;
|
|
57
90
|
private flushWrites;
|
|
58
91
|
private runWrites;
|
|
59
92
|
synchronize(thread: string): Promise<void>;
|
|
60
93
|
earlier(thread: string, before: {
|
|
61
94
|
timestamp: number;
|
|
62
95
|
id: string;
|
|
63
|
-
}, limit?: number): Promise<
|
|
64
|
-
messages: T[];
|
|
65
|
-
hasEarlier: boolean;
|
|
66
|
-
}>;
|
|
96
|
+
}, limit?: number): Promise<MessageSnapshot<T>>;
|
|
67
97
|
private fillWindow;
|
|
98
|
+
private denyView;
|
|
99
|
+
private denyScope;
|
|
100
|
+
revokeScope(): Promise<number>;
|
|
68
101
|
revoke(thread: string): Promise<void>;
|
|
69
|
-
close(): Promise<void>;
|
|
102
|
+
close(beforeDispose?: Promise<unknown>): Promise<void>;
|
|
70
103
|
}
|