@pmate/chat-sdk 0.12.0-next.57 → 0.12.0-next.59
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 +26 -0
- package/lib/runtime/chatSession.d.ts +3 -1
- package/lib/runtime/chatSession.js +31 -0
- package/lib/runtime/durableSender.d.ts +16 -5
- package/lib/runtime/durableSender.js +161 -68
- package/lib/runtime/outboxSchema.d.ts +5 -0
- package/lib/runtime/outboxSchema.js +58 -0
- package/lib/runtime/readBatchJournal.d.ts +35 -0
- package/lib/runtime/readBatchJournal.js +210 -0
- package/lib/runtime/readIndex.d.ts +13 -4
- package/lib/runtime/readIndex.js +216 -138
- package/lib/runtime/readSchema.d.ts +2 -0
- package/lib/runtime/readSchema.js +58 -0
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Dexie } from "dexie";
|
|
2
|
+
export type ReadMutation = {
|
|
3
|
+
table: "readingThreads" | "readingMessages" | "readingPositions";
|
|
4
|
+
key: string[];
|
|
5
|
+
value?: unknown;
|
|
6
|
+
};
|
|
7
|
+
type Plan = {
|
|
8
|
+
profileKey: string[];
|
|
9
|
+
header: unknown;
|
|
10
|
+
versions?: unknown;
|
|
11
|
+
};
|
|
12
|
+
/** Cross-document serialization; the persisted journal is the crash boundary. */
|
|
13
|
+
export declare function lockReadOwner<T>(factory: IDBFactory, name: string, owner: string, run: () => Promise<T>, fallback?: (run: () => Promise<T>) => Promise<T>): Promise<T>;
|
|
14
|
+
/** Stage the full plan first, roll forward bounded batches, publish cursor last. */
|
|
15
|
+
export declare class ReadBatchJournal {
|
|
16
|
+
private db;
|
|
17
|
+
private closed;
|
|
18
|
+
private rows;
|
|
19
|
+
private knownOwners;
|
|
20
|
+
private leases;
|
|
21
|
+
private acquiring;
|
|
22
|
+
constructor(db: Dexie, closed: () => boolean);
|
|
23
|
+
lease<T>(owner: string, run: () => Promise<T>): Promise<T>;
|
|
24
|
+
releaseLeases(): Promise<void>;
|
|
25
|
+
checkLeases(renew?: boolean): Promise<void>;
|
|
26
|
+
private check;
|
|
27
|
+
private tables;
|
|
28
|
+
recover(owner: string): Promise<void>;
|
|
29
|
+
private cleanup;
|
|
30
|
+
private apply;
|
|
31
|
+
private validatePlan;
|
|
32
|
+
private finish;
|
|
33
|
+
commit(owner: string, operations: Iterable<ReadMutation>, plan: Plan): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Dexie } from "dexie";
|
|
2
|
+
const LIMIT = 124, BYTES = 1024 * 1024, LEASE_MS = 5_000;
|
|
3
|
+
const size = (value) => JSON.stringify(value).length * 3;
|
|
4
|
+
const localLocks = new WeakMap();
|
|
5
|
+
/** Cross-document serialization; the persisted journal is the crash boundary. */
|
|
6
|
+
export async function lockReadOwner(factory, name, owner, run, fallback) {
|
|
7
|
+
const key = JSON.stringify(["pmate-reading", name, owner]);
|
|
8
|
+
if (typeof navigator !== "undefined" && navigator.locks)
|
|
9
|
+
return navigator.locks.request(key, run);
|
|
10
|
+
if (typeof window !== "undefined") {
|
|
11
|
+
if (!fallback)
|
|
12
|
+
throw new Error("READ_STORAGE_LOCK_UNAVAILABLE");
|
|
13
|
+
return fallback(run);
|
|
14
|
+
}
|
|
15
|
+
let locks = localLocks.get(factory);
|
|
16
|
+
if (!locks) {
|
|
17
|
+
locks = new Map();
|
|
18
|
+
localLocks.set(factory, locks);
|
|
19
|
+
}
|
|
20
|
+
const previous = locks.get(key) ?? Promise.resolve();
|
|
21
|
+
let release;
|
|
22
|
+
const current = new Promise(resolve => { release = resolve; });
|
|
23
|
+
locks.set(key, current);
|
|
24
|
+
await previous;
|
|
25
|
+
try {
|
|
26
|
+
return await run();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
release();
|
|
30
|
+
if (locks.get(key) === current)
|
|
31
|
+
locks.delete(key);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Stage the full plan first, roll forward bounded batches, publish cursor last. */
|
|
35
|
+
export class ReadBatchJournal {
|
|
36
|
+
db;
|
|
37
|
+
closed;
|
|
38
|
+
rows;
|
|
39
|
+
knownOwners = new Set();
|
|
40
|
+
leases = new Map();
|
|
41
|
+
acquiring = new Set();
|
|
42
|
+
constructor(db, closed) {
|
|
43
|
+
this.db = db;
|
|
44
|
+
this.closed = closed;
|
|
45
|
+
this.rows = db.table("readBatches");
|
|
46
|
+
}
|
|
47
|
+
async lease(owner, run) {
|
|
48
|
+
const token = crypto.randomUUID();
|
|
49
|
+
for (;;) {
|
|
50
|
+
this.check();
|
|
51
|
+
const attempt = (async () => {
|
|
52
|
+
const acquired = await this.db.transaction("rw", this.rows, async () => {
|
|
53
|
+
const old = (await this.rows.get([owner, "lease"]))?.lease;
|
|
54
|
+
if (old && old.expires > Date.now())
|
|
55
|
+
return false;
|
|
56
|
+
await this.rows.put({ owner, key: "lease", lease: { token, expires: Date.now() + LEASE_MS } });
|
|
57
|
+
return true;
|
|
58
|
+
});
|
|
59
|
+
if (acquired)
|
|
60
|
+
this.leases.set(owner, token);
|
|
61
|
+
return acquired;
|
|
62
|
+
})();
|
|
63
|
+
this.acquiring.add(attempt);
|
|
64
|
+
const acquired = await attempt.finally(() => this.acquiring.delete(attempt));
|
|
65
|
+
if (acquired)
|
|
66
|
+
break;
|
|
67
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const result = await run();
|
|
71
|
+
this.check();
|
|
72
|
+
await this.db.transaction("r", this.rows, () => this.checkLeases());
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
this.leases.delete(owner);
|
|
77
|
+
if (!this.closed())
|
|
78
|
+
await this.db.transaction("rw", this.rows, async () => { if ((await this.rows.get([owner, "lease"]))?.lease?.token === token)
|
|
79
|
+
await this.rows.delete([owner, "lease"]); });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async releaseLeases() {
|
|
83
|
+
await Promise.allSettled([...this.acquiring]);
|
|
84
|
+
for (const [owner, token] of this.leases)
|
|
85
|
+
await this.db.transaction("rw", this.rows, async () => { if ((await this.rows.get([owner, "lease"]))?.lease?.token === token)
|
|
86
|
+
await this.rows.delete([owner, "lease"]); });
|
|
87
|
+
}
|
|
88
|
+
async checkLeases(renew = false) {
|
|
89
|
+
for (const [owner, token] of this.leases) {
|
|
90
|
+
if ((await this.rows.get([owner, "lease"]))?.lease?.token !== token)
|
|
91
|
+
throw new Error("READ_STORAGE_LOCK_LOST");
|
|
92
|
+
if (renew)
|
|
93
|
+
await this.rows.put({ owner, key: "lease", lease: { token, expires: Date.now() + LEASE_MS } });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
check() { if (this.closed())
|
|
97
|
+
throw new Error("READ_STORAGE_CLOSED"); }
|
|
98
|
+
tables() { return [this.rows, ...["readingProfiles", "readingVersions", "readingThreads", "readingMessages", "readingPositions"].map(name => this.db.table(name))]; }
|
|
99
|
+
async recover(owner) {
|
|
100
|
+
this.check();
|
|
101
|
+
const active = (await this.rows.get([owner, "active"]))?.active;
|
|
102
|
+
if (!active) {
|
|
103
|
+
if (!this.knownOwners.has(owner)) {
|
|
104
|
+
await this.cleanup(owner);
|
|
105
|
+
this.knownOwners.add(owner);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const plan = (await this.rows.get([owner, `${active.id}:plan`]))?.plan;
|
|
110
|
+
if (!plan || typeof active.id !== "string" || !Number.isSafeInteger(active.next) || !Number.isSafeInteger(active.total) || active.next < 0 || active.total < 1 || active.next > active.total)
|
|
111
|
+
throw new Error("READ_STORAGE_INCOMPLETE");
|
|
112
|
+
this.validatePlan(owner, plan);
|
|
113
|
+
while (active.next < active.total) {
|
|
114
|
+
this.check();
|
|
115
|
+
await this.db.transaction("rw", this.tables(), async () => {
|
|
116
|
+
await this.checkLeases();
|
|
117
|
+
const chunk = await this.rows.get([owner, `${active.id}:${active.next}`]);
|
|
118
|
+
if (!chunk?.operations || chunk.operations.length > LIMIT || chunk.operations.some(operation => !["readingThreads", "readingMessages", "readingPositions"].includes(operation.table) || operation.key[0] !== owner || operation.value !== undefined && operation.value.owner !== owner))
|
|
119
|
+
throw new Error("READ_STORAGE_INCOMPLETE");
|
|
120
|
+
await this.apply(chunk.operations);
|
|
121
|
+
await this.rows.put({ owner, key: "active", active: { ...active, next: active.next + 1 } });
|
|
122
|
+
await this.checkLeases(true);
|
|
123
|
+
});
|
|
124
|
+
active.next++;
|
|
125
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
126
|
+
}
|
|
127
|
+
this.check();
|
|
128
|
+
await this.db.transaction("rw", this.tables(), async () => { await this.checkLeases(); await this.finish(plan); await this.rows.delete([owner, "active"]); await this.checkLeases(true); });
|
|
129
|
+
await this.cleanup(owner);
|
|
130
|
+
this.knownOwners.add(owner);
|
|
131
|
+
}
|
|
132
|
+
async cleanup(owner) {
|
|
133
|
+
for (;;) {
|
|
134
|
+
this.check();
|
|
135
|
+
const keys = await this.rows.where("owner").equals(owner).limit(LIMIT).primaryKeys();
|
|
136
|
+
const removable = keys.filter(key => key[1] !== "lease");
|
|
137
|
+
if (removable.length)
|
|
138
|
+
await this.db.transaction("rw", this.rows, async () => { await this.checkLeases(); await this.rows.bulkDelete(removable); await this.checkLeases(true); });
|
|
139
|
+
const count = keys.length;
|
|
140
|
+
if (count < LIMIT)
|
|
141
|
+
return;
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async apply(operations) {
|
|
146
|
+
for (const name of ["readingThreads", "readingMessages", "readingPositions"]) {
|
|
147
|
+
const selected = operations.filter(operation => operation.table === name);
|
|
148
|
+
const writes = selected.filter(operation => operation.value !== undefined).map(operation => operation.value);
|
|
149
|
+
const deletes = selected.filter(operation => operation.value === undefined).map(operation => operation.key);
|
|
150
|
+
if (writes.length)
|
|
151
|
+
await this.db.table(name).bulkPut(writes);
|
|
152
|
+
if (deletes.length)
|
|
153
|
+
await this.db.table(name).bulkDelete(deletes);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
validatePlan(owner, plan) {
|
|
157
|
+
const header = plan.header;
|
|
158
|
+
if (!Array.isArray(plan.profileKey) || JSON.stringify(plan.profileKey) !== owner || !header || header.schemaVersion !== 1 || header.appId !== plan.profileKey[0] || header.profileId !== plan.profileKey[1])
|
|
159
|
+
throw new Error("READ_IDENTITY_MISMATCH");
|
|
160
|
+
}
|
|
161
|
+
async finish(plan) {
|
|
162
|
+
if (plan.versions !== undefined)
|
|
163
|
+
await this.db.table("readingVersions").put(plan.versions, plan.profileKey);
|
|
164
|
+
await this.db.table("readingProfiles").put(plan.header, plan.profileKey);
|
|
165
|
+
}
|
|
166
|
+
async commit(owner, operations, plan) {
|
|
167
|
+
this.check();
|
|
168
|
+
this.validatePlan(owner, plan);
|
|
169
|
+
this.knownOwners.delete(owner);
|
|
170
|
+
await this.cleanup(owner);
|
|
171
|
+
if (size(plan) > BYTES)
|
|
172
|
+
throw new Error("READ_STORAGE_RECORD_TOO_LARGE");
|
|
173
|
+
const id = crypto.randomUUID(), iterator = operations[Symbol.iterator]();
|
|
174
|
+
let next = iterator.next(), total = 0;
|
|
175
|
+
while (!next.done) {
|
|
176
|
+
const batch = [];
|
|
177
|
+
let bytes = 512;
|
|
178
|
+
while (!next.done && batch.length < LIMIT) {
|
|
179
|
+
const weight = size(next.value);
|
|
180
|
+
if (weight > BYTES - 512)
|
|
181
|
+
throw new Error("READ_STORAGE_RECORD_TOO_LARGE");
|
|
182
|
+
if (batch.length && bytes + weight > BYTES)
|
|
183
|
+
break;
|
|
184
|
+
bytes += weight;
|
|
185
|
+
batch.push(next.value);
|
|
186
|
+
next = iterator.next();
|
|
187
|
+
}
|
|
188
|
+
if (total === 0 && next.done) {
|
|
189
|
+
await this.db.transaction("rw", this.tables(), async () => { await this.checkLeases(); await this.apply(batch); await this.finish(plan); await this.checkLeases(true); });
|
|
190
|
+
this.knownOwners.add(owner);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.check();
|
|
194
|
+
await this.db.transaction("rw", this.rows, async () => { await this.checkLeases(); await this.rows.put({ owner, key: `${id}:${total++}`, operations: batch }); await this.checkLeases(true); });
|
|
195
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
196
|
+
}
|
|
197
|
+
if (!total) {
|
|
198
|
+
await this.db.transaction("rw", this.tables(), async () => { await this.checkLeases(); await this.finish(plan); await this.checkLeases(true); });
|
|
199
|
+
this.knownOwners.add(owner);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
await this.db.transaction("rw", this.rows, async () => {
|
|
203
|
+
await this.checkLeases();
|
|
204
|
+
await this.rows.put({ owner, key: `${id}:plan`, plan });
|
|
205
|
+
await this.rows.put({ owner, key: "active", active: { id, next: 0, total } });
|
|
206
|
+
await this.checkLeases(true);
|
|
207
|
+
});
|
|
208
|
+
await this.recover(owner);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -110,7 +110,7 @@ export interface ReadIndexPersistence {
|
|
|
110
110
|
/** Transform and persist atomically; UI publishes only the committed result. */
|
|
111
111
|
update(appId: string, profileId: string, transform: ReadIndexTransform, observation?: ReadObservationScope): Promise<ReadIndexState>;
|
|
112
112
|
}
|
|
113
|
-
/**
|
|
113
|
+
/** Preserves the database and logical keys; store renaming fences old format writers. */
|
|
114
114
|
export declare class IndexedDbReadIndex implements ReadIndexPersistence {
|
|
115
115
|
private storageScope?;
|
|
116
116
|
private migrateLegacy;
|
|
@@ -121,10 +121,17 @@ export declare class IndexedDbReadIndex implements ReadIndexPersistence {
|
|
|
121
121
|
private messageRows;
|
|
122
122
|
private positionRows;
|
|
123
123
|
private normalizedStates;
|
|
124
|
+
private orderedStates;
|
|
125
|
+
private legacyClean;
|
|
124
126
|
private observationMessages;
|
|
125
127
|
private versions;
|
|
126
128
|
private closed;
|
|
129
|
+
private journal;
|
|
130
|
+
private factory;
|
|
131
|
+
private schemaReady;
|
|
132
|
+
private ownerTail;
|
|
127
133
|
constructor(factory?: IDBFactory, name?: string, storageScope?: string | undefined, migrateLegacy?: boolean, metadataOnly?: boolean);
|
|
134
|
+
private owned;
|
|
128
135
|
private transaction;
|
|
129
136
|
private key;
|
|
130
137
|
/** Keyset pages avoid materializing a second, unbounded array of durable rows.
|
|
@@ -133,9 +140,7 @@ export declare class IndexedDbReadIndex implements ReadIndexPersistence {
|
|
|
133
140
|
private hydrate;
|
|
134
141
|
private loadCurrent;
|
|
135
142
|
private loadStored;
|
|
136
|
-
|
|
137
|
-
* profile/cursor writes, and update only changed normalized rows on sync. */
|
|
138
|
-
private persist;
|
|
143
|
+
private cleanupClaimedLegacy;
|
|
139
144
|
load(appId: string, profileId: string): Promise<ReadIndexState | undefined>;
|
|
140
145
|
loadVersioned(appId: string, profileId: string): Promise<{
|
|
141
146
|
state: ReadIndexState | undefined;
|
|
@@ -145,6 +150,10 @@ export declare class IndexedDbReadIndex implements ReadIndexPersistence {
|
|
|
145
150
|
invalidateTeam(appId: string, profileId: string, teamId: string, version: number, knownThreadIds?: readonly string[]): Promise<ReadIndexState>;
|
|
146
151
|
private observe;
|
|
147
152
|
update(appId: string, profileId: string, transform: ReadIndexTransform, observation?: ReadObservationScope): Promise<ReadIndexState>;
|
|
153
|
+
private updateOwned;
|
|
154
|
+
private writeState;
|
|
155
|
+
private cleanUnreachable;
|
|
156
|
+
private changes;
|
|
148
157
|
close(): Promise<void>;
|
|
149
158
|
}
|
|
150
159
|
export declare function sourcesInConversation(state: ReadIndexState, conversationId: string): IndexedReadThread[];
|