@runappsai/sdk 0.5.0 → 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 (73) hide show
  1. package/README.md +30 -1
  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/models.d.ts +1 -1
  50. package/dist/models.d.ts.map +1 -1
  51. package/dist/models.js +4 -2
  52. package/dist/models.js.map +1 -1
  53. package/dist/sdk.umd.js +2 -2
  54. package/dist/sdk.umd.js.map +1 -1
  55. package/dist/transport.d.ts +11 -0
  56. package/dist/transport.d.ts.map +1 -1
  57. package/dist/transport.js +33 -6
  58. package/dist/transport.js.map +1 -1
  59. package/package.json +2 -1
  60. package/src/activity-tracker.ts +5 -0
  61. package/src/browser-auth.ts +103 -16
  62. package/src/client.ts +36 -0
  63. package/src/events.ts +22 -0
  64. package/src/files.ts +7 -5
  65. package/src/idb-sync/codec.ts +354 -0
  66. package/src/idb-sync/engine.ts +818 -0
  67. package/src/idb-sync/hook.ts +495 -0
  68. package/src/idb-sync/index.ts +5 -0
  69. package/src/idb-sync/log.ts +170 -0
  70. package/src/idb-sync/remote.ts +59 -0
  71. package/src/index.ts +13 -0
  72. package/src/models.ts +4 -2
  73. package/src/transport.ts +38 -8
@@ -0,0 +1,59 @@
1
+ // idb-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/index.ts CHANGED
@@ -34,6 +34,19 @@ export {
34
34
  type ActivitySnapshot,
35
35
  } from "./activity-tracker.js";
36
36
  export { BrowserAuth, type BrowserAuthOptions, type BrowserUser } from "./browser-auth.js";
37
+ // IndexedDB sync — on by default in apps; exported for status UIs and
38
+ // for tests that drive the engine with their own storage.
39
+ export {
40
+ SyncEngine,
41
+ FilesRemote,
42
+ installIndexedDBHook,
43
+ type SyncOptions,
44
+ type SyncState,
45
+ type DbChange,
46
+ type SyncRemote,
47
+ type SyncDeps,
48
+ } from "./idb-sync/index.js";
49
+ export type { SyncStateEvent, DbChangeEvent } from "./events.js";
37
50
  export { APIError } from "./errors.js";
38
51
  export type { Usage } from "./types.js";
39
52
 
package/src/models.ts CHANGED
@@ -105,7 +105,7 @@ export class ModelsService {
105
105
 
106
106
  /**
107
107
  * List models available on the gateway. Pass `{ capability: "..." }` to
108
- * filter server-side.
108
+ * filter server-side. Needs no sign-in or key.
109
109
  */
110
110
  async list(
111
111
  opts: ModelListOptions = {},
@@ -115,7 +115,9 @@ export class ModelsService {
115
115
  if (opts.capability) {
116
116
  path += `?capability=${encodeURIComponent(opts.capability)}`;
117
117
  }
118
- const resp = await this.transport.getJSON<{ data: Model[] }>(path, init);
118
+ // The catalogue is public: no token, so listing models never sends a
119
+ // signed-out user to sign in.
120
+ const resp = await this.transport.getPublicJSON<{ data: Model[] }>(path, init);
119
121
  return resp.data;
120
122
  }
121
123
  }
package/src/transport.ts CHANGED
@@ -145,24 +145,47 @@ export class Transport {
145
145
  return (await resp.json()) as T;
146
146
  }
147
147
 
148
+ /** GET a public path: no Authorization header, so it never fetches a
149
+ * token — in browser sign-in mode that would send a signed-out user to
150
+ * the sign-in page just to read the model catalogue. */
151
+ async getPublicJSON<T>(
152
+ path: string,
153
+ init?: { signal?: AbortSignal },
154
+ ): Promise<T> {
155
+ const resp = await this.fetchImpl(this.baseURL + path, {
156
+ method: "GET",
157
+ cache: "no-store",
158
+ ...(init?.signal ? { signal: init.signal } : {}),
159
+ });
160
+ if (!resp.ok) {
161
+ throw await this.parseError(resp);
162
+ }
163
+ return (await resp.json()) as T;
164
+ }
165
+
148
166
  /** GET path; return raw bytes + content-type (for video/audio downloads)
149
167
  * and the ETag of those bytes, when the server sends one. */
150
168
  async getRaw(
151
169
  path: string,
152
- init?: { signal?: AbortSignal },
153
- ): Promise<{ data: Uint8Array; contentType: string; etag?: string }> {
154
- const resp = await this.fetchWithAuthRetry(path, async () => ({
155
- method: "GET",
156
- headers: await this.authHeaders(),
157
- signal: init?.signal,
158
- }));
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
+ }
159
182
  if (!resp.ok) {
160
183
  throw await this.parseError(resp);
161
184
  }
162
185
  const buf = new Uint8Array(await resp.arrayBuffer());
163
186
  const contentType = resp.headers.get("content-type") ?? "";
164
187
  const etag = (resp.headers.get("etag") ?? "").replace(/"/g, "") || undefined;
165
- return { data: buf, contentType, etag };
188
+ return { data: buf, contentType, etag, serverTime };
166
189
  }
167
190
 
168
191
  /** POST a multipart/form-data body. Used by audio.transcribe and image.edit. */
@@ -290,3 +313,10 @@ export class Transport {
290
313
  return new APIError(resp.status, type, message);
291
314
  }
292
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
+ }