@proappstore/sdk 1.16.94 → 1.16.96
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 +6 -0
- package/dist/auth.js +2 -0
- package/dist/auth.js.map +1 -1
- package/dist/base-types.d.ts +5 -0
- package/dist/base-types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/offline/downloads.d.ts +44 -0
- package/dist/offline/downloads.d.ts.map +1 -0
- package/dist/offline/downloads.js +221 -0
- package/dist/offline/downloads.js.map +1 -0
- package/dist/offline/jobs.d.ts +78 -0
- package/dist/offline/jobs.d.ts.map +1 -0
- package/dist/offline/jobs.js +204 -0
- package/dist/offline/jobs.js.map +1 -0
- package/dist/offline/locks.d.ts +6 -0
- package/dist/offline/locks.d.ts.map +1 -0
- package/dist/offline/locks.js +47 -0
- package/dist/offline/locks.js.map +1 -0
- package/dist/offline/store.d.ts +94 -0
- package/dist/offline/store.d.ts.map +1 -0
- package/dist/offline/store.js +260 -0
- package/dist/offline/store.js.map +1 -0
- package/dist/offline.d.ts +8 -0
- package/dist/offline.d.ts.map +1 -0
- package/dist/offline.js +5 -0
- package/dist/offline.js.map +1 -0
- package/dist/sdk-version.js +1 -1
- package/dist/shell-resilience.d.ts +3 -1
- package/dist/shell-resilience.d.ts.map +1 -1
- package/dist/shell-resilience.js +2 -2
- package/dist/shell-resilience.js.map +1 -1
- package/dist/shell.d.ts +11 -2
- package/dist/shell.d.ts.map +1 -1
- package/dist/shell.js +30 -11
- package/dist/shell.js.map +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { OfflineError } from './store.js';
|
|
2
|
+
import { offlineLock } from './locks.js';
|
|
3
|
+
const validId = (id) => {
|
|
4
|
+
if (!id || id.length > 256)
|
|
5
|
+
throw new OfflineError('invalid', 'Work ids must contain 1–256 characters.');
|
|
6
|
+
};
|
|
7
|
+
const errorText = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 1000);
|
|
8
|
+
const checkAbort = (signal) => { if (signal?.aborted)
|
|
9
|
+
throw new DOMException('Work cancelled.', 'AbortError'); };
|
|
10
|
+
export class LocalJobs {
|
|
11
|
+
store;
|
|
12
|
+
constructor(store) {
|
|
13
|
+
this.store = store;
|
|
14
|
+
}
|
|
15
|
+
create(input) {
|
|
16
|
+
validId(input.id);
|
|
17
|
+
validId(input.kind);
|
|
18
|
+
return this.store.transaction(['jobs'], 'readwrite', (tx, done) => {
|
|
19
|
+
const request = tx.objectStore('jobs').get(input.id);
|
|
20
|
+
request.onsuccess = () => {
|
|
21
|
+
if (request.result) {
|
|
22
|
+
tx.abort();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const row = { ...input, checkpoint: null, result: null, status: 'pending', attempts: 0, updatedAt: Date.now(), error: null };
|
|
26
|
+
tx.objectStore('jobs').put(row, row.id);
|
|
27
|
+
done(row);
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
get(id) { return this.store.read('jobs', id); }
|
|
32
|
+
list(options = {}) { return this.store.page('jobs', options); }
|
|
33
|
+
update(id, change) {
|
|
34
|
+
return this.store.transaction(['jobs'], 'readwrite', (tx, done) => {
|
|
35
|
+
const request = tx.objectStore('jobs').get(id);
|
|
36
|
+
request.onsuccess = () => {
|
|
37
|
+
if (!request.result) {
|
|
38
|
+
tx.abort();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const next = { ...change(request.result), updatedAt: Date.now() };
|
|
42
|
+
tx.objectStore('jobs').put(next, id);
|
|
43
|
+
done(next);
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async checkpoint(id, value) { await this.update(id, (row) => ({ ...row, checkpoint: value })); }
|
|
48
|
+
async setStatus(id, status) {
|
|
49
|
+
if (!['pending', 'running', 'paused', 'interrupted', 'completed', 'failed'].includes(status))
|
|
50
|
+
throw new OfflineError('invalid', 'Unknown local job status.');
|
|
51
|
+
await this.update(id, (row) => ({ ...row, status }));
|
|
52
|
+
}
|
|
53
|
+
/** No automatic execution. Re-entering run() passes the last committed checkpoint. */
|
|
54
|
+
async run(id, handler, options = {}) {
|
|
55
|
+
return this.store.runWork(() => offlineLock(`${this.store.databaseName}:jobs`, async () => {
|
|
56
|
+
checkAbort(options.signal);
|
|
57
|
+
const found = await this.get(id);
|
|
58
|
+
if (!found)
|
|
59
|
+
throw new OfflineError('invalid', 'Local job does not exist.');
|
|
60
|
+
if (found.status === 'completed')
|
|
61
|
+
return found;
|
|
62
|
+
await this.update(id, (row) => ({ ...row, status: 'running', attempts: row.attempts + 1, error: null }));
|
|
63
|
+
try {
|
|
64
|
+
const result = await handler(found.input, {
|
|
65
|
+
savedCheckpoint: found.checkpoint, signal: options.signal,
|
|
66
|
+
checkpoint: async (value) => { checkAbort(options.signal); await this.checkpoint(id, value); },
|
|
67
|
+
});
|
|
68
|
+
checkAbort(options.signal);
|
|
69
|
+
return await this.update(id, (row) => ({ ...row, result, status: 'completed' }));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// A locked account deliberately prevents further writes; its running job
|
|
73
|
+
// can be recovered after the owner explicitly opens it again.
|
|
74
|
+
try {
|
|
75
|
+
await this.update(id, (row) => ({ ...row, status: options.signal?.aborted ? 'paused' : 'failed', error: errorText(error) }));
|
|
76
|
+
}
|
|
77
|
+
catch { /* Preserve the original failure; no unlocked-account fallback. */ }
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}, true, options.signal ? { signal: options.signal } : {}), options.signal);
|
|
81
|
+
}
|
|
82
|
+
/** The same Web Lock excludes active SDK runners before recovery. */
|
|
83
|
+
async recover() {
|
|
84
|
+
return this.store.runWork(() => offlineLock(`${this.store.databaseName}:jobs`, () => this.store.transaction(['jobs'], 'readwrite', (tx, done) => {
|
|
85
|
+
let count = 0;
|
|
86
|
+
const request = tx.objectStore('jobs').openCursor();
|
|
87
|
+
request.onsuccess = () => {
|
|
88
|
+
const cursor = request.result;
|
|
89
|
+
if (!cursor) {
|
|
90
|
+
done(count);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const row = cursor.value;
|
|
94
|
+
if (row.status === 'running') {
|
|
95
|
+
cursor.update({ ...row, status: 'interrupted', updatedAt: Date.now() });
|
|
96
|
+
count++;
|
|
97
|
+
}
|
|
98
|
+
cursor.continue();
|
|
99
|
+
};
|
|
100
|
+
}), true));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** At-least-once delivery. Handlers MUST enforce server idempotency using operationId. */
|
|
104
|
+
export class SyncQueue {
|
|
105
|
+
store;
|
|
106
|
+
constructor(store) {
|
|
107
|
+
this.store = store;
|
|
108
|
+
}
|
|
109
|
+
async enqueue(input) {
|
|
110
|
+
validId(input.id);
|
|
111
|
+
validId(input.kind);
|
|
112
|
+
if (input.idempotent !== true)
|
|
113
|
+
throw new OfflineError('invalid', 'Only explicitly idempotent operations may be queued.');
|
|
114
|
+
return this.store.transaction(['outbox'], 'readwrite', (tx, done) => {
|
|
115
|
+
const request = tx.objectStore('outbox').get(input.id);
|
|
116
|
+
request.onsuccess = () => {
|
|
117
|
+
// A duplicate id cannot replace another payload (including after delivery).
|
|
118
|
+
if (request.result) {
|
|
119
|
+
tx.abort();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const row = { id: input.id, kind: input.kind, payload: input.payload, status: 'pending', attempts: 0, updatedAt: Date.now(), error: null };
|
|
123
|
+
tx.objectStore('outbox').put(row, row.id);
|
|
124
|
+
done(row);
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
get(id) { return this.store.read('outbox', id); }
|
|
129
|
+
list(options = {}) { return this.store.page('outbox', options); }
|
|
130
|
+
async change(id, status, error, increment = false) {
|
|
131
|
+
return this.store.transaction(['outbox'], 'readwrite', (tx, done) => {
|
|
132
|
+
const request = tx.objectStore('outbox').get(id);
|
|
133
|
+
request.onsuccess = () => {
|
|
134
|
+
if (!request.result) {
|
|
135
|
+
tx.abort();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const row = request.result;
|
|
139
|
+
tx.objectStore('outbox').put({ ...row, status, error, attempts: row.attempts + Number(increment), updatedAt: Date.now() }, id);
|
|
140
|
+
done(undefined);
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
retry(id) {
|
|
145
|
+
return this.store.runWork(() => offlineLock(`${this.store.databaseName}:sync`, async () => {
|
|
146
|
+
const row = await this.get(id);
|
|
147
|
+
if (!row || row.status !== 'failed')
|
|
148
|
+
throw new OfflineError('invalid', 'Only a failed operation can be retried.');
|
|
149
|
+
await this.change(id, 'pending', null);
|
|
150
|
+
}, true));
|
|
151
|
+
}
|
|
152
|
+
/** Explicit caller action; never triggered automatically by an online event. */
|
|
153
|
+
flush(handlers, options = {}) {
|
|
154
|
+
const limit = options.limit ?? 50;
|
|
155
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
156
|
+
throw new OfflineError('invalid', 'Flush limit must be between 1 and 200.');
|
|
157
|
+
return this.store.runWork(() => offlineLock(`${this.store.databaseName}:sync`, async () => {
|
|
158
|
+
checkAbort(options.signal);
|
|
159
|
+
const pending = await this.store.transaction(['outbox'], 'readonly', (tx, done) => {
|
|
160
|
+
const rows = [];
|
|
161
|
+
const request = tx.objectStore('outbox').openCursor();
|
|
162
|
+
request.onsuccess = () => {
|
|
163
|
+
const cursor = request.result;
|
|
164
|
+
if (!cursor || rows.length >= limit) {
|
|
165
|
+
done(rows);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const row = cursor.value;
|
|
169
|
+
// Running after acquiring the lock means a previous tab terminated.
|
|
170
|
+
if (row.status === 'pending' || row.status === 'running')
|
|
171
|
+
rows.push(row);
|
|
172
|
+
cursor.continue();
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
let completed = 0, failed = 0;
|
|
176
|
+
for (const row of pending) {
|
|
177
|
+
checkAbort(options.signal);
|
|
178
|
+
this.store.assertAccess();
|
|
179
|
+
const handler = Object.hasOwn(handlers, row.kind) ? handlers[row.kind] : undefined;
|
|
180
|
+
if (!handler) {
|
|
181
|
+
await this.change(row.id, 'failed', 'No handler registered for this operation.');
|
|
182
|
+
failed++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
await this.change(row.id, 'running', null, true);
|
|
186
|
+
try {
|
|
187
|
+
await handler(row.payload, { operationId: row.id, signal: options.signal });
|
|
188
|
+
await this.change(row.id, 'completed', null);
|
|
189
|
+
completed++;
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
await this.change(row.id, options.signal?.aborted ? 'pending' : 'failed', errorText(error));
|
|
193
|
+
if (options.signal?.aborted)
|
|
194
|
+
throw error;
|
|
195
|
+
failed++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { completed, failed };
|
|
199
|
+
}, true, options.signal ? { signal: options.signal } : {}), options.signal);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export function createLocalJobs(store) { return new LocalJobs(store); }
|
|
203
|
+
export function createSyncQueue(store) { return new SyncQueue(store); }
|
|
204
|
+
//# sourceMappingURL=jobs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.js","sourceRoot":"","sources":["../../src/offline/jobs.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../src/offline/locks.ts"],"names":[],"mappings":"AAKA,wBAAsB,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,kBAAkB,UAAQ,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAuB/M"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { OfflineError } from './store.js';
|
|
2
|
+
// Web Locks coordinate tabs/workers. Non-browser tests use realm-local queues.
|
|
3
|
+
const queues = new Map();
|
|
4
|
+
const readers = new Map();
|
|
5
|
+
export async function offlineLock(name, work, requireBrowserLock = false, options = {}) {
|
|
6
|
+
if (typeof navigator !== 'undefined' && navigator.locks) {
|
|
7
|
+
return navigator.locks.request(`pas:${name}`, options, (lock) => {
|
|
8
|
+
if (!lock)
|
|
9
|
+
throw new OfflineError('busy', 'Local work is active. Cancel or finish it before clearing storage.');
|
|
10
|
+
return work();
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
if (requireBrowserLock && (typeof window !== 'undefined' || typeof self !== 'undefined'))
|
|
14
|
+
throw new OfflineError('unavailable', 'This browser cannot coordinate offline work between tabs.');
|
|
15
|
+
const run = () => { if (options.signal?.aborted)
|
|
16
|
+
throw new DOMException('Work cancelled.', 'AbortError'); return work(); };
|
|
17
|
+
if (options.ifAvailable && ((readers.get(name) ?? 0) > 0 || queues.has(name)))
|
|
18
|
+
throw new OfflineError('busy', 'Local work is active. Cancel or finish it before clearing storage.');
|
|
19
|
+
if (options.mode === 'shared') {
|
|
20
|
+
// Purge uses ifAvailable and never queues a writer ahead of nested readers.
|
|
21
|
+
const exclusive = queues.get(name);
|
|
22
|
+
if (exclusive)
|
|
23
|
+
await exclusive.catch(() => { });
|
|
24
|
+
readers.set(name, (readers.get(name) ?? 0) + 1);
|
|
25
|
+
try {
|
|
26
|
+
return await run();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
const count = (readers.get(name) ?? 1) - 1;
|
|
30
|
+
if (count)
|
|
31
|
+
readers.set(name, count);
|
|
32
|
+
else
|
|
33
|
+
readers.delete(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const prior = queues.get(name) ?? Promise.resolve();
|
|
37
|
+
const next = prior.catch(() => { }).then(run);
|
|
38
|
+
queues.set(name, next);
|
|
39
|
+
try {
|
|
40
|
+
return await next;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
if (queues.get(name) === next)
|
|
44
|
+
queues.delete(name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=locks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../src/offline/locks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C,+EAA+E;AAC/E,MAAM,MAAM,GAAG,IAAI,GAAG,EAA4B,CAAC;AACnD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;AAC1C,MAAM,CAAC,KAAK,UAAU,WAAW,CAAI,IAAY,EAAE,IAAsB,EAAE,kBAAkB,GAAG,KAAK,EAAE,UAA0F,EAAE;IACjM,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,SAAS,CAAC,KAAK,EAAE,CAAC;QACxD,OAAO,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YAC9D,IAAI,CAAC,IAAI;gBAAE,MAAM,IAAI,YAAY,CAAC,MAAM,EAAE,oEAAoE,CAAC,CAAC;YAChH,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC,CAAC,CAAC;IACL,CAAC;IACD,IAAI,kBAAkB,IAAI,CAAC,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,IAAI,KAAK,WAAW,CAAC;QAAE,MAAM,IAAI,YAAY,CAAC,aAAa,EAAE,2DAA2D,CAAC,CAAC;IAC7L,MAAM,GAAG,GAAG,GAAG,EAAE,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO;QAAE,MAAM,IAAI,YAAY,CAAC,iBAAiB,EAAE,YAAY,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3H,IAAI,OAAO,CAAC,WAAW,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,YAAY,CAAC,MAAM,EAAE,oEAAoE,CAAC,CAAC;IACpL,IAAI,OAAO,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9B,4EAA4E;QAC5E,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,SAAS;YAAE,MAAM,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC/C,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAChD,IAAI,CAAC;YAAC,OAAO,MAAM,GAAG,EAAE,CAAC;QAAC,CAAC;gBACnB,CAAC;YAAC,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YAAC,IAAI,KAAK;gBAAE,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;;gBAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC;IACzH,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IACpD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC7C,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACvB,IAAI,CAAC;QAAC,OAAO,MAAM,IAAI,CAAC;IAAC,CAAC;YAClB,CAAC;QAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI;YAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;AACjE,CAAC"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Intentional local content, not a cache of authenticated API responses. */
|
|
2
|
+
export type OfflineScope = Readonly<{
|
|
3
|
+
kind: 'device';
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'account';
|
|
6
|
+
ownerId: string;
|
|
7
|
+
}>;
|
|
8
|
+
export type OfflineErrorCode = 'unavailable' | 'closed' | 'access-denied' | 'quota' | 'blocked' | 'invalid' | 'storage' | 'integrity' | 'network' | 'busy';
|
|
9
|
+
export declare class OfflineError extends Error {
|
|
10
|
+
readonly code: OfflineErrorCode;
|
|
11
|
+
constructor(code: OfflineErrorCode, message: string, options?: ErrorOptions);
|
|
12
|
+
}
|
|
13
|
+
export type OfflineStoreOptions = {
|
|
14
|
+
appId: string;
|
|
15
|
+
indexedDB?: IDBFactory;
|
|
16
|
+
} & ({
|
|
17
|
+
scope: {
|
|
18
|
+
kind: 'device';
|
|
19
|
+
};
|
|
20
|
+
canAccess?: () => boolean;
|
|
21
|
+
} | {
|
|
22
|
+
scope: {
|
|
23
|
+
kind: 'account';
|
|
24
|
+
ownerId: string;
|
|
25
|
+
};
|
|
26
|
+
canAccess: () => boolean;
|
|
27
|
+
});
|
|
28
|
+
export interface LocalRecord<T = unknown> {
|
|
29
|
+
id: string;
|
|
30
|
+
value: T;
|
|
31
|
+
updatedAt: number;
|
|
32
|
+
blobBytes: number;
|
|
33
|
+
blobType: string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface LocalWrite<T = unknown> {
|
|
36
|
+
id: string;
|
|
37
|
+
value: T;
|
|
38
|
+
blob?: Blob;
|
|
39
|
+
}
|
|
40
|
+
export interface LocalPage {
|
|
41
|
+
after?: string;
|
|
42
|
+
limit?: number;
|
|
43
|
+
}
|
|
44
|
+
export type OfflineTable = 'records' | 'blobs' | 'downloads' | 'jobs' | 'outbox';
|
|
45
|
+
export declare const OFFLINE_SCHEMA_VERSION = 2;
|
|
46
|
+
export declare function offlineFailure(error: unknown): OfflineError;
|
|
47
|
+
/** Every handle stays bound to one app/owner. close() locks it; clear() deletes it. */
|
|
48
|
+
export declare class OfflineStore {
|
|
49
|
+
readonly scope: OfflineScope;
|
|
50
|
+
readonly appId: string;
|
|
51
|
+
readonly databaseName: string;
|
|
52
|
+
private readonly factory;
|
|
53
|
+
private readonly canAccess;
|
|
54
|
+
private closed;
|
|
55
|
+
private readonly active;
|
|
56
|
+
constructor(options: OfflineStoreOptions);
|
|
57
|
+
assertAccess(): void;
|
|
58
|
+
close(): void;
|
|
59
|
+
private open;
|
|
60
|
+
/** @internal Shared transaction primitive for the opt-in offline modules. */
|
|
61
|
+
transaction<T>(tables: OfflineTable[], mode: IDBTransactionMode, work: (tx: IDBTransaction, done: (value: T) => void) => void): Promise<T>;
|
|
62
|
+
/** @internal */
|
|
63
|
+
read<T>(table: OfflineTable, id: string): Promise<T | null>;
|
|
64
|
+
/** @internal */
|
|
65
|
+
write<T>(table: OfflineTable, id: string, value: T): Promise<void>;
|
|
66
|
+
/** @internal Bounded cursor listing; metadata stores contain no audio bytes. */
|
|
67
|
+
page<T>(table: OfflineTable, options?: LocalPage): Promise<T[]>;
|
|
68
|
+
put<T>(entry: LocalWrite<T>): Promise<LocalRecord<T>>;
|
|
69
|
+
get<T = unknown>(id: string): Promise<LocalRecord<T> | null>;
|
|
70
|
+
getBlob(id: string): Promise<Blob | null>;
|
|
71
|
+
list<T = unknown>(options?: LocalPage): Promise<LocalRecord<T>[]>;
|
|
72
|
+
delete(id: string): Promise<void>;
|
|
73
|
+
/** @internal Shared lease excludes purge until durable work finishes. */
|
|
74
|
+
runWork<T>(work: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
75
|
+
/** Rejects busy while SDK jobs/downloads/sync are active. Cancel and await runners before retrying. */
|
|
76
|
+
clear(): Promise<void>;
|
|
77
|
+
/** Portable JSON export of one user-selected record, including an optional base64 blob. */
|
|
78
|
+
exportRecord(id: string): Promise<Blob>;
|
|
79
|
+
}
|
|
80
|
+
export declare function createOfflineStore(options: OfflineStoreOptions): OfflineStore;
|
|
81
|
+
/** Call on explicit logout/account changes; closing never deletes unsynced work. */
|
|
82
|
+
export declare function bindOfflineAccount(store: OfflineStore, auth: {
|
|
83
|
+
onChange: (listener: (user: {
|
|
84
|
+
id: string;
|
|
85
|
+
} | null) => void) => () => void;
|
|
86
|
+
}): () => void;
|
|
87
|
+
/** Persistence is a request, never a promise that the browser cannot remove data. */
|
|
88
|
+
export declare function localStorageStatus(requestPersistence?: boolean): Promise<{
|
|
89
|
+
supported: boolean;
|
|
90
|
+
persisted: boolean;
|
|
91
|
+
usage: number | null;
|
|
92
|
+
quota: number | null;
|
|
93
|
+
}>;
|
|
94
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/offline/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { offlineLock } from './locks.js';
|
|
2
|
+
export class OfflineError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message, options) {
|
|
5
|
+
super(message, options);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'OfflineError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const TABLES = ['records', 'blobs', 'downloads', 'jobs', 'outbox'];
|
|
11
|
+
export const OFFLINE_SCHEMA_VERSION = 2;
|
|
12
|
+
export function offlineFailure(error) {
|
|
13
|
+
if (error instanceof OfflineError)
|
|
14
|
+
return error;
|
|
15
|
+
return new OfflineError(error instanceof DOMException && error.name === 'QuotaExceededError' ? 'quota' : 'storage', 'Local storage operation failed.', { cause: error });
|
|
16
|
+
}
|
|
17
|
+
function key(value) {
|
|
18
|
+
if (!value || value.length > 512)
|
|
19
|
+
throw new OfflineError('invalid', 'A non-empty key of at most 512 characters is required.');
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
/** Every handle stays bound to one app/owner. close() locks it; clear() deletes it. */
|
|
23
|
+
export class OfflineStore {
|
|
24
|
+
scope;
|
|
25
|
+
appId;
|
|
26
|
+
databaseName;
|
|
27
|
+
factory;
|
|
28
|
+
canAccess;
|
|
29
|
+
closed = false;
|
|
30
|
+
active = new Set();
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.appId = key(options.appId);
|
|
33
|
+
this.scope = Object.freeze({ ...options.scope });
|
|
34
|
+
if (this.scope.kind !== 'device' && this.scope.kind !== 'account')
|
|
35
|
+
throw new OfflineError('invalid', 'Unknown local-content scope.');
|
|
36
|
+
if (this.scope.kind === 'account') {
|
|
37
|
+
key(this.scope.ownerId);
|
|
38
|
+
if (typeof options.canAccess !== 'function')
|
|
39
|
+
throw new OfflineError('invalid', 'Account-local content requires an explicit canAccess decision.');
|
|
40
|
+
}
|
|
41
|
+
this.canAccess = options.canAccess;
|
|
42
|
+
this.databaseName = `pas-offline:${JSON.stringify([this.appId, this.scope.kind, this.scope.kind === 'account' ? this.scope.ownerId : null])}`;
|
|
43
|
+
const factory = options.indexedDB ?? globalThis.indexedDB;
|
|
44
|
+
if (!factory)
|
|
45
|
+
throw new OfflineError('unavailable', 'IndexedDB is unavailable on this device.');
|
|
46
|
+
this.factory = factory;
|
|
47
|
+
this.assertAccess();
|
|
48
|
+
}
|
|
49
|
+
assertAccess() {
|
|
50
|
+
if (this.closed)
|
|
51
|
+
throw new OfflineError('closed', 'This local-content handle has been closed.');
|
|
52
|
+
if (this.canAccess && !this.canAccess()) {
|
|
53
|
+
this.close();
|
|
54
|
+
throw new OfflineError('access-denied', 'Local content is locked for this account.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
close() {
|
|
58
|
+
this.closed = true;
|
|
59
|
+
for (const tx of this.active) {
|
|
60
|
+
try {
|
|
61
|
+
tx.abort();
|
|
62
|
+
}
|
|
63
|
+
catch { /* Already committed. */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
open() {
|
|
67
|
+
this.assertAccess();
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let refused = false;
|
|
70
|
+
let request;
|
|
71
|
+
try {
|
|
72
|
+
request = this.factory.open(this.databaseName, OFFLINE_SCHEMA_VERSION);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
reject(offlineFailure(error));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
request.onupgradeneeded = () => {
|
|
79
|
+
// Version 1 held records/blobs; version 2 adds resumable work. Existing data survives.
|
|
80
|
+
for (const table of TABLES)
|
|
81
|
+
if (!request.result.objectStoreNames.contains(table))
|
|
82
|
+
request.result.createObjectStore(table);
|
|
83
|
+
};
|
|
84
|
+
request.onblocked = () => {
|
|
85
|
+
refused = true;
|
|
86
|
+
reject(new OfflineError('blocked', 'Another tab is blocking the local database upgrade. Close it and retry.'));
|
|
87
|
+
};
|
|
88
|
+
request.onerror = () => reject(offlineFailure(request.error));
|
|
89
|
+
request.onsuccess = () => {
|
|
90
|
+
const db = request.result;
|
|
91
|
+
db.onversionchange = () => db.close();
|
|
92
|
+
if (refused) {
|
|
93
|
+
db.close();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
this.assertAccess();
|
|
98
|
+
resolve(db);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
db.close();
|
|
102
|
+
reject(error);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** @internal Shared transaction primitive for the opt-in offline modules. */
|
|
108
|
+
async transaction(tables, mode, work) {
|
|
109
|
+
const db = await this.open();
|
|
110
|
+
try {
|
|
111
|
+
this.assertAccess();
|
|
112
|
+
return await new Promise((resolve, reject) => {
|
|
113
|
+
const tx = db.transaction(tables, mode);
|
|
114
|
+
this.active.add(tx);
|
|
115
|
+
let value;
|
|
116
|
+
let thrown;
|
|
117
|
+
tx.oncomplete = () => {
|
|
118
|
+
this.active.delete(tx);
|
|
119
|
+
try {
|
|
120
|
+
this.assertAccess();
|
|
121
|
+
resolve(value);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
reject(error);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
tx.onabort = () => { this.active.delete(tx); reject(offlineFailure(thrown ?? tx.error)); };
|
|
128
|
+
tx.onerror = () => { };
|
|
129
|
+
try {
|
|
130
|
+
work(tx, (next) => { value = next; });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
thrown = error;
|
|
134
|
+
tx.abort();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
throw offlineFailure(error);
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
db.close();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** @internal */
|
|
146
|
+
read(table, id) {
|
|
147
|
+
return this.transaction([table], 'readonly', (tx, done) => {
|
|
148
|
+
const request = tx.objectStore(table).get(id);
|
|
149
|
+
request.onsuccess = () => done(request.result ?? null);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** @internal */
|
|
153
|
+
write(table, id, value) {
|
|
154
|
+
return this.transaction([table], 'readwrite', (tx, done) => { tx.objectStore(table).put(value, id); done(undefined); });
|
|
155
|
+
}
|
|
156
|
+
/** @internal Bounded cursor listing; metadata stores contain no audio bytes. */
|
|
157
|
+
page(table, options = {}) {
|
|
158
|
+
const limit = options.limit ?? 50;
|
|
159
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
160
|
+
throw new OfflineError('invalid', 'Page limit must be between 1 and 200.');
|
|
161
|
+
return this.transaction([table], 'readonly', (tx, done) => {
|
|
162
|
+
const rows = [];
|
|
163
|
+
const request = tx.objectStore(table).openCursor();
|
|
164
|
+
request.onsuccess = () => {
|
|
165
|
+
const cursor = request.result;
|
|
166
|
+
if (!cursor || rows.length >= limit) {
|
|
167
|
+
done(rows);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (options.after === undefined || String(cursor.key) > options.after)
|
|
171
|
+
rows.push(cursor.value);
|
|
172
|
+
if (rows.length === limit)
|
|
173
|
+
done(rows);
|
|
174
|
+
else
|
|
175
|
+
cursor.continue();
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async put(entry) {
|
|
180
|
+
key(entry.id);
|
|
181
|
+
const record = { id: entry.id, value: entry.value, updatedAt: Date.now(), blobBytes: entry.blob?.size ?? 0, blobType: entry.blob?.type ?? null };
|
|
182
|
+
return this.transaction(['records', 'blobs'], 'readwrite', (tx, done) => {
|
|
183
|
+
tx.objectStore('records').put(record, entry.id);
|
|
184
|
+
if (entry.blob)
|
|
185
|
+
tx.objectStore('blobs').put(entry.blob, `record:${entry.id}`);
|
|
186
|
+
else
|
|
187
|
+
tx.objectStore('blobs').delete(`record:${entry.id}`);
|
|
188
|
+
done(record);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
get(id) { return this.read('records', key(id)); }
|
|
192
|
+
getBlob(id) { return this.read('blobs', `record:${key(id)}`); }
|
|
193
|
+
list(options = {}) { return this.page('records', options); }
|
|
194
|
+
delete(id) {
|
|
195
|
+
key(id);
|
|
196
|
+
return this.transaction(['records', 'blobs'], 'readwrite', (tx, done) => {
|
|
197
|
+
tx.objectStore('records').delete(id);
|
|
198
|
+
tx.objectStore('blobs').delete(`record:${id}`);
|
|
199
|
+
done(undefined);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** @internal Shared lease excludes purge until durable work finishes. */
|
|
203
|
+
runWork(work, signal) {
|
|
204
|
+
return offlineLock(`${this.databaseName}:work`, work, true, { mode: 'shared', ...(signal ? { signal } : {}) });
|
|
205
|
+
}
|
|
206
|
+
/** Rejects busy while SDK jobs/downloads/sync are active. Cancel and await runners before retrying. */
|
|
207
|
+
clear() {
|
|
208
|
+
return offlineLock(`${this.databaseName}:work`, () => this.transaction(TABLES, 'readwrite', (tx, done) => { for (const table of TABLES)
|
|
209
|
+
tx.objectStore(table).clear(); done(undefined); }), true, { ifAvailable: true });
|
|
210
|
+
}
|
|
211
|
+
/** Portable JSON export of one user-selected record, including an optional base64 blob. */
|
|
212
|
+
async exportRecord(id) {
|
|
213
|
+
const snapshot = await this.transaction(['records', 'blobs'], 'readonly', (tx, done) => {
|
|
214
|
+
const record = tx.objectStore('records').get(key(id));
|
|
215
|
+
const blob = tx.objectStore('blobs').get(`record:${id}`);
|
|
216
|
+
blob.onsuccess = () => done({ record: record.result ?? null, blob: blob.result ?? null });
|
|
217
|
+
});
|
|
218
|
+
if (!snapshot.record)
|
|
219
|
+
throw new OfflineError('invalid', 'The local record does not exist.');
|
|
220
|
+
let base64 = null;
|
|
221
|
+
if (snapshot.blob) {
|
|
222
|
+
const bytes = new Uint8Array(await snapshot.blob.arrayBuffer());
|
|
223
|
+
const parts = [];
|
|
224
|
+
for (let start = 0; start < bytes.length; start += 0x8000)
|
|
225
|
+
parts.push(String.fromCharCode(...bytes.subarray(start, start + 0x8000)));
|
|
226
|
+
base64 = btoa(parts.join(''));
|
|
227
|
+
}
|
|
228
|
+
this.assertAccess();
|
|
229
|
+
return new Blob([JSON.stringify({ format: 'pas-local-record', version: 1, appId: this.appId, scope: this.scope, record: snapshot.record, base64 })], { type: 'application/json' });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export function createOfflineStore(options) { return new OfflineStore(options); }
|
|
233
|
+
/** Call on explicit logout/account changes; closing never deletes unsynced work. */
|
|
234
|
+
export function bindOfflineAccount(store, auth) {
|
|
235
|
+
const scope = store.scope;
|
|
236
|
+
if (scope.kind !== 'account')
|
|
237
|
+
throw new OfflineError('invalid', 'Only account-local stores bind to account changes.');
|
|
238
|
+
// Broadcast only a revocation bit within this app/owner namespace; never credentials.
|
|
239
|
+
const channel = typeof BroadcastChannel === 'undefined' ? null : new BroadcastChannel(`${store.databaseName}:access`);
|
|
240
|
+
if (channel)
|
|
241
|
+
channel.onmessage = (event) => { if (event.data === 'lock')
|
|
242
|
+
store.close(); };
|
|
243
|
+
const unsubscribe = auth.onChange((user) => {
|
|
244
|
+
if (!user || user.id !== scope.ownerId) {
|
|
245
|
+
store.close();
|
|
246
|
+
channel?.postMessage('lock');
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
return () => { unsubscribe(); channel?.close(); };
|
|
250
|
+
}
|
|
251
|
+
/** Persistence is a request, never a promise that the browser cannot remove data. */
|
|
252
|
+
export async function localStorageStatus(requestPersistence = false) {
|
|
253
|
+
const storage = typeof navigator === 'undefined' ? undefined : navigator.storage;
|
|
254
|
+
if (!storage)
|
|
255
|
+
return { supported: false, persisted: false, usage: null, quota: null };
|
|
256
|
+
const estimate = await storage.estimate();
|
|
257
|
+
const persisted = (await storage.persisted()) || (requestPersistence ? await storage.persist() : false);
|
|
258
|
+
return { supported: true, persisted, usage: estimate.usage ?? null, quota: estimate.quota ?? null };
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=store.js.map
|