@runappsai/sdk 0.5.1 → 0.6.1

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.
Files changed (68) hide show
  1. package/README.md +29 -0
  2. package/dist/activity-tracker.d.ts +8 -0
  3. package/dist/activity-tracker.d.ts.map +1 -1
  4. package/dist/activity-tracker.js +3 -0
  5. package/dist/activity-tracker.js.map +1 -1
  6. package/dist/browser-auth.d.ts +7 -0
  7. package/dist/browser-auth.d.ts.map +1 -1
  8. package/dist/browser-auth.js +105 -15
  9. package/dist/browser-auth.js.map +1 -1
  10. package/dist/client.d.ts +24 -0
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +22 -0
  13. package/dist/client.js.map +1 -1
  14. package/dist/events.d.ts +20 -0
  15. package/dist/events.d.ts.map +1 -1
  16. package/dist/events.js.map +1 -1
  17. package/dist/files.d.ts +3 -0
  18. package/dist/files.d.ts.map +1 -1
  19. package/dist/files.js +4 -1
  20. package/dist/files.js.map +1 -1
  21. package/dist/idb-sync/codec.d.ts +39 -0
  22. package/dist/idb-sync/codec.d.ts.map +1 -0
  23. package/dist/idb-sync/codec.js +364 -0
  24. package/dist/idb-sync/codec.js.map +1 -0
  25. package/dist/idb-sync/engine.d.ts +143 -0
  26. package/dist/idb-sync/engine.d.ts.map +1 -0
  27. package/dist/idb-sync/engine.js +727 -0
  28. package/dist/idb-sync/engine.js.map +1 -0
  29. package/dist/idb-sync/hook.d.ts +42 -0
  30. package/dist/idb-sync/hook.d.ts.map +1 -0
  31. package/dist/idb-sync/hook.js +426 -0
  32. package/dist/idb-sync/hook.js.map +1 -0
  33. package/dist/idb-sync/index.d.ts +6 -0
  34. package/dist/idb-sync/index.d.ts.map +1 -0
  35. package/dist/idb-sync/index.js +6 -0
  36. package/dist/idb-sync/index.js.map +1 -0
  37. package/dist/idb-sync/log.d.ts +41 -0
  38. package/dist/idb-sync/log.d.ts.map +1 -0
  39. package/dist/idb-sync/log.js +143 -0
  40. package/dist/idb-sync/log.js.map +1 -0
  41. package/dist/idb-sync/remote.d.ts +38 -0
  42. package/dist/idb-sync/remote.d.ts.map +1 -0
  43. package/dist/idb-sync/remote.js +61 -0
  44. package/dist/idb-sync/remote.js.map +1 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +3 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/sdk.umd.js +2 -2
  50. package/dist/sdk.umd.js.map +1 -1
  51. package/dist/transport.d.ts +5 -0
  52. package/dist/transport.d.ts.map +1 -1
  53. package/dist/transport.js +19 -6
  54. package/dist/transport.js.map +1 -1
  55. package/package.json +2 -1
  56. package/src/activity-tracker.ts +5 -0
  57. package/src/browser-auth.ts +103 -16
  58. package/src/client.ts +36 -0
  59. package/src/events.ts +22 -0
  60. package/src/files.ts +7 -5
  61. package/src/idb-sync/codec.ts +354 -0
  62. package/src/idb-sync/engine.ts +818 -0
  63. package/src/idb-sync/hook.ts +495 -0
  64. package/src/idb-sync/index.ts +5 -0
  65. package/src/idb-sync/log.ts +170 -0
  66. package/src/idb-sync/remote.ts +59 -0
  67. package/src/index.ts +13 -0
  68. package/src/transport.ts +20 -8
@@ -0,0 +1,354 @@
1
+ // idb-sync/codec.ts — JSON encoding for IndexedDB values and keys.
2
+ //
3
+ // IndexedDB stores anything the structured-clone algorithm accepts:
4
+ // Dates, Maps, Sets, binary, Blobs, nested objects, undefined. The sync
5
+ // snapshots on the server are JSON, so every such value is wrapped in a
6
+ // small tagged object on the way out and rebuilt on the way in. Plain
7
+ // JSON values pass through untouched, which keeps the common case (a
8
+ // record of strings and numbers) byte-for-byte readable on the server.
9
+ //
10
+ // Blobs are not inlined: `encodeValue` hands them to a `BlobSink` which
11
+ // stores the bytes elsewhere (the engine uploads them as files) and
12
+ // returns a reference; `decodeValue` asks a `BlobSource` for the bytes.
13
+
14
+ const TAG = "$rt";
15
+
16
+ /** How an encoded value refers to a Blob stored out of band. */
17
+ export interface BlobRef {
18
+ ref: string;
19
+ type: string;
20
+ size: number;
21
+ }
22
+
23
+ export interface BlobSink {
24
+ /** Store the blob and return a stable reference for it. */
25
+ (blob: Blob): Promise<BlobRef>;
26
+ }
27
+
28
+ export interface BlobSource {
29
+ (ref: BlobRef): Promise<Blob>;
30
+ }
31
+
32
+ type Tagged =
33
+ | { [TAG]: "date"; v: number | string }
34
+ | { [TAG]: "undef" }
35
+ | { [TAG]: "num"; v: "NaN" | "Infinity" | "-Infinity" | "-0" }
36
+ | { [TAG]: "bigint"; v: string }
37
+ | { [TAG]: "map"; v: unknown[] }
38
+ | { [TAG]: "set"; v: unknown[] }
39
+ | { [TAG]: "bin"; c: string; v: string }
40
+ | { [TAG]: "blob"; r: string; t: string; s: number; n?: string; m?: number }
41
+ | { [TAG]: "re"; s: string; f: string }
42
+ | { [TAG]: "obj"; v: Record<string, unknown> };
43
+
44
+ const BIN_CTORS: Record<string, (buf: ArrayBuffer) => ArrayBufferView | ArrayBuffer> = {
45
+ ArrayBuffer: (b) => b,
46
+ Uint8Array: (b) => new Uint8Array(b),
47
+ Int8Array: (b) => new Int8Array(b),
48
+ Uint8ClampedArray: (b) => new Uint8ClampedArray(b),
49
+ Int16Array: (b) => new Int16Array(b),
50
+ Uint16Array: (b) => new Uint16Array(b),
51
+ Int32Array: (b) => new Int32Array(b),
52
+ Uint32Array: (b) => new Uint32Array(b),
53
+ Float32Array: (b) => new Float32Array(b),
54
+ Float64Array: (b) => new Float64Array(b),
55
+ BigInt64Array: (b) => new BigInt64Array(b),
56
+ BigUint64Array: (b) => new BigUint64Array(b),
57
+ DataView: (b) => new DataView(b),
58
+ };
59
+
60
+ function bytesToBase64(bytes: Uint8Array): string {
61
+ let s = "";
62
+ for (let i = 0; i < bytes.length; i += 0x8000) {
63
+ s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
64
+ }
65
+ return btoa(s);
66
+ }
67
+
68
+ function base64ToBytes(b64: string): Uint8Array {
69
+ const s = atob(b64);
70
+ const out = new Uint8Array(s.length);
71
+ for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
72
+ return out;
73
+ }
74
+
75
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
76
+ if (v === null || typeof v !== "object") return false;
77
+ const proto = Object.getPrototypeOf(v);
78
+ return proto === Object.prototype || proto === null;
79
+ }
80
+
81
+ /**
82
+ * Encode a structured-clone value into a JSON-safe one. Async only
83
+ * because Blobs have to be read.
84
+ */
85
+ export async function encodeValue(value: unknown, sink?: BlobSink): Promise<unknown> {
86
+ if (value === undefined) return { [TAG]: "undef" } satisfies Tagged;
87
+ if (value === null) return null;
88
+ switch (typeof value) {
89
+ case "string":
90
+ case "boolean":
91
+ return value;
92
+ case "number":
93
+ if (Number.isNaN(value)) return { [TAG]: "num", v: "NaN" };
94
+ if (value === Infinity) return { [TAG]: "num", v: "Infinity" };
95
+ if (value === -Infinity) return { [TAG]: "num", v: "-Infinity" };
96
+ if (Object.is(value, -0)) return { [TAG]: "num", v: "-0" };
97
+ return value;
98
+ case "bigint":
99
+ return { [TAG]: "bigint", v: value.toString() };
100
+ case "function":
101
+ case "symbol":
102
+ throw new TypeError(`runapps sync: ${typeof value} values cannot be stored`);
103
+ }
104
+ if (value instanceof Date) {
105
+ const t = value.getTime();
106
+ return { [TAG]: "date", v: Number.isNaN(t) ? "invalid" : t };
107
+ }
108
+ if (value instanceof RegExp) return { [TAG]: "re", s: value.source, f: value.flags };
109
+ if (typeof Blob !== "undefined" && value instanceof Blob) {
110
+ if (!sink) throw new TypeError("runapps sync: Blob values need a blob sink");
111
+ const ref = await sink(value);
112
+ const out: Tagged = { [TAG]: "blob", r: ref.ref, t: ref.type, s: ref.size };
113
+ const f = value as Blob & { name?: string; lastModified?: number };
114
+ if (typeof File !== "undefined" && value instanceof File) {
115
+ out.n = f.name;
116
+ out.m = f.lastModified;
117
+ }
118
+ return out;
119
+ }
120
+ if (value instanceof ArrayBuffer) {
121
+ return { [TAG]: "bin", c: "ArrayBuffer", v: bytesToBase64(new Uint8Array(value)) };
122
+ }
123
+ if (ArrayBuffer.isView(value)) {
124
+ const name = value.constructor.name;
125
+ if (!(name in BIN_CTORS)) throw new TypeError(`runapps sync: unsupported binary type ${name}`);
126
+ const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
127
+ return { [TAG]: "bin", c: name, v: bytesToBase64(bytes) };
128
+ }
129
+ if (value instanceof Map) {
130
+ const v: unknown[] = [];
131
+ for (const [k, x] of value) v.push([await encodeValue(k, sink), await encodeValue(x, sink)]);
132
+ return { [TAG]: "map", v };
133
+ }
134
+ if (value instanceof Set) {
135
+ const v: unknown[] = [];
136
+ for (const x of value) v.push(await encodeValue(x, sink));
137
+ return { [TAG]: "set", v };
138
+ }
139
+ if (Array.isArray(value)) {
140
+ const out: unknown[] = new Array(value.length);
141
+ for (let i = 0; i < value.length; i++) out[i] = await encodeValue(value[i], sink);
142
+ return out;
143
+ }
144
+ if (isPlainObject(value)) {
145
+ // A plain object that happens to carry our tag key is wrapped so it
146
+ // cannot be mistaken for an encoding marker.
147
+ const needsWrap = Object.prototype.hasOwnProperty.call(value, TAG);
148
+ const out: Record<string, unknown> = {};
149
+ for (const k of Object.keys(value)) out[k] = await encodeValue(value[k], sink);
150
+ return needsWrap ? { [TAG]: "obj", v: out } : out;
151
+ }
152
+ // Class instances: structured clone keeps own enumerable properties
153
+ // only, so do the same.
154
+ const out: Record<string, unknown> = {};
155
+ for (const k of Object.keys(value as object)) out[k] = await encodeValue((value as Record<string, unknown>)[k], sink);
156
+ return out;
157
+ }
158
+
159
+ /** Inverse of `encodeValue`. */
160
+ export async function decodeValue(value: unknown, source?: BlobSource): Promise<unknown> {
161
+ if (value === null || typeof value !== "object") return value;
162
+ if (Array.isArray(value)) {
163
+ const out: unknown[] = new Array(value.length);
164
+ for (let i = 0; i < value.length; i++) out[i] = await decodeValue(value[i], source);
165
+ return out;
166
+ }
167
+ const obj = value as Record<string, unknown>;
168
+ const tag = obj[TAG];
169
+ if (typeof tag === "string") {
170
+ const t = obj as unknown as Tagged;
171
+ switch (t[TAG]) {
172
+ case "undef":
173
+ return undefined;
174
+ case "date":
175
+ return t.v === "invalid" ? new Date(NaN) : new Date(t.v);
176
+ case "num":
177
+ return t.v === "NaN" ? NaN : t.v === "Infinity" ? Infinity : t.v === "-Infinity" ? -Infinity : -0;
178
+ case "bigint":
179
+ return BigInt(t.v);
180
+ case "re":
181
+ return new RegExp(t.s, t.f);
182
+ case "bin": {
183
+ const bytes = base64ToBytes(t.v);
184
+ const buf = new ArrayBuffer(bytes.byteLength);
185
+ new Uint8Array(buf).set(bytes);
186
+ const make = BIN_CTORS[t.c];
187
+ if (!make) throw new TypeError(`runapps sync: unsupported binary type ${t.c}`);
188
+ return make(buf);
189
+ }
190
+ case "map": {
191
+ const m = new Map<unknown, unknown>();
192
+ for (const pair of t.v as [unknown, unknown][]) {
193
+ m.set(await decodeValue(pair[0], source), await decodeValue(pair[1], source));
194
+ }
195
+ return m;
196
+ }
197
+ case "set": {
198
+ const s = new Set<unknown>();
199
+ for (const x of t.v) s.add(await decodeValue(x, source));
200
+ return s;
201
+ }
202
+ case "blob": {
203
+ if (!source) throw new TypeError("runapps sync: Blob values need a blob source");
204
+ const blob = await source({ ref: t.r, type: t.t, size: t.s });
205
+ if (t.n !== undefined && typeof File !== "undefined") {
206
+ return new File([blob], t.n, { type: t.t, lastModified: t.m ?? Date.now() });
207
+ }
208
+ return blob;
209
+ }
210
+ case "obj":
211
+ return decodeValue(t.v, source);
212
+ }
213
+ }
214
+ const out: Record<string, unknown> = {};
215
+ for (const k of Object.keys(obj)) out[k] = await decodeValue(obj[k], source);
216
+ return out;
217
+ }
218
+
219
+ // ── Keys ─────────────────────────────────────────────────────────────
220
+ //
221
+ // A valid IndexedDB key is a number, a string, a Date, binary, or an
222
+ // array of those. Keys are encoded synchronously (no Blobs possible)
223
+ // and also rendered to a canonical string that serves as the record id
224
+ // inside a shard and as the input to the shard hash.
225
+
226
+ export function encodeKey(key: IDBValidKey): unknown {
227
+ if (typeof key === "number") {
228
+ if (Number.isNaN(key)) throw new TypeError("runapps sync: NaN is not a valid key");
229
+ return key;
230
+ }
231
+ if (typeof key === "string") return key;
232
+ if (key instanceof Date) return { [TAG]: "date", v: key.getTime() };
233
+ if (key instanceof ArrayBuffer) return { [TAG]: "bin", c: "ArrayBuffer", v: bytesToBase64(new Uint8Array(key)) };
234
+ if (ArrayBuffer.isView(key)) {
235
+ return {
236
+ [TAG]: "bin",
237
+ c: "ArrayBuffer",
238
+ v: bytesToBase64(new Uint8Array(key.buffer, key.byteOffset, key.byteLength)),
239
+ };
240
+ }
241
+ if (Array.isArray(key)) return key.map((k) => encodeKey(k as IDBValidKey));
242
+ throw new TypeError("runapps sync: invalid IndexedDB key");
243
+ }
244
+
245
+ export function decodeKey(encoded: unknown): IDBValidKey {
246
+ if (typeof encoded === "number" || typeof encoded === "string") return encoded;
247
+ if (Array.isArray(encoded)) return encoded.map(decodeKey);
248
+ if (encoded && typeof encoded === "object") {
249
+ const t = encoded as { [TAG]?: string; v?: unknown };
250
+ if (t[TAG] === "date") return new Date(t.v as number);
251
+ if (t[TAG] === "bin") {
252
+ const bytes = base64ToBytes(t.v as string);
253
+ const buf = new ArrayBuffer(bytes.byteLength);
254
+ new Uint8Array(buf).set(bytes);
255
+ return buf;
256
+ }
257
+ }
258
+ throw new TypeError("runapps sync: invalid encoded key");
259
+ }
260
+
261
+ /** Canonical string id for a key: equal keys give equal ids. */
262
+ export function keyId(key: IDBValidKey): string {
263
+ return JSON.stringify(encodeKey(key));
264
+ }
265
+
266
+ /** Deterministic 32-bit hash of a string (FNV-1a). */
267
+ export function hash32(s: string): number {
268
+ let h = 0x811c9dc5;
269
+ for (let i = 0; i < s.length; i++) {
270
+ h ^= s.charCodeAt(i);
271
+ h = Math.imul(h, 0x01000193);
272
+ }
273
+ return h >>> 0;
274
+ }
275
+
276
+ /**
277
+ * Structural equality for structured-clone values: Dates by time,
278
+ * binary by bytes, Map/Set by entries, Blobs by identity (two different
279
+ * Blob objects count as a change). Used to find which fields a write
280
+ * touched.
281
+ */
282
+ export function deepEqual(a: unknown, b: unknown): boolean {
283
+ if (Object.is(a, b)) return true;
284
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
285
+ if (a instanceof Date || b instanceof Date) {
286
+ return a instanceof Date && b instanceof Date && Object.is(a.getTime(), b.getTime());
287
+ }
288
+ if (typeof Blob !== "undefined" && (a instanceof Blob || b instanceof Blob)) return false;
289
+ if (ArrayBuffer.isView(a) || a instanceof ArrayBuffer || ArrayBuffer.isView(b) || b instanceof ArrayBuffer) {
290
+ const toBytes = (x: unknown): Uint8Array | null =>
291
+ x instanceof ArrayBuffer ? new Uint8Array(x) : ArrayBuffer.isView(x) ? new Uint8Array(x.buffer, x.byteOffset, x.byteLength) : null;
292
+ const ba = toBytes(a);
293
+ const bb = toBytes(b);
294
+ if (!ba || !bb || ba.length !== bb.length || a.constructor !== b.constructor) return false;
295
+ for (let i = 0; i < ba.length; i++) if (ba[i] !== bb[i]) return false;
296
+ return true;
297
+ }
298
+ if (a instanceof Map || b instanceof Map) {
299
+ if (!(a instanceof Map && b instanceof Map) || a.size !== b.size) return false;
300
+ for (const [k, v] of a) if (!b.has(k) || !deepEqual(v, b.get(k))) return false;
301
+ return true;
302
+ }
303
+ if (a instanceof Set || b instanceof Set) {
304
+ if (!(a instanceof Set && b instanceof Set) || a.size !== b.size) return false;
305
+ for (const v of a) if (!b.has(v)) return false;
306
+ return true;
307
+ }
308
+ if (Array.isArray(a) || Array.isArray(b)) {
309
+ if (!(Array.isArray(a) && Array.isArray(b)) || a.length !== b.length) return false;
310
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
311
+ return true;
312
+ }
313
+ const ka = Object.keys(a as object);
314
+ const kb = Object.keys(b as object);
315
+ if (ka.length !== kb.length) return false;
316
+ for (const k of ka) {
317
+ if (!Object.prototype.hasOwnProperty.call(b, k)) return false;
318
+ if (!deepEqual((a as Record<string, unknown>)[k], (b as Record<string, unknown>)[k])) return false;
319
+ }
320
+ return true;
321
+ }
322
+
323
+ /** The fields of `value` that differ from `prior` (added, changed or
324
+ * removed). `undefined` when there is no prior: everything is new. */
325
+ export function changedFields(prior: unknown, value: unknown): string[] | undefined {
326
+ if (!isPlainObject(prior) || !isPlainObject(value)) return undefined;
327
+ const out: string[] = [];
328
+ for (const k of new Set([...Object.keys(prior), ...Object.keys(value)])) {
329
+ const inP = Object.prototype.hasOwnProperty.call(prior, k);
330
+ const inV = Object.prototype.hasOwnProperty.call(value, k);
331
+ if (inP !== inV || !deepEqual(prior[k], value[k])) out.push(k);
332
+ }
333
+ return out;
334
+ }
335
+
336
+ /** Read an inline key out of a value by IndexedDB keyPath rules. */
337
+ export function extractKey(value: unknown, keyPath: string | string[] | null): IDBValidKey | undefined {
338
+ if (keyPath === null || keyPath === undefined) return undefined;
339
+ const one = (path: string): unknown => {
340
+ let cur: unknown = value;
341
+ if (path === "") return cur;
342
+ for (const part of path.split(".")) {
343
+ if (cur === null || typeof cur !== "object") return undefined;
344
+ cur = (cur as Record<string, unknown>)[part];
345
+ }
346
+ return cur;
347
+ };
348
+ if (Array.isArray(keyPath)) {
349
+ const parts = keyPath.map(one);
350
+ return parts.some((p) => p === undefined) ? undefined : (parts as IDBValidKey[]);
351
+ }
352
+ const k = one(keyPath);
353
+ return k === undefined ? undefined : (k as IDBValidKey);
354
+ }