@rayfold/server 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.
Files changed (73) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +70 -0
  4. package/args.d.ts +15 -0
  5. package/args.js +226 -0
  6. package/args.js.map +1 -0
  7. package/batch.d.ts +54 -0
  8. package/batch.js +483 -0
  9. package/batch.js.map +1 -0
  10. package/bindings.d.ts +37 -0
  11. package/bindings.js +284 -0
  12. package/bindings.js.map +1 -0
  13. package/capability-scope.d.ts +8 -0
  14. package/capability-scope.js +19 -0
  15. package/capability-scope.js.map +1 -0
  16. package/capability.d.ts +56 -0
  17. package/capability.js +112 -0
  18. package/capability.js.map +1 -0
  19. package/context.d.ts +74 -0
  20. package/context.js +109 -0
  21. package/context.js.map +1 -0
  22. package/core.d.ts +18 -0
  23. package/core.js +18 -0
  24. package/core.js.map +1 -0
  25. package/cost.d.ts +15 -0
  26. package/cost.js +110 -0
  27. package/cost.js.map +1 -0
  28. package/executor.d.ts +89 -0
  29. package/executor.js +695 -0
  30. package/executor.js.map +1 -0
  31. package/guard.d.ts +33 -0
  32. package/guard.js +65 -0
  33. package/guard.js.map +1 -0
  34. package/http.d.ts +33 -0
  35. package/http.js +379 -0
  36. package/http.js.map +1 -0
  37. package/index.d.ts +8 -0
  38. package/index.js +9 -0
  39. package/index.js.map +1 -0
  40. package/instrumentation.d.ts +36 -0
  41. package/instrumentation.js +2 -0
  42. package/instrumentation.js.map +1 -0
  43. package/live.d.ts +37 -0
  44. package/live.js +240 -0
  45. package/live.js.map +1 -0
  46. package/mcp.d.ts +55 -0
  47. package/mcp.js +314 -0
  48. package/mcp.js.map +1 -0
  49. package/openapi.d.ts +11 -0
  50. package/openapi.js +124 -0
  51. package/openapi.js.map +1 -0
  52. package/package.json +53 -0
  53. package/policy.d.ts +17 -0
  54. package/policy.js +64 -0
  55. package/policy.js.map +1 -0
  56. package/protocol.d.ts +139 -0
  57. package/protocol.js +98 -0
  58. package/protocol.js.map +1 -0
  59. package/server.d.ts +58 -0
  60. package/server.js +79 -0
  61. package/server.js.map +1 -0
  62. package/usage.d.ts +39 -0
  63. package/usage.js +44 -0
  64. package/usage.js.map +1 -0
  65. package/views.d.ts +33 -0
  66. package/views.js +108 -0
  67. package/views.js.map +1 -0
  68. package/wiring.d.ts +16 -0
  69. package/wiring.js +57 -0
  70. package/wiring.js.map +1 -0
  71. package/ws.d.ts +21 -0
  72. package/ws.js +209 -0
  73. package/ws.js.map +1 -0
package/context.js ADDED
@@ -0,0 +1,109 @@
1
+ /** In-process event bus used for `emits` and for streams that subscribe to events. */
2
+ export class EventBus {
3
+ subs = new Map();
4
+ seq = 0;
5
+ publish(name, payload) {
6
+ this.seq++;
7
+ const enriched = { ...payload, seq: this.seq };
8
+ for (const fn of this.subs.get(name) ?? [])
9
+ fn(enriched);
10
+ for (const fn of this.subs.get("*") ?? [])
11
+ fn({ event: name, ...enriched });
12
+ }
13
+ on(name, fn) {
14
+ let set = this.subs.get(name);
15
+ if (!set)
16
+ this.subs.set(name, (set = new Set()));
17
+ set.add(fn);
18
+ return () => set.delete(fn);
19
+ }
20
+ /** Async iterator over an event; ends when `signal` aborts. Buffers between pulls. */
21
+ subscribe(name, signal) {
22
+ const bus = this;
23
+ return {
24
+ [Symbol.asyncIterator]() {
25
+ const queue = [];
26
+ let waiting = null;
27
+ let done = false;
28
+ const off = bus.on(name, (p) => {
29
+ if (done)
30
+ return;
31
+ if (waiting) {
32
+ const w = waiting;
33
+ waiting = null;
34
+ w({ value: p, done: false });
35
+ }
36
+ else
37
+ queue.push(p);
38
+ });
39
+ const finish = () => {
40
+ if (done)
41
+ return;
42
+ done = true;
43
+ off();
44
+ if (waiting) {
45
+ const w = waiting;
46
+ waiting = null;
47
+ w({ value: undefined, done: true });
48
+ }
49
+ };
50
+ signal?.addEventListener("abort", finish, { once: true });
51
+ if (signal?.aborted)
52
+ finish();
53
+ return {
54
+ next() {
55
+ if (queue.length)
56
+ return Promise.resolve({ value: queue.shift(), done: false });
57
+ if (done)
58
+ return Promise.resolve({ value: undefined, done: true });
59
+ return new Promise((res) => (waiting = res));
60
+ },
61
+ return() {
62
+ finish();
63
+ return Promise.resolve({ value: undefined, done: true });
64
+ },
65
+ };
66
+ },
67
+ };
68
+ }
69
+ }
70
+ /**
71
+ * In-memory idempotency records. Records expire after `ttlMs`; expired ones are dropped on read and swept on every
72
+ * write, and past `maxEntries` the oldest go first, so memory stays bounded however many commands arrive.
73
+ */
74
+ export class MemoryIdempotencyStore {
75
+ ttlMs;
76
+ now;
77
+ maxEntries;
78
+ map = new Map();
79
+ constructor(ttlMs = 24 * 3_600_000, now = Date.now, maxEntries = 100_000) {
80
+ this.ttlMs = ttlMs;
81
+ this.now = now;
82
+ this.maxEntries = maxEntries;
83
+ }
84
+ async get(scope, key) {
85
+ const k = `${scope}\u0000${key}`;
86
+ const r = this.map.get(k);
87
+ if (r && this.now() - r.at > this.ttlMs) {
88
+ this.map.delete(k);
89
+ return undefined;
90
+ }
91
+ return r;
92
+ }
93
+ async put(scope, key, record) {
94
+ const k = `${scope}\u0000${key}`;
95
+ this.map.delete(k); // re-inserted at the end, so the map stays ordered oldest first
96
+ this.map.set(k, record);
97
+ const t = this.now();
98
+ for (const [old, r] of this.map) {
99
+ if (this.map.size > this.maxEntries || t - r.at > this.ttlMs)
100
+ this.map.delete(old);
101
+ else
102
+ break;
103
+ }
104
+ }
105
+ get size() {
106
+ return this.map.size;
107
+ }
108
+ }
109
+ //# sourceMappingURL=context.js.map
package/context.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Expr, Shape } from \"@rayfold/schema\";\nimport type { RequestMeta } from \"./protocol.ts\";\n\n/** In-process event bus used for `emits` and for streams that subscribe to events. */\nexport class EventBus {\n private readonly subs = new Map<string, Set<(payload: unknown) => void>>();\n private seq = 0;\n\n publish(name: string, payload: Record<string, unknown>): void {\n this.seq++;\n const enriched = { ...payload, seq: this.seq };\n for (const fn of this.subs.get(name) ?? []) fn(enriched);\n for (const fn of this.subs.get(\"*\") ?? []) fn({ event: name, ...enriched });\n }\n\n on(name: string, fn: (payload: unknown) => void): () => void {\n let set = this.subs.get(name);\n if (!set) this.subs.set(name, (set = new Set()));\n set.add(fn);\n return () => set!.delete(fn);\n }\n\n /** Async iterator over an event; ends when `signal` aborts. Buffers between pulls. */\n subscribe<T = unknown>(name: string, signal?: AbortSignal): AsyncIterable<T> {\n const bus = this;\n return {\n [Symbol.asyncIterator]() {\n const queue: T[] = [];\n let waiting: ((r: IteratorResult<T>) => void) | null = null;\n let done = false;\n const off = bus.on(name, (p) => {\n if (done) return;\n if (waiting) {\n const w = waiting;\n waiting = null;\n w({ value: p as T, done: false });\n } else queue.push(p as T);\n });\n const finish = () => {\n if (done) return;\n done = true;\n off();\n if (waiting) {\n const w = waiting;\n waiting = null;\n w({ value: undefined as never, done: true });\n }\n };\n signal?.addEventListener(\"abort\", finish, { once: true });\n if (signal?.aborted) finish();\n return {\n next(): Promise<IteratorResult<T>> {\n if (queue.length) return Promise.resolve({ value: queue.shift()!, done: false });\n if (done) return Promise.resolve({ value: undefined as never, done: true });\n return new Promise((res) => (waiting = res));\n },\n return(): Promise<IteratorResult<T>> {\n finish();\n return Promise.resolve({ value: undefined as never, done: true });\n },\n };\n },\n };\n }\n}\n\nexport interface PolicyHint {\n /** Pushable read policy for the type being loaded, if any (spec 06 §4). */\n filter?: Expr;\n}\n\n/** What every resolver receives. `V` is the server's viewer type. */\nexport interface RayfoldContext<V = unknown> {\n viewer: V;\n signal: AbortSignal;\n simulate: boolean;\n /** Compact wire mode requested by the client (spec 04 section 1). */\n compact?: boolean;\n /** Expected version for a conditional command (spec 03 section 4a). */\n ifVersion?: string | number;\n /**\n * Conditional-write check: throws VersionConflict when the request carried `ifVersion` and it differs\n * from `actual`. `current` is the entity as stored; it is returned to the client in the op's shape.\n */\n checkVersion(key: string, actual: unknown, current: unknown): void;\n events: EventBus;\n meta: RequestMeta;\n opId: number;\n opName: string;\n policy: PolicyHint;\n /** The shape this op asked for, so an adapter can plan a whole screen at once (spec 02). */\n shape?: Shape;\n /** Values for `$name` references in the op's shape. */\n vars?: Record<string, unknown>;\n /** Per-request scratch space (e.g. per-request loader caches). */\n state: Map<string, unknown>;\n /**\n * Scratch space shared by every op of the batch. The executor keeps loaded field values here, so an entity one op\n * already loaded is not loaded again by another op of the same request (spec 03 section 2).\n */\n batch: Map<string, unknown>;\n /** Wall clock, injectable for tests. */\n now: () => number;\n}\n\nexport interface IdempotencyRecord {\n argsHash: string;\n /** the full ok frame */\n frame: unknown;\n /** the same result in compact form, for retries that ask for compact frames */\n compactFrame?: unknown;\n at: number;\n}\n\nexport interface IdempotencyStore {\n get(scope: string, key: string): Promise<IdempotencyRecord | undefined>;\n put(scope: string, key: string, record: IdempotencyRecord): Promise<void>;\n}\n\n/**\n * In-memory idempotency records. Records expire after `ttlMs`; expired ones are dropped on read and swept on every\n * write, and past `maxEntries` the oldest go first, so memory stays bounded however many commands arrive.\n */\nexport class MemoryIdempotencyStore implements IdempotencyStore {\n private readonly map = new Map<string, IdempotencyRecord>();\n constructor(\n private readonly ttlMs = 24 * 3_600_000,\n private readonly now: () => number = Date.now,\n private readonly maxEntries = 100_000,\n ) {}\n async get(scope: string, key: string): Promise<IdempotencyRecord | undefined> {\n const k = `${scope}\\u0000${key}`;\n const r = this.map.get(k);\n if (r && this.now() - r.at > this.ttlMs) {\n this.map.delete(k);\n return undefined;\n }\n return r;\n }\n async put(scope: string, key: string, record: IdempotencyRecord): Promise<void> {\n const k = `${scope}\\u0000${key}`;\n this.map.delete(k); // re-inserted at the end, so the map stays ordered oldest first\n this.map.set(k, record);\n const t = this.now();\n for (const [old, r] of this.map) {\n if (this.map.size > this.maxEntries || t - r.at > this.ttlMs) this.map.delete(old);\n else break;\n }\n }\n get size(): number {\n return this.map.size;\n }\n}\n"]}
package/core.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The runtime without a transport: schema, batches, policies, cost, live queries. Nothing here needs Node, so it runs
3
+ * in a browser, a worker or any JavaScript runtime; the HTTP, WebSocket and MCP transports are in the main entry.
4
+ */
5
+ export * from "./protocol.js";
6
+ export { EventBus, MemoryIdempotencyStore, type IdempotencyStore, type IdempotencyRecord, type RayfoldContext, type PolicyHint } from "./context.js";
7
+ export { Executor, ok, derivePatches, stripTypes, type Resolvers, type FieldResolver, type SingleFieldResolver, type RootResolver, type StreamResolver, type CommandResult } from "./executor.js";
8
+ export { executeBatch, FrameSink, type BatchOptions, type BatchRuntime, type ExecuteOptions } from "./batch.js";
9
+ export { RayfoldServer, createRayfoldServer, type RayfoldServerOptions } from "./server.js";
10
+ export { MemoryShapeRegistry, defaultShape, resolveRequestShape, type ShapeRegistry } from "./views.js";
11
+ export { coerceArgs, coerceValue, coerceScalar, resolveRefs, collectRefs, getPath } from "./args.js";
12
+ export { estimateCost, type CostEstimate } from "./cost.js";
13
+ export { decide, decisionError, pushableFilter, hasPolicy, type Decision } from "./policy.js";
14
+ export { checkWiring } from "./wiring.js";
15
+ export { ChangeBus, changeFromPatch, diffResults, normalizeResult, readSetOf, type Change } from "./live.js";
16
+ export type { Instrumentation, BatchInfo, OpInfo, LoaderInfo, Outcome } from "./instrumentation.js";
17
+ export { MemoryUsage, type UsageSink, type UsageEvent, type UsageEntry } from "./usage.js";
18
+ export { capabilityAllows } from "./capability-scope.js";
package/core.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The runtime without a transport: schema, batches, policies, cost, live queries. Nothing here needs Node, so it runs
3
+ * in a browser, a worker or any JavaScript runtime; the HTTP, WebSocket and MCP transports are in the main entry.
4
+ */
5
+ export * from "./protocol.js";
6
+ export { EventBus, MemoryIdempotencyStore } from "./context.js";
7
+ export { Executor, ok, derivePatches, stripTypes } from "./executor.js";
8
+ export { executeBatch, FrameSink } from "./batch.js";
9
+ export { RayfoldServer, createRayfoldServer } from "./server.js";
10
+ export { MemoryShapeRegistry, defaultShape, resolveRequestShape } from "./views.js";
11
+ export { coerceArgs, coerceValue, coerceScalar, resolveRefs, collectRefs, getPath } from "./args.js";
12
+ export { estimateCost } from "./cost.js";
13
+ export { decide, decisionError, pushableFilter, hasPolicy } from "./policy.js";
14
+ export { checkWiring } from "./wiring.js";
15
+ export { ChangeBus, changeFromPatch, diffResults, normalizeResult, readSetOf } from "./live.js";
16
+ export { MemoryUsage } from "./usage.js";
17
+ export { capabilityAllows } from "./capability-scope.js";
18
+ //# sourceMappingURL=core.js.map
package/core.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,sBAAsB,EAAuF,MAAM,cAAc,CAAC;AACrJ,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,aAAa,EAAE,UAAU,EAA4H,MAAM,eAAe,CAAC;AAClM,OAAO,EAAE,YAAY,EAAE,SAAS,EAA6D,MAAM,YAAY,CAAC;AAChH,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAA6B,MAAM,aAAa,CAAC;AAC5F,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,mBAAmB,EAAsB,MAAM,YAAY,CAAC;AACxG,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACrG,OAAO,EAAE,YAAY,EAAqB,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAiB,MAAM,aAAa,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,eAAe,EAAE,SAAS,EAAe,MAAM,WAAW,CAAC;AAE7G,OAAO,EAAE,WAAW,EAAoD,MAAM,YAAY,CAAC;AAC3F,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC","sourcesContent":["/**\n * The runtime without a transport: schema, batches, policies, cost, live queries. Nothing here needs Node, so it runs\n * in a browser, a worker or any JavaScript runtime; the HTTP, WebSocket and MCP transports are in the main entry.\n */\nexport * from \"./protocol.ts\";\nexport { EventBus, MemoryIdempotencyStore, type IdempotencyStore, type IdempotencyRecord, type RayfoldContext, type PolicyHint } from \"./context.ts\";\nexport { Executor, ok, derivePatches, stripTypes, type Resolvers, type FieldResolver, type SingleFieldResolver, type RootResolver, type StreamResolver, type CommandResult } from \"./executor.ts\";\nexport { executeBatch, FrameSink, type BatchOptions, type BatchRuntime, type ExecuteOptions } from \"./batch.ts\";\nexport { RayfoldServer, createRayfoldServer, type RayfoldServerOptions } from \"./server.ts\";\nexport { MemoryShapeRegistry, defaultShape, resolveRequestShape, type ShapeRegistry } from \"./views.ts\";\nexport { coerceArgs, coerceValue, coerceScalar, resolveRefs, collectRefs, getPath } from \"./args.ts\";\nexport { estimateCost, type CostEstimate } from \"./cost.ts\";\nexport { decide, decisionError, pushableFilter, hasPolicy, type Decision } from \"./policy.ts\";\nexport { checkWiring } from \"./wiring.ts\";\nexport { ChangeBus, changeFromPatch, diffResults, normalizeResult, readSetOf, type Change } from \"./live.ts\";\nexport type { Instrumentation, BatchInfo, OpInfo, LoaderInfo, Outcome } from \"./instrumentation.ts\";\nexport { MemoryUsage, type UsageSink, type UsageEvent, type UsageEntry } from \"./usage.ts\";\nexport { capabilityAllows } from \"./capability-scope.ts\";\n"]}
package/cost.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /** Static cost, depth and field-count estimation. Spec: spec/06 §5, spec/02 §5. */
2
+ import { type OpDef, type RayfoldSchemaIR, type Shape } from "@rayfold/schema";
3
+ export interface CostEstimate {
4
+ cost: number;
5
+ depth: number;
6
+ fields: number;
7
+ }
8
+ /**
9
+ * cost = op.base + op.perItem × first + Σ over selected fields of (field.base + field.perItem × childFirst) × mult
10
+ * where mult is the product of page sizes enclosing the field (fields inside `items` of a Page<T> count `first` times).
11
+ * A field's base defaults to 1 when it returns objects (an entity, an object, a page, a list of them) and to 0 when
12
+ * it returns scalars or enums: those come with the row that is already loaded. The perItem of a page, op or field,
13
+ * defaults to 1, so every row a page can return is charged. `@cost` overrides each default.
14
+ */
15
+ export declare function estimateCost(ir: RayfoldSchemaIR, op: OpDef, args: Record<string, unknown>, shape: Shape, vars?: Record<string, unknown>): CostEstimate;
package/cost.js ADDED
@@ -0,0 +1,110 @@
1
+ /** Static cost, depth and field-count estimation. Spec: spec/06 §5, spec/02 §5. */
2
+ import { annotation, fieldsOf, isPageRef } from "@rayfold/schema";
3
+ import { defaultShape, isScalarLike } from "./views.js";
4
+ const DEFAULT_FIRST = 20;
5
+ /** Largest page the runtime serves; any page size the cost model cannot trust counts as this. */
6
+ const MAX_FIRST = 200;
7
+ /**
8
+ * cost = op.base + op.perItem × first + Σ over selected fields of (field.base + field.perItem × childFirst) × mult
9
+ * where mult is the product of page sizes enclosing the field (fields inside `items` of a Page<T> count `first` times).
10
+ * A field's base defaults to 1 when it returns objects (an entity, an object, a page, a list of them) and to 0 when
11
+ * it returns scalars or enums: those come with the row that is already loaded. The perItem of a page, op or field,
12
+ * defaults to 1, so every row a page can return is charged. `@cost` overrides each default.
13
+ */
14
+ export function estimateCost(ir, op, args, shape, vars = {}) {
15
+ const c = annotation(op, "cost");
16
+ const base = num(c?.args["base"]) ?? 1;
17
+ const perItem = num(c?.args["perItem"]) ?? (isPageRef(op.returns) ? 1 : 0);
18
+ const first = isPageRef(op.returns) ? pageFirst(args, op.args) : 1;
19
+ const acc = { fields: 0, depth: 0 };
20
+ const shapeCost = walk(ir, op.returns, shape, 1, first, 1, acc, vars);
21
+ return { cost: Math.max(1, base + perItem * first + shapeCost), depth: acc.depth, fields: acc.fields };
22
+ }
23
+ /** Fields without a sub-shape that resolve to objects get that type's default view, exactly as the executor does. */
24
+ function walk(ir, t, shape, mult, itemsFirst, depth, acc, vars) {
25
+ acc.depth = Math.max(acc.depth, depth);
26
+ const isPage = isPageRef(t) || (t.kind === "list" && isPageRef(t.of));
27
+ let total = 0;
28
+ const visit = (items, ref) => {
29
+ const fields = fieldsOf(ir, ref) ?? [];
30
+ for (const it of items) {
31
+ switch (it.kind) {
32
+ case "field": {
33
+ acc.fields++;
34
+ const f = fields.find((x) => x.name === it.name);
35
+ const fc = f ? annotation(f, "cost") : undefined;
36
+ const childFirst = f && isPageRef(f.type) ? pageFirst(substituteVars(it.args ?? {}, vars), f.args) : 1;
37
+ const defaultBase = f && isScalarLike(ir, f.type) ? 0 : 1;
38
+ const defaultPerItem = f && isPageRef(f.type) ? 1 : 0;
39
+ total += mult * ((num(fc?.args["base"]) ?? defaultBase) + (num(fc?.args["perItem"]) ?? defaultPerItem) * childFirst);
40
+ if (f && !isScalarLike(ir, f.type)) {
41
+ const sub = it.shape ?? defaultShape(ir, f.type);
42
+ const childMult = isPage && it.name === "items" ? mult * itemsFirst : mult;
43
+ total += walk(ir, f.type, sub, childMult, childFirst, depth + 1, acc, vars);
44
+ }
45
+ break;
46
+ }
47
+ case "spread": {
48
+ const v = ir.views[`${it.type}.${it.view}`];
49
+ if (v)
50
+ visit(v.shape.items, ref);
51
+ break;
52
+ }
53
+ case "on":
54
+ visit(it.shape.items, { kind: "named", name: it.type, nullable: false });
55
+ break;
56
+ case "defer":
57
+ visit(it.shape.items, ref);
58
+ break;
59
+ }
60
+ }
61
+ };
62
+ visit(shape.items, t);
63
+ return total;
64
+ }
65
+ /**
66
+ * Page size for costing. The estimate runs on the request as sent, before coercion, so anything that is not a whole
67
+ * number from 0 to MAX_FIRST (negative, fractional, huge, a $ref, a missing variable) counts as MAX_FIRST:
68
+ * bad input can raise the estimate but never lower it.
69
+ */
70
+ function pageFirst(args, defs) {
71
+ const page = args["page"];
72
+ if (page !== undefined) {
73
+ if (!page || typeof page !== "object" || Array.isArray(page))
74
+ return MAX_FIRST;
75
+ if ("first" in page)
76
+ return pageSize(page["first"]);
77
+ }
78
+ if ("first" in args)
79
+ return pageSize(args["first"]);
80
+ const def = defs.find((a) => a.name === "page")?.default;
81
+ if (def && typeof def === "object" && !Array.isArray(def) && "first" in def)
82
+ return pageSize(def["first"]);
83
+ const firstDef = defs.find((a) => a.name === "first")?.default;
84
+ if (firstDef !== undefined)
85
+ return pageSize(firstDef);
86
+ return DEFAULT_FIRST;
87
+ }
88
+ function pageSize(v) {
89
+ return typeof v === "number" && Number.isInteger(v) && v >= 0 ? Math.min(v, MAX_FIRST) : MAX_FIRST;
90
+ }
91
+ function num(v) {
92
+ return typeof v === "number" ? v : undefined;
93
+ }
94
+ function substituteVars(v, vars) {
95
+ const walkV = (x) => {
96
+ if (x === null || typeof x !== "object")
97
+ return x;
98
+ if (Array.isArray(x))
99
+ return x.map(walkV);
100
+ const o = x;
101
+ if (typeof o["$var"] === "string")
102
+ return vars[o["$var"]];
103
+ const out = {};
104
+ for (const [k, y] of Object.entries(o))
105
+ out[k] = walkV(y);
106
+ return out;
107
+ };
108
+ return walkV(v);
109
+ }
110
+ //# sourceMappingURL=cost.js.map
package/cost.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cost.js","sourceRoot":"","sources":["../src/cost.ts"],"names":[],"mappings":"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","sourcesContent":["/** Static cost, depth and field-count estimation. Spec: spec/06 §5, spec/02 §5. */\nimport { annotation, fieldsOf, isPageRef, type ArgDef, type OpDef, type RayfoldSchemaIR, type Shape, type ShapeItem, type TypeRef } from \"@rayfold/schema\";\nimport { defaultShape, isScalarLike } from \"./views.ts\";\n\nexport interface CostEstimate {\n cost: number;\n depth: number;\n fields: number;\n}\n\nconst DEFAULT_FIRST = 20;\n/** Largest page the runtime serves; any page size the cost model cannot trust counts as this. */\nconst MAX_FIRST = 200;\n\n/**\n * cost = op.base + op.perItem × first + Σ over selected fields of (field.base + field.perItem × childFirst) × mult\n * where mult is the product of page sizes enclosing the field (fields inside `items` of a Page<T> count `first` times).\n * A field's base defaults to 1 when it returns objects (an entity, an object, a page, a list of them) and to 0 when\n * it returns scalars or enums: those come with the row that is already loaded. The perItem of a page, op or field,\n * defaults to 1, so every row a page can return is charged. `@cost` overrides each default.\n */\nexport function estimateCost(ir: RayfoldSchemaIR, op: OpDef, args: Record<string, unknown>, shape: Shape, vars: Record<string, unknown> = {}): CostEstimate {\n const c = annotation(op, \"cost\");\n const base = num(c?.args[\"base\"]) ?? 1;\n const perItem = num(c?.args[\"perItem\"]) ?? (isPageRef(op.returns) ? 1 : 0);\n const first = isPageRef(op.returns) ? pageFirst(args, op.args) : 1;\n const acc = { fields: 0, depth: 0 };\n const shapeCost = walk(ir, op.returns, shape, 1, first, 1, acc, vars);\n return { cost: Math.max(1, base + perItem * first + shapeCost), depth: acc.depth, fields: acc.fields };\n}\n\n/** Fields without a sub-shape that resolve to objects get that type's default view, exactly as the executor does. */\nfunction walk(ir: RayfoldSchemaIR, t: TypeRef, shape: Shape, mult: number, itemsFirst: number, depth: number, acc: { fields: number; depth: number }, vars: Record<string, unknown>): number {\n acc.depth = Math.max(acc.depth, depth);\n const isPage = isPageRef(t) || (t.kind === \"list\" && isPageRef(t.of));\n let total = 0;\n const visit = (items: ShapeItem[], ref: TypeRef): void => {\n const fields = fieldsOf(ir, ref) ?? [];\n for (const it of items) {\n switch (it.kind) {\n case \"field\": {\n acc.fields++;\n const f = fields.find((x) => x.name === it.name);\n const fc = f ? annotation(f, \"cost\") : undefined;\n const childFirst = f && isPageRef(f.type) ? pageFirst(substituteVars(it.args ?? {}, vars), f.args) : 1;\n const defaultBase = f && isScalarLike(ir, f.type) ? 0 : 1;\n const defaultPerItem = f && isPageRef(f.type) ? 1 : 0;\n total += mult * ((num(fc?.args[\"base\"]) ?? defaultBase) + (num(fc?.args[\"perItem\"]) ?? defaultPerItem) * childFirst);\n if (f && !isScalarLike(ir, f.type)) {\n const sub = it.shape ?? defaultShape(ir, f.type);\n const childMult = isPage && it.name === \"items\" ? mult * itemsFirst : mult;\n total += walk(ir, f.type, sub, childMult, childFirst, depth + 1, acc, vars);\n }\n break;\n }\n case \"spread\": {\n const v = ir.views[`${it.type}.${it.view}`];\n if (v) visit(v.shape.items, ref);\n break;\n }\n case \"on\":\n visit(it.shape.items, { kind: \"named\", name: it.type, nullable: false });\n break;\n case \"defer\":\n visit(it.shape.items, ref);\n break;\n }\n }\n };\n visit(shape.items, t);\n return total;\n}\n\n/**\n * Page size for costing. The estimate runs on the request as sent, before coercion, so anything that is not a whole\n * number from 0 to MAX_FIRST (negative, fractional, huge, a $ref, a missing variable) counts as MAX_FIRST:\n * bad input can raise the estimate but never lower it.\n */\nfunction pageFirst(args: Record<string, unknown>, defs: ArgDef[]): number {\n const page = args[\"page\"];\n if (page !== undefined) {\n if (!page || typeof page !== \"object\" || Array.isArray(page)) return MAX_FIRST;\n if (\"first\" in page) return pageSize((page as Record<string, unknown>)[\"first\"]);\n }\n if (\"first\" in args) return pageSize(args[\"first\"]);\n const def = defs.find((a) => a.name === \"page\")?.default;\n if (def && typeof def === \"object\" && !Array.isArray(def) && \"first\" in def) return pageSize((def as Record<string, unknown>)[\"first\"]);\n const firstDef = defs.find((a) => a.name === \"first\")?.default;\n if (firstDef !== undefined) return pageSize(firstDef);\n return DEFAULT_FIRST;\n}\n\nfunction pageSize(v: unknown): number {\n return typeof v === \"number\" && Number.isInteger(v) && v >= 0 ? Math.min(v, MAX_FIRST) : MAX_FIRST;\n}\n\nfunction num(v: unknown): number | undefined {\n return typeof v === \"number\" ? v : undefined;\n}\n\nfunction substituteVars(v: unknown, vars: Record<string, unknown>): Record<string, unknown> {\n const walkV = (x: unknown): unknown => {\n if (x === null || typeof x !== \"object\") return x;\n if (Array.isArray(x)) return x.map(walkV);\n const o = x as Record<string, unknown>;\n if (typeof o[\"$var\"] === \"string\") return vars[o[\"$var\"]];\n const out: Record<string, unknown> = {};\n for (const [k, y] of Object.entries(o)) out[k] = walkV(y);\n return out;\n };\n return walkV(v) as Record<string, unknown>;\n}\n"]}
package/executor.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Operation execution and shape projection.
3
+ * Level-wise batching: every field of every object at one nesting level is resolved with one loader
4
+ * call, across lists and pages, so N+1 cannot occur (spec 01 §4 @load, spec 02).
5
+ */
6
+ import type { Instrumentation } from "./instrumentation.js";
7
+ import { type Annotation, type OpDef, type RayfoldSchemaIR, type Shape } from "@rayfold/schema";
8
+ import type { RayfoldContext } from "./context.js";
9
+ import { type Frame, type PatchOp } from "./protocol.js";
10
+ import type { UsageSink } from "./usage.js";
11
+ export type FieldResolver<P = unknown, A = Record<string, unknown>, R = unknown> = (parents: P[], args: A, ctx: RayfoldContext<never>) => R[] | Promise<R[]>;
12
+ export type SingleFieldResolver<P = unknown, A = Record<string, unknown>, R = unknown> = (parent: P, args: A, ctx: RayfoldContext<never>) => R | Promise<R>;
13
+ export type RootResolver<A = Record<string, unknown>, R = unknown> = (args: A, ctx: RayfoldContext<never>) => R | Promise<R>;
14
+ export type StreamResolver<A = Record<string, unknown>, R = unknown> = (args: A, ctx: RayfoldContext<never>) => AsyncIterable<R>;
15
+ export interface Resolvers {
16
+ Query?: Record<string, RootResolver<never, unknown>>;
17
+ Command?: Record<string, RootResolver<never, unknown>>;
18
+ Stream?: Record<string, StreamResolver<never, unknown>>;
19
+ /** Entity/object field loaders, batch by default: (parents[], args, ctx) => results[] */
20
+ [typeName: string]: Record<string, FieldResolver<never, never, unknown> | SingleFieldResolver<never, never, unknown> | RootResolver<never, unknown> | StreamResolver<never, unknown>> | undefined;
21
+ }
22
+ declare const COMMAND_RESULT: unique symbol;
23
+ export interface CommandResult<R = unknown> {
24
+ [COMMAND_RESULT]: true;
25
+ result: R;
26
+ patch?: PatchOp[];
27
+ emit?: Array<{
28
+ event: string;
29
+ payload: Record<string, unknown>;
30
+ }>;
31
+ }
32
+ /** Wrap a command's return value to attach extra patches or events. */
33
+ export declare function ok<R>(result: R, extra?: {
34
+ patch?: PatchOp[];
35
+ emit?: Array<{
36
+ event: string;
37
+ payload: Record<string, unknown>;
38
+ }>;
39
+ }): CommandResult<R>;
40
+ export interface ExecutorOptions {
41
+ maxDepth: number;
42
+ maxFields: number;
43
+ instrumentation?: Instrumentation;
44
+ /** Records which members each client asked for (spec 11). Nothing is recorded without one. */
45
+ usage?: UsageSink;
46
+ }
47
+ type Emit = (frame: Frame) => void;
48
+ /** Compact mode: drop `$type` except on union members; keep everything else identical. */
49
+ /** Compact form of a query frame (`data` or `at`): `$type` stripped where the schema fixes it, `meta` dropped. */
50
+ export declare function compactQueryFrame(f: Frame): Frame;
51
+ export declare function stripTypes(v: unknown): unknown;
52
+ export declare class Executor {
53
+ private readonly ir;
54
+ private readonly resolvers;
55
+ private readonly opts;
56
+ constructor(ir: RayfoldSchemaIR, resolvers: Resolvers, opts: ExecutorOptions);
57
+ runQuery(op: OpDef, args: Record<string, unknown>, shape: Shape, explicit: boolean, cost: number, ctx: RayfoldContext, emit: Emit): Promise<unknown>;
58
+ runCommand(op: OpDef, args: Record<string, unknown>, shape: Shape, explicit: boolean, cost: number, ctx: RayfoldContext, emit: Emit): Promise<{
59
+ result: unknown;
60
+ frame: Frame;
61
+ full: Frame;
62
+ compact: Frame;
63
+ patch: PatchOp[];
64
+ }>;
65
+ /** VersionConflict -> failed_precondition carrying the current entity in the op's shape. */
66
+ private conflictWithCurrent;
67
+ private checkDeclaredError;
68
+ runStream(op: OpDef, args: Record<string, unknown>, shape: Shape, explicit: boolean, ctx: RayfoldContext, emit: Emit): Promise<void>;
69
+ /** The command's write policy, checked before an idempotent replay is served. */
70
+ authorize(op: OpDef, args: Record<string, unknown>, ctx: RayfoldContext): void;
71
+ private checkOpPolicy;
72
+ private flushDeferred;
73
+ /** Project one value (object, list, or null) of static type `t`. */
74
+ private projectValue;
75
+ /** Project all `slots` (objects of static type `t`) through `shape`, batching each field once. */
76
+ private projectMany;
77
+ /** Resolve field values for all targets with one loader call (or property access). */
78
+ private loadField;
79
+ /** The pushable read policy of the entity a resolver loads, handed to it as ctx.policy.filter (spec 06 section 4). */
80
+ private policyHint;
81
+ /** Expand spreads/type conditions and group field selections by (name, args). */
82
+ private flatten;
83
+ }
84
+ /** Every entity object in a projected result becomes a `set` patch (spec 04 §2). */
85
+ export declare function derivePatches(data: unknown): PatchOp[];
86
+ export declare function annotationsOf(x: {
87
+ annotations: Annotation[];
88
+ }): Annotation[];
89
+ export {};