@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.
- package/README.md +30 -1
- package/dist/activity-tracker.d.ts +8 -0
- package/dist/activity-tracker.d.ts.map +1 -1
- package/dist/activity-tracker.js +3 -0
- package/dist/activity-tracker.js.map +1 -1
- package/dist/browser-auth.d.ts +7 -0
- package/dist/browser-auth.d.ts.map +1 -1
- package/dist/browser-auth.js +105 -15
- package/dist/browser-auth.js.map +1 -1
- package/dist/client.d.ts +24 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +22 -0
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +20 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/files.d.ts +3 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +4 -1
- package/dist/files.js.map +1 -1
- package/dist/idb-sync/codec.d.ts +39 -0
- package/dist/idb-sync/codec.d.ts.map +1 -0
- package/dist/idb-sync/codec.js +364 -0
- package/dist/idb-sync/codec.js.map +1 -0
- package/dist/idb-sync/engine.d.ts +143 -0
- package/dist/idb-sync/engine.d.ts.map +1 -0
- package/dist/idb-sync/engine.js +727 -0
- package/dist/idb-sync/engine.js.map +1 -0
- package/dist/idb-sync/hook.d.ts +42 -0
- package/dist/idb-sync/hook.d.ts.map +1 -0
- package/dist/idb-sync/hook.js +426 -0
- package/dist/idb-sync/hook.js.map +1 -0
- package/dist/idb-sync/index.d.ts +6 -0
- package/dist/idb-sync/index.d.ts.map +1 -0
- package/dist/idb-sync/index.js +6 -0
- package/dist/idb-sync/index.js.map +1 -0
- package/dist/idb-sync/log.d.ts +41 -0
- package/dist/idb-sync/log.d.ts.map +1 -0
- package/dist/idb-sync/log.js +143 -0
- package/dist/idb-sync/log.js.map +1 -0
- package/dist/idb-sync/remote.d.ts +38 -0
- package/dist/idb-sync/remote.d.ts.map +1 -0
- package/dist/idb-sync/remote.js +61 -0
- package/dist/idb-sync/remote.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +1 -1
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +4 -2
- package/dist/models.js.map +1 -1
- package/dist/sdk.umd.js +2 -2
- package/dist/sdk.umd.js.map +1 -1
- package/dist/transport.d.ts +11 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +33 -6
- package/dist/transport.js.map +1 -1
- package/package.json +2 -1
- package/src/activity-tracker.ts +5 -0
- package/src/browser-auth.ts +103 -16
- package/src/client.ts +36 -0
- package/src/events.ts +22 -0
- package/src/files.ts +7 -5
- package/src/idb-sync/codec.ts +354 -0
- package/src/idb-sync/engine.ts +818 -0
- package/src/idb-sync/hook.ts +495 -0
- package/src/idb-sync/index.ts +5 -0
- package/src/idb-sync/log.ts +170 -0
- package/src/idb-sync/remote.ts +59 -0
- package/src/index.ts +13 -0
- package/src/models.ts +4 -2
- 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
|
-
|
|
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
|
-
|
|
156
|
-
headers
|
|
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
|
+
}
|