@rayfold/client 0.1.0

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/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { RayfoldCache, entityKey, isRef } from "./cache.js";
2
+ export { createFetchTransport, createLocalTransport } from "./transport.js";
3
+ export { RayfoldClient, RayfoldClientError, Batch, OpHandle } from "./client.js";
4
+ export { localStorageQueue, memoryQueue, isUnreachable } from "./offline.js";
5
+ export { createWebSocketTransport } from "./ws-transport.js";
6
+ export { restoreTypes, typeAtPath } from "./types.js";
7
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,EAAsF,MAAM,YAAY,CAAC;AAChJ,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAA8C,MAAM,gBAAgB,CAAC;AACxH,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,KAAK,EAAE,QAAQ,EAAgG,MAAM,aAAa,CAAC;AAC/K,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAA0D,MAAM,cAAc,CAAC;AACrI,OAAO,EAAE,wBAAwB,EAA2B,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC","sourcesContent":["export { RayfoldCache, entityKey, isRef, type EntityKey, type Ref, type CachedResult, type CacheListener, type OptimisticOp } from \"./cache.ts\";\nexport { createFetchTransport, createLocalTransport, type Transport, type FetchTransportOptions } from \"./transport.ts\";\nexport { RayfoldClient, RayfoldClientError, Batch, OpHandle, type ClientOptions, type OpOptions, type CommandOptions, type QueryOptions, type BatchResult } from \"./client.ts\";\nexport { localStorageQueue, memoryQueue, isUnreachable, type QueueStorage, type QueuedCommand, type QueueEvent } from \"./offline.ts\";\nexport { createWebSocketTransport, type WsTransportOptions } from \"./ws-transport.ts\";\nexport { restoreTypes, typeAtPath } from \"./types.ts\";\n"]}
package/offline.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The offline queue of sub-profile `sync` (spec 08 section 5): commands made while the server cannot be reached wait,
3
+ * in the order they were made and with their idempotency keys, and go out when the connection is back. The keys make
4
+ * the resend safe: a command the server ran before its answer was lost is replayed, not run again.
5
+ */
6
+ import type { OptimisticOp } from "./cache.js";
7
+ import type { OpOptions } from "./client.js";
8
+ /** A command waiting for the server: everything needed to send it again, and to show its prediction meanwhile. */
9
+ export interface QueuedCommand {
10
+ key: string;
11
+ op: string;
12
+ args: Record<string, unknown>;
13
+ options: OpOptions;
14
+ optimistic?: OptimisticOp[];
15
+ queuedAt: number;
16
+ /** Order of creation: a command queued late because its attempt failed late still goes out in its place. */
17
+ seq: number;
18
+ }
19
+ /** Where the queue is kept, so waiting commands survive a reload. */
20
+ export interface QueueStorage {
21
+ load(): QueuedCommand[] | Promise<QueuedCommand[]>;
22
+ save(queue: readonly QueuedCommand[]): void | Promise<void>;
23
+ }
24
+ export interface QueueEvent {
25
+ type: "queued" | "sent" | "failed";
26
+ command: QueuedCommand;
27
+ /** Why the server refused a queued command (`failed`). */
28
+ error?: unknown;
29
+ /** Commands still waiting after this event. */
30
+ pending: number;
31
+ }
32
+ /** Keeps the queue in memory only: waiting commands are lost with the page. */
33
+ export declare function memoryQueue(): QueueStorage;
34
+ /** Keeps the queue in `localStorage` (or any storage with the same three methods) under `name`. */
35
+ export declare function localStorageQueue(name?: string, storage?: Pick<Storage, "getItem" | "setItem" | "removeItem"> | undefined): QueueStorage;
36
+ /** Whether a failure means the server could not be reached, so the command may go out again later with its key. */
37
+ export declare function isUnreachable(e: unknown): boolean;
38
+ /** @internal The queue itself; RayfoldClient owns one when created with `offline`. */
39
+ export declare class OfflineQueue {
40
+ private readonly storage;
41
+ private readonly send;
42
+ /** Called once a command has left the queue, sent or refused: its prediction goes. */
43
+ private readonly settle;
44
+ private readonly entries;
45
+ private draining;
46
+ private readonly listeners;
47
+ readonly restored: Promise<void>;
48
+ constructor(storage: QueueStorage, send: (c: QueuedCommand) => Promise<unknown>,
49
+ /** Called once a command has left the queue, sent or refused: its prediction goes. */
50
+ settle: (c: QueuedCommand) => void,
51
+ /** Called for each command a reload brought back: its prediction is shown again. */
52
+ restore: (c: QueuedCommand) => void);
53
+ get size(): number;
54
+ get commands(): readonly QueuedCommand[];
55
+ add<T>(command: QueuedCommand): Promise<T>;
56
+ /** Sends waiting commands in order and resolves to how many still wait: the rest stay when the server is unreachable. */
57
+ drain(): Promise<number>;
58
+ private run;
59
+ subscribe(fn: (e: QueueEvent) => void): () => void;
60
+ private emit;
61
+ }
package/offline.js ADDED
@@ -0,0 +1,119 @@
1
+ /** Keeps the queue in memory only: waiting commands are lost with the page. */
2
+ export function memoryQueue() {
3
+ let saved = [];
4
+ return { load: () => structuredClone(saved), save: (q) => void (saved = structuredClone([...q])) };
5
+ }
6
+ /** Keeps the queue in `localStorage` (or any storage with the same three methods) under `name`. */
7
+ export function localStorageQueue(name = "rayfold.queue", storage = globalThis.localStorage) {
8
+ return {
9
+ load: () => {
10
+ const raw = storage?.getItem(name);
11
+ if (!raw)
12
+ return [];
13
+ try {
14
+ const v = JSON.parse(raw);
15
+ return Array.isArray(v) ? v : [];
16
+ }
17
+ catch {
18
+ return []; // a damaged entry is dropped rather than blocking every later command
19
+ }
20
+ },
21
+ save: (q) => {
22
+ if (!storage)
23
+ return;
24
+ if (q.length)
25
+ storage.setItem(name, JSON.stringify(q));
26
+ else
27
+ storage.removeItem(name);
28
+ },
29
+ };
30
+ }
31
+ /** Whether a failure means the server could not be reached, so the command may go out again later with its key. */
32
+ export function isUnreachable(e) {
33
+ if (e instanceof TypeError)
34
+ return true; // what fetch throws when the network fails
35
+ return e?.code === "unavailable";
36
+ }
37
+ /** @internal The queue itself; RayfoldClient owns one when created with `offline`. */
38
+ export class OfflineQueue {
39
+ storage;
40
+ send;
41
+ settle;
42
+ entries = [];
43
+ draining = null;
44
+ listeners = new Set();
45
+ restored;
46
+ constructor(storage, send,
47
+ /** Called once a command has left the queue, sent or refused: its prediction goes. */
48
+ settle,
49
+ /** Called for each command a reload brought back: its prediction is shown again. */
50
+ restore) {
51
+ this.storage = storage;
52
+ this.send = send;
53
+ this.settle = settle;
54
+ this.restored = Promise.resolve(storage.load()).then((saved) => {
55
+ for (const command of saved) {
56
+ this.entries.push({ command });
57
+ restore(command);
58
+ }
59
+ });
60
+ }
61
+ get size() {
62
+ return this.entries.length;
63
+ }
64
+ get commands() {
65
+ return this.entries.map((e) => e.command);
66
+ }
67
+ add(command) {
68
+ const p = new Promise((resolve, reject) => {
69
+ const at = this.entries.findIndex((e) => e.command.seq > command.seq);
70
+ this.entries.splice(at < 0 ? this.entries.length : at, 0, { command, resolve: resolve, reject });
71
+ });
72
+ p.catch(() => { }); // a caller that fired and forgot must not see an unhandled rejection later
73
+ void this.storage.save(this.commands);
74
+ this.emit({ type: "queued", command, pending: this.entries.length });
75
+ return p;
76
+ }
77
+ /** Sends waiting commands in order and resolves to how many still wait: the rest stay when the server is unreachable. */
78
+ drain() {
79
+ this.draining ??= this.run().finally(() => {
80
+ this.draining = null;
81
+ });
82
+ return this.draining;
83
+ }
84
+ async run() {
85
+ await this.restored;
86
+ while (this.entries.length) {
87
+ const e = this.entries[0];
88
+ let result;
89
+ try {
90
+ result = await this.send(e.command);
91
+ }
92
+ catch (err) {
93
+ if (isUnreachable(err))
94
+ break; // still offline: it and everything behind it keep waiting
95
+ this.entries.shift();
96
+ await this.storage.save(this.commands);
97
+ this.settle(e.command);
98
+ e.reject?.(err);
99
+ this.emit({ type: "failed", command: e.command, error: err, pending: this.entries.length });
100
+ continue;
101
+ }
102
+ this.entries.shift();
103
+ await this.storage.save(this.commands);
104
+ this.settle(e.command);
105
+ e.resolve?.(result);
106
+ this.emit({ type: "sent", command: e.command, pending: this.entries.length });
107
+ }
108
+ return this.entries.length;
109
+ }
110
+ subscribe(fn) {
111
+ this.listeners.add(fn);
112
+ return () => this.listeners.delete(fn);
113
+ }
114
+ emit(e) {
115
+ for (const fn of this.listeners)
116
+ fn(e);
117
+ }
118
+ }
119
+ //# sourceMappingURL=offline.js.map
package/offline.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline.js","sourceRoot":"","sources":["../src/offline.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The offline queue of sub-profile `sync` (spec 08 section 5): commands made while the server cannot be reached wait,\n * in the order they were made and with their idempotency keys, and go out when the connection is back. The keys make\n * the resend safe: a command the server ran before its answer was lost is replayed, not run again.\n */\nimport type { OptimisticOp } from \"./cache.ts\";\nimport type { OpOptions } from \"./client.ts\";\n\n/** A command waiting for the server: everything needed to send it again, and to show its prediction meanwhile. */\nexport interface QueuedCommand {\n key: string;\n op: string;\n args: Record<string, unknown>;\n options: OpOptions;\n optimistic?: OptimisticOp[];\n queuedAt: number;\n /** Order of creation: a command queued late because its attempt failed late still goes out in its place. */\n seq: number;\n}\n\n/** Where the queue is kept, so waiting commands survive a reload. */\nexport interface QueueStorage {\n load(): QueuedCommand[] | Promise<QueuedCommand[]>;\n save(queue: readonly QueuedCommand[]): void | Promise<void>;\n}\n\nexport interface QueueEvent {\n type: \"queued\" | \"sent\" | \"failed\";\n command: QueuedCommand;\n /** Why the server refused a queued command (`failed`). */\n error?: unknown;\n /** Commands still waiting after this event. */\n pending: number;\n}\n\n/** Keeps the queue in memory only: waiting commands are lost with the page. */\nexport function memoryQueue(): QueueStorage {\n let saved: QueuedCommand[] = [];\n return { load: () => structuredClone(saved), save: (q) => void (saved = structuredClone([...q])) };\n}\n\n/** Keeps the queue in `localStorage` (or any storage with the same three methods) under `name`. */\nexport function localStorageQueue(name = \"rayfold.queue\", storage: Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\"> | undefined = globalThis.localStorage): QueueStorage {\n return {\n load: () => {\n const raw = storage?.getItem(name);\n if (!raw) return [];\n try {\n const v: unknown = JSON.parse(raw);\n return Array.isArray(v) ? (v as QueuedCommand[]) : [];\n } catch {\n return []; // a damaged entry is dropped rather than blocking every later command\n }\n },\n save: (q) => {\n if (!storage) return;\n if (q.length) storage.setItem(name, JSON.stringify(q));\n else storage.removeItem(name);\n },\n };\n}\n\n/** Whether a failure means the server could not be reached, so the command may go out again later with its key. */\nexport function isUnreachable(e: unknown): boolean {\n if (e instanceof TypeError) return true; // what fetch throws when the network fails\n return (e as { code?: unknown } | null)?.code === \"unavailable\";\n}\n\ninterface Entry {\n command: QueuedCommand;\n resolve?: (v: unknown) => void;\n reject?: (e: unknown) => void;\n}\n\n/** @internal The queue itself; RayfoldClient owns one when created with `offline`. */\nexport class OfflineQueue {\n private readonly entries: Entry[] = [];\n private draining: Promise<number> | null = null;\n private readonly listeners = new Set<(e: QueueEvent) => void>();\n readonly restored: Promise<void>;\n\n constructor(\n private readonly storage: QueueStorage,\n private readonly send: (c: QueuedCommand) => Promise<unknown>,\n /** Called once a command has left the queue, sent or refused: its prediction goes. */\n private readonly settle: (c: QueuedCommand) => void,\n /** Called for each command a reload brought back: its prediction is shown again. */\n restore: (c: QueuedCommand) => void,\n ) {\n this.restored = Promise.resolve(storage.load()).then((saved) => {\n for (const command of saved) {\n this.entries.push({ command });\n restore(command);\n }\n });\n }\n\n get size(): number {\n return this.entries.length;\n }\n\n get commands(): readonly QueuedCommand[] {\n return this.entries.map((e) => e.command);\n }\n\n add<T>(command: QueuedCommand): Promise<T> {\n const p = new Promise<T>((resolve, reject) => {\n const at = this.entries.findIndex((e) => e.command.seq > command.seq);\n this.entries.splice(at < 0 ? this.entries.length : at, 0, { command, resolve: resolve as (v: unknown) => void, reject });\n });\n p.catch(() => {}); // a caller that fired and forgot must not see an unhandled rejection later\n void this.storage.save(this.commands);\n this.emit({ type: \"queued\", command, pending: this.entries.length });\n return p;\n }\n\n /** Sends waiting commands in order and resolves to how many still wait: the rest stay when the server is unreachable. */\n drain(): Promise<number> {\n this.draining ??= this.run().finally(() => {\n this.draining = null;\n });\n return this.draining;\n }\n\n private async run(): Promise<number> {\n await this.restored;\n while (this.entries.length) {\n const e = this.entries[0]!;\n let result: unknown;\n try {\n result = await this.send(e.command);\n } catch (err) {\n if (isUnreachable(err)) break; // still offline: it and everything behind it keep waiting\n this.entries.shift();\n await this.storage.save(this.commands);\n this.settle(e.command);\n e.reject?.(err);\n this.emit({ type: \"failed\", command: e.command, error: err, pending: this.entries.length });\n continue;\n }\n this.entries.shift();\n await this.storage.save(this.commands);\n this.settle(e.command);\n e.resolve?.(result);\n this.emit({ type: \"sent\", command: e.command, pending: this.entries.length });\n }\n return this.entries.length;\n }\n\n subscribe(fn: (e: QueueEvent) => void): () => void {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n }\n\n private emit(e: QueueEvent): void {\n for (const fn of this.listeners) fn(e);\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@rayfold/client",
3
+ "version": "0.1.0",
4
+ "description": "Rayfold client for browsers and Node.js: a normalized cache kept current by server patches, batching and live queries.",
5
+ "keywords": [
6
+ "rayfold",
7
+ "api",
8
+ "protocol",
9
+ "client",
10
+ "cache",
11
+ "fetch",
12
+ "websocket",
13
+ "live-queries"
14
+ ],
15
+ "license": "Apache-2.0",
16
+ "homepage": "https://eddyboutros.github.io/rayfold/",
17
+ "bugs": {
18
+ "url": "https://github.com/eddyboutros/rayfold/issues"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/eddyboutros/rayfold.git",
23
+ "directory": "packages/client"
24
+ },
25
+ "type": "module",
26
+ "sideEffects": false,
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "main": "./index.js",
31
+ "types": "./index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./index.d.ts",
35
+ "default": "./index.js"
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "@rayfold/schema": "^0.1.0",
40
+ "@rayfold/rb": "^0.1.0",
41
+ "@rayfold/server": "^0.1.0"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }
package/transport.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /** Transports deliver a batch and yield frames. Spec: spec/04. */
2
+ import type { Frame, RequestEnvelope } from "@rayfold/server/protocol";
3
+ import { type RayfoldSchemaIR } from "@rayfold/schema";
4
+ export interface Transport {
5
+ send(envelope: RequestEnvelope, opts?: {
6
+ signal?: AbortSignal;
7
+ safe?: boolean;
8
+ }): AsyncIterable<Frame>;
9
+ }
10
+ /** A schema with the hash the server gives it, as GET /rayfold/manifest serves them. */
11
+ export interface SchemaWithHash {
12
+ schema: RayfoldSchemaIR;
13
+ schemaHash: string;
14
+ }
15
+ export interface FetchTransportOptions {
16
+ url: string;
17
+ /** Extra headers per request (e.g. Authorization). */
18
+ headers?: () => Record<string, string> | Promise<Record<string, string>>;
19
+ fetch?: typeof fetch;
20
+ /** Use the HTTP QUERY method for safe batches when the runtime supports it. Default: POST + Rayfold-Safe. */
21
+ useQueryMethod?: boolean;
22
+ /**
23
+ * Rayfold Binary: send and read RB instead of JSON. Pass the manifest from GET /rayfold/manifest, or the server's full
24
+ * schema IR. RB key numbers come from the schema, so RB is used only once a response's Rayfold-Schema header shows the
25
+ * server holds that same schema (spec 09 section 3). Until then, and after a response with another hash, requests go
26
+ * as JSON. The manifest's `schema` on its own hashes differently from the server's schema, so pass the whole manifest.
27
+ */
28
+ binary?: RayfoldSchemaIR | SchemaWithHash;
29
+ }
30
+ /** HTTP transport: POST (or QUERY) /rayfold, response is newline-delimited frames streamed as they arrive. */
31
+ export declare function createFetchTransport(o: FetchTransportOptions): Transport;
32
+ /** In-process transport over a RayfoldServer-like object (tests, SSR, workers). */
33
+ export declare function createLocalTransport(server: {
34
+ execute(envelope: RequestEnvelope, opts?: {
35
+ viewer?: unknown;
36
+ signal?: AbortSignal;
37
+ }): AsyncIterable<Frame>;
38
+ }, viewer?: () => unknown): Transport;
package/transport.js ADDED
@@ -0,0 +1,109 @@
1
+ import { RbCodec, RB_CONTENT_TYPE } from "@rayfold/rb";
2
+ import { schemaHash } from "@rayfold/schema";
3
+ /** HTTP transport: POST (or QUERY) /rayfold, response is newline-delimited frames streamed as they arrive. */
4
+ export function createFetchTransport(o) {
5
+ const f = o.fetch ?? globalThis.fetch;
6
+ const rb = o.binary
7
+ ? "schemaHash" in o.binary
8
+ ? { codec: new RbCodec(o.binary.schema), hash: o.binary.schemaHash }
9
+ : { codec: new RbCodec(o.binary), hash: schemaHash(o.binary) }
10
+ : null;
11
+ // the hash the server's last response reported; RB waits until it is known to match the codec's schema
12
+ let serverHash = null;
13
+ return {
14
+ send(envelope, opts = {}) {
15
+ return (async function* () {
16
+ const codec = rb !== null && serverHash === rb.hash ? rb.codec : null;
17
+ const headers = codec
18
+ ? { "content-type": RB_CONTENT_TYPE, accept: RB_CONTENT_TYPE, ...(await o.headers?.()) }
19
+ : { "content-type": "application/rayfold+json", accept: "application/rayfold-frames+json", ...(await o.headers?.()) };
20
+ let method = "POST";
21
+ if (opts.safe) {
22
+ if (o.useQueryMethod)
23
+ method = "QUERY";
24
+ else
25
+ headers["rayfold-safe"] = "true";
26
+ }
27
+ const init = { method, headers, body: codec ? codec.encode(envelope) : JSON.stringify(envelope) };
28
+ if (opts.signal)
29
+ init.signal = opts.signal;
30
+ const res = await f(o.url, init);
31
+ const reported = res.headers.get("rayfold-schema");
32
+ if (reported)
33
+ serverHash = reported;
34
+ const ct = res.headers.get("content-type") ?? "";
35
+ if (codec && ct.startsWith(RB_CONTENT_TYPE)) {
36
+ if (reported !== rb?.hash) {
37
+ // the server moved to another schema since the last response: its key numbers are not this codec's
38
+ await res.body?.cancel();
39
+ yield {
40
+ error: { code: "unavailable", message: "The server's schema changed, so its binary answer cannot be read. Later requests use JSON; load the manifest again to use RB." },
41
+ fin: true,
42
+ };
43
+ return;
44
+ }
45
+ const d = codec.decoder();
46
+ if (res.body) {
47
+ const reader = res.body.getReader();
48
+ for (;;) {
49
+ const { value, done } = await reader.read();
50
+ if (done)
51
+ break;
52
+ for (const fr of d.feed(value))
53
+ yield fr;
54
+ }
55
+ }
56
+ else
57
+ for (const fr of codec.decodeFrames(new Uint8Array(await res.arrayBuffer())))
58
+ yield fr;
59
+ return;
60
+ }
61
+ if (!res.ok && !ct.startsWith("application/rayfold-frames+json")) {
62
+ const body = ct.includes("json") ? await res.json().catch(() => ({})) : {};
63
+ const code = typeof body.code === "string" ? body.code : "unavailable";
64
+ yield { error: { code, message: body.detail ?? `HTTP ${res.status}` }, fin: true };
65
+ return;
66
+ }
67
+ if (!res.body) {
68
+ const text = await res.text();
69
+ for (const line of text.split("\n"))
70
+ if (line.trim())
71
+ yield JSON.parse(line);
72
+ return;
73
+ }
74
+ const reader = res.body.getReader();
75
+ const decoder = new TextDecoder();
76
+ let buf = "";
77
+ for (;;) {
78
+ const { value, done } = await reader.read();
79
+ if (done)
80
+ break;
81
+ buf += decoder.decode(value, { stream: true });
82
+ let nl;
83
+ while ((nl = buf.indexOf("\n")) >= 0) {
84
+ const line = buf.slice(0, nl).trim();
85
+ buf = buf.slice(nl + 1);
86
+ if (line)
87
+ yield JSON.parse(line);
88
+ }
89
+ }
90
+ if (buf.trim())
91
+ yield JSON.parse(buf);
92
+ })();
93
+ },
94
+ };
95
+ }
96
+ /** In-process transport over a RayfoldServer-like object (tests, SSR, workers). */
97
+ export function createLocalTransport(server, viewer) {
98
+ return {
99
+ send(envelope, opts = {}) {
100
+ const o = {};
101
+ if (viewer)
102
+ o.viewer = viewer();
103
+ if (opts.signal)
104
+ o.signal = opts.signal;
105
+ return server.execute(envelope, o);
106
+ },
107
+ };
108
+ }
109
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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","sourcesContent":["/** Transports deliver a batch and yield frames. Spec: spec/04. */\nimport type { Frame, RequestEnvelope } from \"@rayfold/server/protocol\";\nimport { RbCodec, RB_CONTENT_TYPE } from \"@rayfold/rb\";\nimport { schemaHash, type RayfoldSchemaIR } from \"@rayfold/schema\";\n\nexport interface Transport {\n send(envelope: RequestEnvelope, opts?: { signal?: AbortSignal; safe?: boolean }): AsyncIterable<Frame>;\n}\n\n/** A schema with the hash the server gives it, as GET /rayfold/manifest serves them. */\nexport interface SchemaWithHash {\n schema: RayfoldSchemaIR;\n schemaHash: string;\n}\n\nexport interface FetchTransportOptions {\n url: string;\n /** Extra headers per request (e.g. Authorization). */\n headers?: () => Record<string, string> | Promise<Record<string, string>>;\n fetch?: typeof fetch;\n /** Use the HTTP QUERY method for safe batches when the runtime supports it. Default: POST + Rayfold-Safe. */\n useQueryMethod?: boolean;\n /**\n * Rayfold Binary: send and read RB instead of JSON. Pass the manifest from GET /rayfold/manifest, or the server's full\n * schema IR. RB key numbers come from the schema, so RB is used only once a response's Rayfold-Schema header shows the\n * server holds that same schema (spec 09 section 3). Until then, and after a response with another hash, requests go\n * as JSON. The manifest's `schema` on its own hashes differently from the server's schema, so pass the whole manifest.\n */\n binary?: RayfoldSchemaIR | SchemaWithHash;\n}\n\n/** HTTP transport: POST (or QUERY) /rayfold, response is newline-delimited frames streamed as they arrive. */\nexport function createFetchTransport(o: FetchTransportOptions): Transport {\n const f = o.fetch ?? globalThis.fetch;\n const rb = o.binary\n ? \"schemaHash\" in o.binary\n ? { codec: new RbCodec(o.binary.schema), hash: o.binary.schemaHash }\n : { codec: new RbCodec(o.binary), hash: schemaHash(o.binary) }\n : null;\n // the hash the server's last response reported; RB waits until it is known to match the codec's schema\n let serverHash: string | null = null;\n return {\n send(envelope, opts = {}) {\n return (async function* () {\n const codec = rb !== null && serverHash === rb.hash ? rb.codec : null;\n const headers: Record<string, string> = codec\n ? { \"content-type\": RB_CONTENT_TYPE, accept: RB_CONTENT_TYPE, ...(await o.headers?.()) }\n : { \"content-type\": \"application/rayfold+json\", accept: \"application/rayfold-frames+json\", ...(await o.headers?.()) };\n let method = \"POST\";\n if (opts.safe) {\n if (o.useQueryMethod) method = \"QUERY\";\n else headers[\"rayfold-safe\"] = \"true\";\n }\n const init: RequestInit = { method, headers, body: codec ? (codec.encode(envelope) as BodyInit) : JSON.stringify(envelope) };\n if (opts.signal) init.signal = opts.signal;\n const res = await f(o.url, init);\n const reported = res.headers.get(\"rayfold-schema\");\n if (reported) serverHash = reported;\n const ct = res.headers.get(\"content-type\") ?? \"\";\n if (codec && ct.startsWith(RB_CONTENT_TYPE)) {\n if (reported !== rb?.hash) {\n // the server moved to another schema since the last response: its key numbers are not this codec's\n await res.body?.cancel();\n yield {\n error: { code: \"unavailable\", message: \"The server's schema changed, so its binary answer cannot be read. Later requests use JSON; load the manifest again to use RB.\" },\n fin: true,\n } as Frame;\n return;\n }\n const d = codec.decoder();\n if (res.body) {\n const reader = res.body.getReader();\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n for (const fr of d.feed(value)) yield fr as Frame;\n }\n } else for (const fr of codec.decodeFrames(new Uint8Array(await res.arrayBuffer()))) yield fr as Frame;\n return;\n }\n if (!res.ok && !ct.startsWith(\"application/rayfold-frames+json\")) {\n const body = ct.includes(\"json\") ? await res.json().catch(() => ({})) : {};\n const code = typeof body.code === \"string\" ? body.code : \"unavailable\";\n yield { error: { code, message: body.detail ?? `HTTP ${res.status}` }, fin: true } as Frame;\n return;\n }\n if (!res.body) {\n const text = await res.text();\n for (const line of text.split(\"\\n\")) if (line.trim()) yield JSON.parse(line) as Frame;\n return;\n }\n const reader = res.body.getReader();\n const decoder = new TextDecoder();\n let buf = \"\";\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n buf += decoder.decode(value, { stream: true });\n let nl: number;\n while ((nl = buf.indexOf(\"\\n\")) >= 0) {\n const line = buf.slice(0, nl).trim();\n buf = buf.slice(nl + 1);\n if (line) yield JSON.parse(line) as Frame;\n }\n }\n if (buf.trim()) yield JSON.parse(buf) as Frame;\n })();\n },\n };\n}\n\n/** In-process transport over a RayfoldServer-like object (tests, SSR, workers). */\nexport function createLocalTransport(server: { execute(envelope: RequestEnvelope, opts?: { viewer?: unknown; signal?: AbortSignal }): AsyncIterable<Frame> }, viewer?: () => unknown): Transport {\n return {\n send(envelope, opts = {}) {\n const o: { viewer?: unknown; signal?: AbortSignal } = {};\n if (viewer) o.viewer = viewer();\n if (opts.signal) o.signal = opts.signal;\n return server.execute(envelope, o);\n },\n };\n}\n"]}
package/types.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /** Schema-aware type restoration for compact frames: re-adds `$type` where the static type is an entity. */
2
+ import { type RayfoldSchemaIR, type TypeRef } from "@rayfold/schema";
3
+ export declare function restoreTypes(ir: RayfoldSchemaIR, t: TypeRef, v: unknown): unknown;
4
+ /** Static type at a result path such as "items.0.author" (indices skip list levels). */
5
+ export declare function typeAtPath(ir: RayfoldSchemaIR, root: TypeRef, path: string): TypeRef | undefined;
package/types.js ADDED
@@ -0,0 +1,48 @@
1
+ /** Schema-aware type restoration for compact frames: re-adds `$type` where the static type is an entity. */
2
+ import { fieldsOf } from "@rayfold/schema";
3
+ export function restoreTypes(ir, t, v) {
4
+ if (v === null || typeof v !== "object")
5
+ return v;
6
+ if (t.kind === "list")
7
+ return Array.isArray(v) ? v.map((x) => restoreTypes(ir, t.of, x)) : v;
8
+ if (Array.isArray(v))
9
+ return v;
10
+ const o = v;
11
+ const explicit = typeof o["$type"] === "string" ? o["$type"] : undefined;
12
+ const def = ir.types[explicit ?? t.name];
13
+ if (!def)
14
+ return v;
15
+ const out = {};
16
+ if (def.kind === "entity")
17
+ out["$type"] = def.name;
18
+ const ref = explicit ? { kind: "named", name: explicit, nullable: false } : t;
19
+ const fields = fieldsOf(ir, ref) ?? [];
20
+ for (const [k, x] of Object.entries(o)) {
21
+ if (k === "$type")
22
+ continue;
23
+ const f = fields.find((fd) => fd.name === k);
24
+ out[k] = f ? restoreTypes(ir, f.type, x) : x;
25
+ }
26
+ return out;
27
+ }
28
+ /** Static type at a result path such as "items.0.author" (indices skip list levels). */
29
+ export function typeAtPath(ir, root, path) {
30
+ let t = root;
31
+ if (path === "")
32
+ return t;
33
+ for (const seg of path.split(".")) {
34
+ if (/^\d+$/.test(seg)) {
35
+ if (t.kind === "list")
36
+ t = t.of;
37
+ continue;
38
+ }
39
+ while (t.kind === "list")
40
+ t = t.of;
41
+ const f = (fieldsOf(ir, t) ?? []).find((fd) => fd.name === seg);
42
+ if (!f)
43
+ return undefined;
44
+ t = f.type;
45
+ }
46
+ return t;
47
+ }
48
+ //# sourceMappingURL=types.js.map
package/types.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,4GAA4G;AAC5G,OAAO,EAAE,QAAQ,EAAsC,MAAM,iBAAiB,CAAC;AAE/E,MAAM,UAAU,YAAY,CAAC,EAAmB,EAAE,CAAU,EAAE,CAAU;IACtE,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IAClD,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7F,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IAC/B,MAAM,CAAC,GAAG,CAA4B,CAAC;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACzE,MAAM,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC,GAAG;QAAE,OAAO,CAAC,CAAC;IACnB,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;QAAE,GAAG,CAAC,OAAO,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC;IACnD,MAAM,GAAG,GAAY,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACvF,MAAM,MAAM,GAAG,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,IAAI,CAAC,KAAK,OAAO;YAAE,SAAS;QAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC;QAC7C,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,UAAU,CAAC,EAAmB,EAAE,IAAa,EAAE,IAAY;IACzE,IAAI,CAAC,GAAY,IAAI,CAAC;IACtB,IAAI,IAAI,KAAK,EAAE;QAAE,OAAO,CAAC,CAAC;IAC1B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QAClC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM;gBAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAChC,SAAS;QACX,CAAC;QACD,OAAO,CAAC,CAAC,IAAI,KAAK,MAAM;YAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;QAChE,IAAI,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACzB,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;IACb,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC","sourcesContent":["/** Schema-aware type restoration for compact frames: re-adds `$type` where the static type is an entity. */\nimport { fieldsOf, type RayfoldSchemaIR, type TypeRef } from \"@rayfold/schema\";\n\nexport function restoreTypes(ir: RayfoldSchemaIR, t: TypeRef, v: unknown): unknown {\n if (v === null || typeof v !== \"object\") return v;\n if (t.kind === \"list\") return Array.isArray(v) ? v.map((x) => restoreTypes(ir, t.of, x)) : v;\n if (Array.isArray(v)) return v;\n const o = v as Record<string, unknown>;\n const explicit = typeof o[\"$type\"] === \"string\" ? o[\"$type\"] : undefined;\n const def = ir.types[explicit ?? t.name];\n if (!def) return v;\n const out: Record<string, unknown> = {};\n if (def.kind === \"entity\") out[\"$type\"] = def.name;\n const ref: TypeRef = explicit ? { kind: \"named\", name: explicit, nullable: false } : t;\n const fields = fieldsOf(ir, ref) ?? [];\n for (const [k, x] of Object.entries(o)) {\n if (k === \"$type\") continue;\n const f = fields.find((fd) => fd.name === k);\n out[k] = f ? restoreTypes(ir, f.type, x) : x;\n }\n return out;\n}\n\n/** Static type at a result path such as \"items.0.author\" (indices skip list levels). */\nexport function typeAtPath(ir: RayfoldSchemaIR, root: TypeRef, path: string): TypeRef | undefined {\n let t: TypeRef = root;\n if (path === \"\") return t;\n for (const seg of path.split(\".\")) {\n if (/^\\d+$/.test(seg)) {\n if (t.kind === \"list\") t = t.of;\n continue;\n }\n while (t.kind === \"list\") t = t.of;\n const f = (fieldsOf(ir, t) ?? []).find((fd) => fd.name === seg);\n if (!f) return undefined;\n t = f.type;\n }\n return t;\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { RayfoldSchemaIR } from "@rayfold/schema";
2
+ import type { Transport } from "./transport.js";
3
+ export interface WsTransportOptions {
4
+ url: string;
5
+ /** Subprotocols; default ["rayfold.0.1"]. */
6
+ protocols?: string[];
7
+ /** WebSocket constructor (defaults to the global one). */
8
+ WebSocket?: typeof WebSocket;
9
+ /** Called to build a fresh socket URL (e.g. to append a token) before each connect. */
10
+ connectUrl?: () => string | Promise<string>;
11
+ /** Rayfold Binary: pass the schema IR (from /rayfold/manifest) to send and receive RB messages instead of JSON text. */
12
+ binary?: RayfoldSchemaIR;
13
+ }
14
+ export declare function createWebSocketTransport(o: WsTransportOptions): Transport & {
15
+ close(): void;
16
+ };