@runappsai/sdk 0.5.1 → 0.6.2

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 +25 -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/index.d.ts +2 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/sdk.umd.js +2 -2
  26. package/dist/sdk.umd.js.map +1 -1
  27. package/dist/sync/codec.d.ts +39 -0
  28. package/dist/sync/codec.d.ts.map +1 -0
  29. package/dist/sync/codec.js +364 -0
  30. package/dist/sync/codec.js.map +1 -0
  31. package/dist/sync/engine.d.ts +143 -0
  32. package/dist/sync/engine.d.ts.map +1 -0
  33. package/dist/sync/engine.js +727 -0
  34. package/dist/sync/engine.js.map +1 -0
  35. package/dist/sync/hook.d.ts +42 -0
  36. package/dist/sync/hook.d.ts.map +1 -0
  37. package/dist/sync/hook.js +426 -0
  38. package/dist/sync/hook.js.map +1 -0
  39. package/dist/sync/index.d.ts +6 -0
  40. package/dist/sync/index.d.ts.map +1 -0
  41. package/dist/sync/index.js +6 -0
  42. package/dist/sync/index.js.map +1 -0
  43. package/dist/sync/log.d.ts +41 -0
  44. package/dist/sync/log.d.ts.map +1 -0
  45. package/dist/sync/log.js +143 -0
  46. package/dist/sync/log.js.map +1 -0
  47. package/dist/sync/remote.d.ts +38 -0
  48. package/dist/sync/remote.d.ts.map +1 -0
  49. package/dist/sync/remote.js +61 -0
  50. package/dist/sync/remote.js.map +1 -0
  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 +37 -0
  59. package/src/events.ts +22 -0
  60. package/src/files.ts +7 -5
  61. package/src/index.ts +13 -0
  62. package/src/sync/codec.ts +354 -0
  63. package/src/sync/engine.ts +818 -0
  64. package/src/sync/hook.ts +495 -0
  65. package/src/sync/index.ts +5 -0
  66. package/src/sync/log.ts +170 -0
  67. package/src/sync/remote.ts +59 -0
  68. package/src/transport.ts +20 -8
@@ -0,0 +1,59 @@
1
+ // sync/remote.ts — the sync engine's storage, over the files API.
2
+ //
3
+ // Shards are plain JSON files in the user's file space for the app, so
4
+ // they share its quota, billing and deletion with everything else the
5
+ // app stores for that user. Conditional reads (304) and writes (412 /
6
+ // 409) come from the files API, which checks them against the store.
7
+
8
+ import type { FilesService } from "../files.js";
9
+ import type { APIError } from "../errors.js";
10
+ import type { SyncRemote } from "./engine.js";
11
+
12
+ export class FilesRemote implements SyncRemote {
13
+ constructor(private readonly files: FilesService) {}
14
+
15
+ async list(prefix: string): Promise<{ path: string; etag: string }[]> {
16
+ const out: { path: string; etag: string }[] = [];
17
+ let cursor: string | undefined;
18
+ do {
19
+ const page = await this.files.list({ prefix, limit: 1000, ...(cursor ? { cursor } : {}) });
20
+ for (const f of page.files) if (f.etag) out.push({ path: f.path, etag: f.etag });
21
+ cursor = page.next_cursor;
22
+ } while (cursor);
23
+ return out;
24
+ }
25
+
26
+ async getJSON(path: string, ifNoneMatch?: string) {
27
+ const r = await this.files.readJSON(path, ifNoneMatch ? { ifNoneMatch } : undefined);
28
+ if (r.notModified) return { status: "unchanged" as const };
29
+ if (r.value === undefined) return { status: "missing" as const };
30
+ return { status: "ok" as const, value: r.value, etag: r.etag ?? "" };
31
+ }
32
+
33
+ async putJSON(path: string, value: unknown, cond: { ifMatch?: string; ifNoneMatch?: boolean }) {
34
+ try {
35
+ const obj = await this.files.put(path, JSON.stringify(value), {
36
+ contentType: "application/json",
37
+ ...(cond.ifMatch ? { ifMatch: cond.ifMatch } : {}),
38
+ ...(cond.ifNoneMatch ? { ifNoneMatch: true } : {}),
39
+ });
40
+ return { status: "ok" as const, etag: obj.etag ?? "", serverTime: obj.last_modified ? Date.parse(obj.last_modified) : undefined };
41
+ } catch (e) {
42
+ const code = (e as APIError).statusCode;
43
+ if (code === 412 || code === 409) return { status: "conflict" as const };
44
+ throw e;
45
+ }
46
+ }
47
+
48
+ async putBlob(path: string, blob: Blob): Promise<void> {
49
+ try {
50
+ await this.files.put(path, blob, { contentType: blob.type || "application/octet-stream", ifNoneMatch: true });
51
+ } catch (e) {
52
+ if ((e as APIError).statusCode !== 409) throw e; // already stored: same bytes, same name
53
+ }
54
+ }
55
+
56
+ getBlob(path: string): Promise<Blob> {
57
+ return this.files.get(path);
58
+ }
59
+ }
package/src/transport.ts CHANGED
@@ -167,20 +167,25 @@ export class Transport {
167
167
  * and the ETag of those bytes, when the server sends one. */
168
168
  async getRaw(
169
169
  path: string,
170
- init?: { signal?: AbortSignal },
171
- ): Promise<{ data: Uint8Array; contentType: string; etag?: string }> {
172
- const resp = await this.fetchWithAuthRetry(path, async () => ({
173
- method: "GET",
174
- headers: await this.authHeaders(),
175
- signal: init?.signal,
176
- }));
170
+ init?: { signal?: AbortSignal; ifNoneMatch?: string },
171
+ ): Promise<{ data: Uint8Array; contentType: string; etag?: string; notModified?: boolean; serverTime?: number }> {
172
+ const resp = await this.fetchWithAuthRetry(path, async () => {
173
+ const headers: Record<string, string> = await this.authHeaders();
174
+ if (init?.ifNoneMatch) headers["If-None-Match"] = `"${init.ifNoneMatch.replace(/"/g, "")}"`;
175
+ return { method: "GET", headers, signal: init?.signal };
176
+ });
177
+ const serverTime = parseServerDate(resp.headers.get("date"));
178
+ if (resp.status === 304) {
179
+ const etag = (resp.headers.get("etag") ?? "").replace(/"/g, "") || init?.ifNoneMatch;
180
+ return { data: new Uint8Array(0), contentType: "", etag, notModified: true, serverTime };
181
+ }
177
182
  if (!resp.ok) {
178
183
  throw await this.parseError(resp);
179
184
  }
180
185
  const buf = new Uint8Array(await resp.arrayBuffer());
181
186
  const contentType = resp.headers.get("content-type") ?? "";
182
187
  const etag = (resp.headers.get("etag") ?? "").replace(/"/g, "") || undefined;
183
- return { data: buf, contentType, etag };
188
+ return { data: buf, contentType, etag, serverTime };
184
189
  }
185
190
 
186
191
  /** POST a multipart/form-data body. Used by audio.transcribe and image.edit. */
@@ -308,3 +313,10 @@ export class Transport {
308
313
  return new APIError(resp.status, type, message);
309
314
  }
310
315
  }
316
+
317
+ /** The server's clock from a Date header, as ms since epoch. */
318
+ export function parseServerDate(h: string | null): number | undefined {
319
+ if (!h) return undefined;
320
+ const t = Date.parse(h);
321
+ return Number.isNaN(t) ? undefined : t;
322
+ }