@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/executor.js ADDED
@@ -0,0 +1,695 @@
1
+ import { annotation, base64urlBytes, baseName, fieldsOf, } from "@rayfold/schema";
2
+ import { coerceArgs } from "./args.js";
3
+ import { decide, decisionError, pushableFilter } from "./policy.js";
4
+ import { RayfoldError, VersionConflict, toWireError } from "./protocol.js";
5
+ import { defaultShape, isScalarLike } from "./views.js";
6
+ const COMMAND_RESULT = Symbol.for("rayfold.commandResult");
7
+ /** Wrap a command's return value to attach extra patches or events. */
8
+ export function ok(result, extra = {}) {
9
+ return { [COMMAND_RESULT]: true, result, ...extra };
10
+ }
11
+ function isCommandResult(v) {
12
+ return !!v && typeof v === "object" && v[COMMAND_RESULT] === true;
13
+ }
14
+ /** Marks output objects whose `$type` is the only way to know their type (union members). */
15
+ const UNION_MEMBER = Symbol.for("rayfold.unionMember");
16
+ /** Compact mode: drop `$type` except on union members; keep everything else identical. */
17
+ /** Compact form of a query frame (`data` or `at`): `$type` stripped where the schema fixes it, `meta` dropped. */
18
+ export function compactQueryFrame(f) {
19
+ if (!("data" in f))
20
+ return f;
21
+ const { meta: _meta, ...rest } = f;
22
+ return { ...rest, data: stripTypes(f.data) };
23
+ }
24
+ export function stripTypes(v) {
25
+ if (v === null || typeof v !== "object")
26
+ return v;
27
+ if (Array.isArray(v))
28
+ return v.map(stripTypes);
29
+ const o = v;
30
+ const out = {};
31
+ for (const [k, x] of Object.entries(o)) {
32
+ if (k === "$type" && !o[UNION_MEMBER])
33
+ continue;
34
+ out[k] = stripTypes(x);
35
+ }
36
+ return out;
37
+ }
38
+ export class Executor {
39
+ ir;
40
+ resolvers;
41
+ opts;
42
+ constructor(ir, resolvers, opts) {
43
+ this.ir = ir;
44
+ this.resolvers = resolvers;
45
+ this.opts = opts;
46
+ }
47
+ // ---------------------------------------------------------------- queries
48
+ async runQuery(op, args, shape, explicit, cost, ctx, emit) {
49
+ this.checkOpPolicy(op, "read", args, ctx);
50
+ ctx.policy = this.policyHint(op.returns);
51
+ const fn = this.resolvers.Query?.[op.name];
52
+ if (!fn)
53
+ throw new RayfoldError("unimplemented", `No resolver for query ${op.name}`);
54
+ const raw = await fn(args, ctx);
55
+ const st = { ctx, errors: [], deferred: [], explicit };
56
+ const data = await this.projectValue(raw, op.returns, shape, "", st);
57
+ const frame = ctx.compact ? { id: ctx.opId, data: stripTypes(data) } : { id: ctx.opId, data, meta: { cost } };
58
+ if (st.errors.length)
59
+ frame.errors = st.errors;
60
+ if (st.deferred.length === 0)
61
+ frame.fin = true;
62
+ emit(frame);
63
+ await this.flushDeferred(st, emit);
64
+ return data;
65
+ }
66
+ // --------------------------------------------------------------- commands
67
+ async runCommand(op, args, shape, explicit, cost, ctx, emit) {
68
+ this.checkOpPolicy(op, "write", args, ctx);
69
+ const fn = this.resolvers.Command?.[op.name];
70
+ if (!fn)
71
+ throw new RayfoldError("unimplemented", `No resolver for command ${op.name}`);
72
+ let raw;
73
+ try {
74
+ raw = await fn(args, ctx);
75
+ }
76
+ catch (e) {
77
+ if (e instanceof VersionConflict)
78
+ throw await this.conflictWithCurrent(op, shape, ctx, e);
79
+ throw this.checkDeclaredError(op, e);
80
+ }
81
+ const cr = isCommandResult(raw) ? raw : ok(raw);
82
+ const st = { ctx, errors: [], deferred: [], explicit };
83
+ const data = await this.projectValue(cr.result, op.returns, shape, "", st);
84
+ // Deferred blocks make no sense on a command result: resolve them inline.
85
+ while (st.deferred.length) {
86
+ const job = st.deferred.shift();
87
+ await this.projectMany(job.slots, job.type, job.shape, st);
88
+ }
89
+ const extra = cr.patch ?? [];
90
+ const patch = derivePatches(data).concat(extra);
91
+ if (!ctx.simulate) {
92
+ for (const ev of cr.emit ?? []) {
93
+ if (!op.emits.includes(ev.event))
94
+ throw new RayfoldError("internal", `${op.name} emitted undeclared event ${ev.event}`);
95
+ ctx.events.publish(ev.event, ev.payload);
96
+ }
97
+ }
98
+ // Compact frames omit `set` patches that restate entities already in `ok`: the client derives them by normalizing `ok`.
99
+ const full = { id: ctx.opId, ok: data, patch, meta: { cost }, fin: true };
100
+ const compact = { id: ctx.opId, ok: stripTypes(data), patch: extra, fin: true };
101
+ if (st.errors.length)
102
+ full.errors = compact.errors = st.errors;
103
+ const frame = ctx.compact ? compact : full;
104
+ emit(frame);
105
+ // Both forms are returned so an idempotent replay can answer in the form the retry asks for.
106
+ return { result: data, frame, full, compact, patch };
107
+ }
108
+ /** VersionConflict -> failed_precondition carrying the current entity in the op's shape. */
109
+ async conflictWithCurrent(op, shape, ctx, e) {
110
+ let current = null;
111
+ if (e.current !== null && e.current !== undefined) {
112
+ const st = { ctx: { ...ctx, compact: false }, errors: [], deferred: [], explicit: false };
113
+ current = await this.projectValue(e.current, { ...op.returns, nullable: true }, shape, "", st);
114
+ while (st.deferred.length) {
115
+ const job = st.deferred.shift();
116
+ await this.projectMany(job.slots, job.type, job.shape, st);
117
+ }
118
+ }
119
+ return new RayfoldError("failed_precondition", e.message, { type: "VersionConflict", data: { key: e.key, expected: e.expected, actual: e.actual, current } });
120
+ }
121
+ checkDeclaredError(op, e) {
122
+ if (e instanceof RayfoldError && e.code === "domain") {
123
+ if (!e.type || !op.throws.includes(e.type)) {
124
+ return new RayfoldError("internal", `${op.name} raised undeclared error ${e.type ?? "?"}`);
125
+ }
126
+ }
127
+ return e;
128
+ }
129
+ // ---------------------------------------------------------------- streams
130
+ async runStream(op, args, shape, explicit, ctx, emit) {
131
+ this.checkOpPolicy(op, "read", args, ctx);
132
+ ctx.policy = this.policyHint(op.returns);
133
+ const fn = this.resolvers.Stream?.[op.name];
134
+ if (!fn)
135
+ throw new RayfoldError("unimplemented", `No resolver for stream ${op.name}`);
136
+ const iterable = fn(args, ctx);
137
+ const iterator = iterable[Symbol.asyncIterator]();
138
+ try {
139
+ for (;;) {
140
+ if (ctx.signal.aborted)
141
+ break;
142
+ const next = await raceAbort(iterator.next(), ctx.signal);
143
+ if (next.done)
144
+ break;
145
+ const st = { ctx, errors: [], deferred: [], explicit };
146
+ const item = await this.projectValue(next.value, op.returns, shape, "", st);
147
+ while (st.deferred.length) {
148
+ const job = st.deferred.shift();
149
+ await this.projectMany(job.slots, job.type, job.shape, st);
150
+ }
151
+ const frame = { id: ctx.opId, item: ctx.compact ? stripTypes(item) : item };
152
+ if (st.errors.length)
153
+ frame.meta = { errors: st.errors };
154
+ emit(frame);
155
+ }
156
+ }
157
+ finally {
158
+ await iterator.return?.();
159
+ }
160
+ if (ctx.signal.aborted)
161
+ throw ctx.signal.reason instanceof RayfoldError ? ctx.signal.reason : new RayfoldError("canceled", "Canceled");
162
+ emit({ id: ctx.opId, fin: true });
163
+ }
164
+ // ------------------------------------------------------------- projection
165
+ /** The command's write policy, checked before an idempotent replay is served. */
166
+ authorize(op, args, ctx) {
167
+ this.checkOpPolicy(op, "write", args, ctx);
168
+ }
169
+ checkOpPolicy(op, mode, args, ctx) {
170
+ const d = decide(op.annotations, mode, { viewer: ctx.viewer, args, this: null, now: ctx.now });
171
+ if (d !== "allow")
172
+ throw decisionError(d, `${op.name}()`);
173
+ }
174
+ async flushDeferred(st, emit) {
175
+ if (!st.deferred.length)
176
+ return;
177
+ while (st.deferred.length) {
178
+ const job = st.deferred.shift();
179
+ const fresh = job.slots.map((s) => ({ value: s.value, out: {}, path: s.path }));
180
+ const sub = { ctx: st.ctx, errors: [], deferred: st.deferred, explicit: job.explicit };
181
+ await this.projectMany(fresh, job.type, job.shape, sub);
182
+ for (const s of fresh) {
183
+ delete s.out["$type"]; // the parent frame carried it; a delta never repeats it
184
+ const f = { id: st.ctx.opId, at: s.path, data: st.ctx.compact ? stripTypes(s.out) : s.out };
185
+ const errs = sub.errors.filter((e) => e.path?.startsWith(s.path));
186
+ if (errs.length)
187
+ f.errors = errs;
188
+ emit(f);
189
+ }
190
+ }
191
+ emit({ id: st.ctx.opId, fin: true });
192
+ }
193
+ /** Project one value (object, list, or null) of static type `t`. */
194
+ async projectValue(value, t, shape, path, st) {
195
+ if (value === null || value === undefined) {
196
+ if (!t.nullable)
197
+ throw new RayfoldError("internal", `Non-null ${path || "result"} resolved to null`, { path });
198
+ return null;
199
+ }
200
+ if (t.kind === "list") {
201
+ if (!Array.isArray(value))
202
+ throw new RayfoldError("internal", `${path || "result"} should be a list`, { path });
203
+ const outs = new Array(value.length);
204
+ const slots = [];
205
+ value.forEach((v, i) => {
206
+ const p = path ? `${path}.${i}` : String(i);
207
+ if (v === null || v === undefined) {
208
+ if (!t.of.nullable)
209
+ throw new RayfoldError("internal", `Non-null ${p} resolved to null`, { path: p });
210
+ outs[i] = null;
211
+ }
212
+ else if (t.of.kind === "list") {
213
+ // nested lists: recurse per element (rare)
214
+ slots.push({ value: { __nested: v }, out: {}, path: p });
215
+ }
216
+ else {
217
+ const s = { value: v, out: {}, path: p, assign: (x) => (outs[i] = x) };
218
+ slots.push(s);
219
+ outs[i] = s.out;
220
+ }
221
+ });
222
+ if (t.of.kind === "list") {
223
+ for (const s of slots)
224
+ outs[Number(s.path.split(".").pop())] = await this.projectValue(s.value["__nested"], t.of, shape, s.path, st);
225
+ }
226
+ else if (isScalarLike(this.ir, t.of)) {
227
+ return value.map((v) => (v === null ? null : serializeScalar(this.ir, t.of, v)));
228
+ }
229
+ else {
230
+ await this.projectMany(slots, t.of, shape, st);
231
+ }
232
+ return outs;
233
+ }
234
+ if (isScalarLike(this.ir, t))
235
+ return serializeScalar(this.ir, t, value);
236
+ if (typeof value !== "object")
237
+ throw new RayfoldError("internal", `${path || "result"} should be an object`, { path });
238
+ const holder = { v: undefined };
239
+ const slot = { value: value, out: {}, path, assign: (v) => (holder.v = v) };
240
+ holder.v = slot.out;
241
+ await this.projectMany([slot], t, shape, st);
242
+ return holder.v;
243
+ }
244
+ /** Project all `slots` (objects of static type `t`) through `shape`, batching each field once. */
245
+ async projectMany(slots, t, shape, st) {
246
+ if (!slots.length)
247
+ return;
248
+ const def = this.ir.types[t.kind === "named" ? t.name : baseName(t)];
249
+ if (!def)
250
+ throw new RayfoldError("internal", `Unknown type ${baseName(t)}`);
251
+ if (def.kind === "union") {
252
+ const groups = new Map();
253
+ for (const s of slots) {
254
+ const tn = s.value["$type"];
255
+ if (typeof tn !== "string" || !def.members.includes(tn)) {
256
+ throw new RayfoldError("internal", `Union ${def.name} value at ${s.path} lacks a valid $type`, { path: s.path });
257
+ }
258
+ s.out["$type"] = tn;
259
+ Object.defineProperty(s.out, UNION_MEMBER, { value: true, enumerable: false });
260
+ (groups.get(tn) ?? groups.set(tn, []).get(tn)).push(s);
261
+ }
262
+ for (const [tn, group] of groups) {
263
+ const memberShape = { items: [] };
264
+ for (const it of shape.items) {
265
+ if (it.kind === "on" && it.type === tn)
266
+ memberShape.items.push(...it.shape.items);
267
+ else if (it.kind === "spread")
268
+ memberShape.items.push(it);
269
+ else if (it.kind === "field")
270
+ memberShape.items.push(it);
271
+ }
272
+ const ref = { kind: "named", name: tn, nullable: false };
273
+ await this.projectMany(group, ref, memberShape.items.length ? memberShape : defaultShape(this.ir, ref), st);
274
+ }
275
+ return;
276
+ }
277
+ // An interface position (spec 01 §2.1): like a union, the concrete type is known only from the value's `$type`,
278
+ // so the slots are grouped by it and projected as that entity. `...on Concrete` then selects fields the interface
279
+ // does not declare, and `$type` survives compact mode because the schema does not fix it here.
280
+ if (def.kind === "object" && def.interface) {
281
+ const members = implementorsOf(this.ir, def.name);
282
+ const groups = new Map();
283
+ for (const s of slots) {
284
+ const tn = s.value["$type"];
285
+ if (typeof tn !== "string" || !members.has(tn)) {
286
+ throw new RayfoldError("internal", `Interface ${def.name} value at ${s.path} lacks a valid $type`, { path: s.path });
287
+ }
288
+ s.out["$type"] = tn;
289
+ Object.defineProperty(s.out, UNION_MEMBER, { value: true, enumerable: false });
290
+ (groups.get(tn) ?? groups.set(tn, []).get(tn)).push(s);
291
+ }
292
+ for (const [tn, group] of groups)
293
+ await this.projectMany(group, { kind: "named", name: tn, nullable: false }, shape, st);
294
+ return;
295
+ }
296
+ if (!("fields" in def))
297
+ throw new RayfoldError("internal", `Cannot project ${def.kind} ${def.name}`);
298
+ // Type-level read policy (spec 06 §2 step 2).
299
+ let allowed = slots;
300
+ if (def.annotations.length) {
301
+ allowed = [];
302
+ for (const s of slots) {
303
+ const d = decide(def.annotations, "read", { viewer: st.ctx.viewer, args: {}, this: s.value, now: st.ctx.now });
304
+ if (d === "allow")
305
+ allowed.push(s);
306
+ // At a nullable position a denied entity reads as null even for an explicit shape, so the answer never tells
307
+ // "exists but forbidden" apart from "does not exist" (spec 06 section 2, spec 12).
308
+ else if (st.explicit && !t.nullable)
309
+ throw decisionError(d, `${def.name} at ${s.path || "result"}`).withPath(s.path);
310
+ else
311
+ markNull(s);
312
+ }
313
+ }
314
+ if (def.kind === "entity")
315
+ for (const s of allowed)
316
+ s.out["$type"] = def.name;
317
+ const fields = fieldsOf(this.ir, t) ?? def.fields;
318
+ const { groups, defers } = this.flatten(shape, def, fields, st);
319
+ // What this client asked for, for `rayfold check --unused` (spec 11). Only the member's path is kept.
320
+ if (this.opts.usage) {
321
+ const client = String(st.ctx.meta.client ?? "");
322
+ const at = st.ctx.now();
323
+ for (const g of groups)
324
+ this.opts.usage.record({ op: st.ctx.opName, path: `${def.name}.${g.field.name}`, client }, at);
325
+ }
326
+ // Phase 1: resolve every field group at this level (batched), collecting children.
327
+ const children = [];
328
+ for (const g of groups) {
329
+ const field = g.field;
330
+ const lazy = annotation(field, "lazy") !== undefined && !g.eager;
331
+ if (lazy) {
332
+ st.deferred.push({ slots: allowed, type: t, shape: { items: [{ ...g.item, eager: true }] }, explicit: st.explicit });
333
+ continue;
334
+ }
335
+ let targets = allowed;
336
+ if (field.annotations.length) {
337
+ targets = [];
338
+ for (const s of allowed) {
339
+ const d = decide(field.annotations, "read", { viewer: st.ctx.viewer, args: g.args, this: s.value, now: st.ctx.now });
340
+ if (d === "allow")
341
+ targets.push(s);
342
+ else if (st.explicit && !g.partial)
343
+ throw decisionError(d, `${def.name}.${field.name}`).withPath(join(s.path, g.alias));
344
+ else if (st.explicit) {
345
+ st.errors.push({ ...decisionError(d, `${def.name}.${field.name}`).toWire(), path: join(s.path, g.alias) });
346
+ s.out[g.alias] = null;
347
+ }
348
+ }
349
+ }
350
+ if (!targets.length)
351
+ continue;
352
+ let values;
353
+ try {
354
+ values = await this.loadField(def, field, targets, g.args, st.ctx);
355
+ }
356
+ catch (e) {
357
+ if (g.partial) {
358
+ const w = toWireError(e);
359
+ for (const s of targets) {
360
+ st.errors.push({ ...w, path: join(s.path, g.alias) });
361
+ s.out[g.alias] = null;
362
+ }
363
+ continue;
364
+ }
365
+ throw e instanceof RayfoldError ? e.withPath(join(targets[0].path, g.alias)) : e;
366
+ }
367
+ if (!Array.isArray(values) || values.length !== targets.length) {
368
+ throw new RayfoldError("internal", `Loader for ${def.name}.${field.name} returned ${Array.isArray(values) ? values.length : "non-array"} for ${targets.length} parents`);
369
+ }
370
+ const scalar = isScalarLike(this.ir, field.type);
371
+ const childSlots = [];
372
+ targets.forEach((s, i) => {
373
+ const v = values[i];
374
+ const p = join(s.path, g.alias);
375
+ if (v === null || v === undefined) {
376
+ if (!field.type.nullable) {
377
+ const err = new RayfoldError("internal", `Non-null field ${def.name}.${field.name} resolved to null`, { path: p });
378
+ if (!g.partial)
379
+ throw err;
380
+ st.errors.push(err.toWire());
381
+ }
382
+ s.out[g.alias] = null;
383
+ return;
384
+ }
385
+ if (scalar) {
386
+ s.out[g.alias] = field.type.kind === "list" ? v.map((x) => (x === null ? null : serializeScalar(this.ir, field.type, x))) : serializeScalar(this.ir, field.type, v);
387
+ return;
388
+ }
389
+ if (field.type.kind === "list") {
390
+ if (!Array.isArray(v))
391
+ throw new RayfoldError("internal", `${p} should be a list`, { path: p });
392
+ const arr = new Array(v.length);
393
+ v.forEach((el, j) => {
394
+ if (el === null || el === undefined) {
395
+ if (!field.type.of.nullable)
396
+ throw new RayfoldError("internal", `Non-null ${p}.${j} resolved to null`, { path: `${p}.${j}` });
397
+ arr[j] = null;
398
+ }
399
+ else {
400
+ const cs = { value: el, out: {}, path: `${p}.${j}`, assign: (x) => (arr[j] = x) };
401
+ childSlots.push(cs);
402
+ arr[j] = cs.out;
403
+ }
404
+ });
405
+ s.out[g.alias] = arr;
406
+ }
407
+ else {
408
+ const cs = { value: v, out: {}, path: p, assign: (x) => (s.out[g.alias] = x) };
409
+ childSlots.push(cs);
410
+ s.out[g.alias] = cs.out;
411
+ }
412
+ });
413
+ if (childSlots.length) {
414
+ const childType = field.type.kind === "list" ? field.type.of : field.type;
415
+ const sub = g.shape ?? defaultShape(this.ir, childType);
416
+ children.push({ slots: childSlots, type: childType, shape: sub, explicit: g.shape ? st.explicit : false });
417
+ }
418
+ }
419
+ // Phase 2: recurse one level deeper, one call per field group (keeps batching across parents).
420
+ for (const c of children) {
421
+ if (c.explicit === st.explicit)
422
+ await this.projectMany(c.slots, c.type, c.shape, st);
423
+ else
424
+ await this.projectMany(c.slots, c.type, c.shape, { ...st, explicit: c.explicit });
425
+ }
426
+ // Deferred blocks run after the enclosing frame is emitted.
427
+ for (const d of defers)
428
+ st.deferred.push({ slots: allowed, type: t, shape: d.shape, explicit: st.explicit });
429
+ }
430
+ /** Resolve field values for all targets with one loader call (or property access). */
431
+ async loadField(def, field, targets, args, ctx) {
432
+ const resolver = this.resolvers[def.name]?.[field.name];
433
+ if (!resolver) {
434
+ if (field.args.length)
435
+ throw new RayfoldError("unimplemented", `No loader for ${def.name}.${field.name}`);
436
+ return targets.map((s) => s.value[field.name]);
437
+ }
438
+ // the loader gets the read policy of what it loads, so it can filter at the source (spec 06 section 4)
439
+ const hinted = { ...ctx, policy: this.policyHint(field.type) };
440
+ const load = annotation(field, "load");
441
+ const single = load && typeof load.args["value"] === "object" && load.args["value"] && "$ident" in load.args["value"] && load.args["value"].$ident === "single";
442
+ // One load per (field, arguments, entity) for the whole batch: an entity another op already loaded, or is
443
+ // loading right now, or that appears twice at this level, is not loaded again. What is remembered is the load
444
+ // in flight, not its result, so ops running at the same time share it. Only entities take part: they have identity.
445
+ const memo = ctx.batch;
446
+ const prefix = `${def.name}.${field.name}|${JSON.stringify(args)}`;
447
+ const keys = targets.map((s) => {
448
+ if (def.kind !== "entity")
449
+ return null;
450
+ const id = s.value["id"];
451
+ return typeof id === "string" || typeof id === "number" ? `${prefix}|${id}` : null;
452
+ });
453
+ const waiting = [];
454
+ const need = [];
455
+ const settlers = [];
456
+ const mine = new Map();
457
+ targets.forEach((s, i) => {
458
+ const k = keys[i];
459
+ const already = k === null ? undefined : memo.get(k);
460
+ if (already) {
461
+ waiting.push(already);
462
+ return;
463
+ }
464
+ if (k !== null && mine.has(k)) {
465
+ waiting.push(waiting[mine.get(k)]);
466
+ return;
467
+ }
468
+ let settle;
469
+ const pending = new Promise((resolve, reject) => (settle = { resolve, reject }));
470
+ // The group's failure is reported by the throw below; this keeps a shared load that nobody awaited quiet.
471
+ pending.catch(() => { });
472
+ waiting.push(pending);
473
+ settlers.push(settle);
474
+ if (k !== null) {
475
+ memo.set(k, pending);
476
+ mine.set(k, i);
477
+ }
478
+ need.push(s);
479
+ });
480
+ if (need.length) {
481
+ const call = async () => {
482
+ if (single) {
483
+ const fn = resolver;
484
+ return Promise.all(need.map((s) => fn(s.value, args, hinted)));
485
+ }
486
+ const fn = resolver;
487
+ return fn(need.map((s) => s.value), args, hinted);
488
+ };
489
+ const hook = this.opts.instrumentation?.loader;
490
+ try {
491
+ const loaded = await (hook ? hook({ type: def.name, field: field.name, parents: need.length }, call) : call());
492
+ if (!Array.isArray(loaded) || loaded.length !== need.length) {
493
+ throw new RayfoldError("internal", `Loader for ${def.name}.${field.name} returned ${Array.isArray(loaded) ? loaded.length : "a non-list"} for ${need.length} parents`);
494
+ }
495
+ loaded.forEach((v, n) => settlers[n].resolve(v));
496
+ }
497
+ catch (e) {
498
+ for (const k of mine.keys())
499
+ memo.delete(k); // a load that failed is not remembered
500
+ for (const s of settlers)
501
+ s.reject(e);
502
+ throw e;
503
+ }
504
+ }
505
+ return Promise.all(waiting);
506
+ }
507
+ /** The pushable read policy of the entity a resolver loads, handed to it as ctx.policy.filter (spec 06 section 4). */
508
+ policyHint(t) {
509
+ const def = this.ir.types[baseName(t)];
510
+ const filter = def && "fields" in def ? pushableFilter(def.annotations) : undefined;
511
+ return filter ? { filter } : {};
512
+ }
513
+ /** Expand spreads/type conditions and group field selections by (name, args). */
514
+ flatten(shape, def, fields, st) {
515
+ const groups = [];
516
+ const defers = [];
517
+ const byAlias = new Map();
518
+ const visit = (items, seen) => {
519
+ for (const it of items) {
520
+ switch (it.kind) {
521
+ case "field": {
522
+ const f = fields.find((x) => x.name === it.name);
523
+ if (!f)
524
+ throw new RayfoldError("invalid_argument", `${def.name} has no field ${it.name}`);
525
+ const alias = it.alias ?? it.name;
526
+ const args = f.args.length ? coerceArgs(this.ir, f.args, substituteVars(it.args ?? {}, st.ctx), `${def.name}.${it.name}`) : {};
527
+ const existing = byAlias.get(alias);
528
+ if (existing) {
529
+ if (existing.field !== f || JSON.stringify(existing.args) !== JSON.stringify(args)) {
530
+ throw new RayfoldError("invalid_argument", `Conflicting selections for ${alias} on ${def.name}`);
531
+ }
532
+ if (it.shape)
533
+ existing.shape = existing.shape ? mergeShapes(existing.shape, it.shape) : it.shape;
534
+ if (it.eager)
535
+ existing.eager = true;
536
+ if (it.partial)
537
+ existing.partial = true;
538
+ continue;
539
+ }
540
+ const g = { field: f, alias, args, item: it, eager: !!it.eager, partial: !!it.partial || annotation(f, "partial") !== undefined };
541
+ if (it.shape)
542
+ g.shape = it.shape;
543
+ byAlias.set(alias, g);
544
+ groups.push(g);
545
+ break;
546
+ }
547
+ case "spread": {
548
+ const key = `${it.type}.${it.view}`;
549
+ const v = this.ir.views[key];
550
+ if (!v)
551
+ throw new RayfoldError("invalid_argument", `Unknown view ${key}`);
552
+ if (seen.has(key))
553
+ throw new RayfoldError("invalid_argument", `View cycle at ${key}`);
554
+ visit(v.shape.items, new Set([...seen, key]));
555
+ break;
556
+ }
557
+ case "on":
558
+ if (it.type === def.name || (def.kind === "entity" && def.implements.includes(it.type)))
559
+ visit(it.shape.items, seen);
560
+ break;
561
+ case "defer":
562
+ defers.push({ shape: it.shape });
563
+ break;
564
+ }
565
+ }
566
+ };
567
+ visit(shape.items, new Set());
568
+ return { groups, defers };
569
+ }
570
+ }
571
+ function mergeShapes(a, b) {
572
+ return { items: [...a.items, ...b.items] };
573
+ }
574
+ function join(path, name) {
575
+ return path ? `${path}.${name}` : name;
576
+ }
577
+ /** Entities that implement an interface, memoised per schema. */
578
+ function implementorsOf(ir, iface) {
579
+ let memo = implementors.get(ir);
580
+ if (!memo)
581
+ implementors.set(ir, (memo = new Map()));
582
+ let set = memo.get(iface);
583
+ if (!set) {
584
+ set = new Set(Object.values(ir.types).filter((t) => t.kind === "entity" && t.implements.includes(iface)).map((t) => t.name));
585
+ memo.set(iface, set);
586
+ }
587
+ return set;
588
+ }
589
+ const implementors = new WeakMap();
590
+ function markNull(s) {
591
+ // A denied entity inside a default view becomes null in its parent (spec 06 §3).
592
+ for (const k of Object.keys(s.out))
593
+ delete s.out[k];
594
+ s.assign?.(null);
595
+ }
596
+ function substituteVars(args, ctx) {
597
+ const vars = ctx.vars ?? {};
598
+ const walk = (v) => {
599
+ if (v === null || typeof v !== "object")
600
+ return v;
601
+ if (Array.isArray(v))
602
+ return v.map((x) => walk(x));
603
+ if ("$var" in v && typeof v["$var"] === "string") {
604
+ const name = v["$var"];
605
+ if (!(name in vars))
606
+ throw new RayfoldError("invalid_argument", `Missing shape variable $${name}`);
607
+ return vars[name];
608
+ }
609
+ const o = {};
610
+ for (const [k, x] of Object.entries(v))
611
+ o[k] = walk(x);
612
+ return o;
613
+ };
614
+ const out = {};
615
+ for (const [k, v] of Object.entries(args))
616
+ out[k] = walk(v);
617
+ return out;
618
+ }
619
+ function serializeScalar(ir, t, v) {
620
+ const name = baseName(t);
621
+ const def = ir.types[name];
622
+ if (def?.kind === "enum")
623
+ return v;
624
+ switch (name) {
625
+ case "Instant":
626
+ return v instanceof Date ? v.toISOString() : v;
627
+ case "Date":
628
+ return v instanceof Date ? v.toISOString().slice(0, 10) : v;
629
+ case "Long":
630
+ return typeof v === "bigint" ? v.toString() : typeof v === "number" && Math.abs(v) > Number.MAX_SAFE_INTEGER ? String(v) : v;
631
+ case "Decimal":
632
+ return typeof v === "number" ? String(v) : v;
633
+ case "Bytes":
634
+ return v instanceof Uint8Array ? base64urlBytes(v) : v;
635
+ default:
636
+ return v;
637
+ }
638
+ }
639
+ /** Every entity object in a projected result becomes a `set` patch (spec 04 §2). */
640
+ export function derivePatches(data) {
641
+ const out = new Map();
642
+ const walk = (v) => {
643
+ if (v === null || typeof v !== "object")
644
+ return;
645
+ if (Array.isArray(v)) {
646
+ v.forEach(walk);
647
+ return;
648
+ }
649
+ const o = v;
650
+ const tn = o["$type"];
651
+ if (typeof tn === "string" && (typeof o["id"] === "string" || typeof o["id"] === "number")) {
652
+ const key = `${tn}:${o["id"]}`;
653
+ const shallow = out.get(key) ?? {};
654
+ for (const [k, x] of Object.entries(o))
655
+ shallow[k] = toRef(x);
656
+ out.set(key, shallow);
657
+ }
658
+ for (const x of Object.values(o))
659
+ walk(x);
660
+ };
661
+ walk(data);
662
+ return [...out.entries()].map(([key, value]) => ({ set: key, value }));
663
+ }
664
+ function toRef(v) {
665
+ if (v === null || typeof v !== "object")
666
+ return v;
667
+ if (Array.isArray(v))
668
+ return v.map(toRef);
669
+ const o = v;
670
+ if (typeof o["$type"] === "string" && o["id"] !== undefined)
671
+ return { $ref: `${o["$type"]}:${o["id"]}` };
672
+ const out = {};
673
+ for (const [k, x] of Object.entries(o))
674
+ out[k] = toRef(x);
675
+ return out;
676
+ }
677
+ function raceAbort(p, signal) {
678
+ if (signal.aborted)
679
+ return Promise.reject(new RayfoldError("canceled", "Canceled"));
680
+ return new Promise((resolve, reject) => {
681
+ const onAbort = () => reject(new RayfoldError("canceled", "Canceled"));
682
+ signal.addEventListener("abort", onAbort, { once: true });
683
+ p.then((v) => {
684
+ signal.removeEventListener("abort", onAbort);
685
+ resolve(v);
686
+ }, (e) => {
687
+ signal.removeEventListener("abort", onAbort);
688
+ reject(e);
689
+ });
690
+ });
691
+ }
692
+ export function annotationsOf(x) {
693
+ return x.annotations;
694
+ }
695
+ //# sourceMappingURL=executor.js.map