@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.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +70 -0
- package/args.d.ts +15 -0
- package/args.js +226 -0
- package/args.js.map +1 -0
- package/batch.d.ts +54 -0
- package/batch.js +483 -0
- package/batch.js.map +1 -0
- package/bindings.d.ts +37 -0
- package/bindings.js +284 -0
- package/bindings.js.map +1 -0
- package/capability-scope.d.ts +8 -0
- package/capability-scope.js +19 -0
- package/capability-scope.js.map +1 -0
- package/capability.d.ts +56 -0
- package/capability.js +112 -0
- package/capability.js.map +1 -0
- package/context.d.ts +74 -0
- package/context.js +109 -0
- package/context.js.map +1 -0
- package/core.d.ts +18 -0
- package/core.js +18 -0
- package/core.js.map +1 -0
- package/cost.d.ts +15 -0
- package/cost.js +110 -0
- package/cost.js.map +1 -0
- package/executor.d.ts +89 -0
- package/executor.js +695 -0
- package/executor.js.map +1 -0
- package/guard.d.ts +33 -0
- package/guard.js +65 -0
- package/guard.js.map +1 -0
- package/http.d.ts +33 -0
- package/http.js +379 -0
- package/http.js.map +1 -0
- package/index.d.ts +8 -0
- package/index.js +9 -0
- package/index.js.map +1 -0
- package/instrumentation.d.ts +36 -0
- package/instrumentation.js +2 -0
- package/instrumentation.js.map +1 -0
- package/live.d.ts +37 -0
- package/live.js +240 -0
- package/live.js.map +1 -0
- package/mcp.d.ts +55 -0
- package/mcp.js +314 -0
- package/mcp.js.map +1 -0
- package/openapi.d.ts +11 -0
- package/openapi.js +124 -0
- package/openapi.js.map +1 -0
- package/package.json +53 -0
- package/policy.d.ts +17 -0
- package/policy.js +64 -0
- package/policy.js.map +1 -0
- package/protocol.d.ts +139 -0
- package/protocol.js +98 -0
- package/protocol.js.map +1 -0
- package/server.d.ts +58 -0
- package/server.js +79 -0
- package/server.js.map +1 -0
- package/usage.d.ts +39 -0
- package/usage.js +44 -0
- package/usage.js.map +1 -0
- package/views.d.ts +33 -0
- package/views.js +108 -0
- package/views.js.map +1 -0
- package/wiring.d.ts +16 -0
- package/wiring.js +57 -0
- package/wiring.js.map +1 -0
- package/ws.d.ts +21 -0
- package/ws.js +209 -0
- package/ws.js.map +1 -0
package/protocol.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const PROTOCOL_CODES = [
|
|
2
|
+
"canceled",
|
|
3
|
+
"unknown",
|
|
4
|
+
"invalid_argument",
|
|
5
|
+
"deadline_exceeded",
|
|
6
|
+
"not_found",
|
|
7
|
+
"already_exists",
|
|
8
|
+
"permission_denied",
|
|
9
|
+
"resource_exhausted",
|
|
10
|
+
"failed_precondition",
|
|
11
|
+
"aborted",
|
|
12
|
+
"out_of_range",
|
|
13
|
+
"unimplemented",
|
|
14
|
+
"internal",
|
|
15
|
+
"unavailable",
|
|
16
|
+
"data_loss",
|
|
17
|
+
"unauthenticated",
|
|
18
|
+
];
|
|
19
|
+
export const HTTP_STATUS = {
|
|
20
|
+
invalid_argument: 400,
|
|
21
|
+
failed_precondition: 400,
|
|
22
|
+
out_of_range: 400,
|
|
23
|
+
unauthenticated: 401,
|
|
24
|
+
permission_denied: 403,
|
|
25
|
+
not_found: 404,
|
|
26
|
+
already_exists: 409,
|
|
27
|
+
aborted: 409,
|
|
28
|
+
resource_exhausted: 429,
|
|
29
|
+
canceled: 499,
|
|
30
|
+
unimplemented: 501,
|
|
31
|
+
unavailable: 503,
|
|
32
|
+
deadline_exceeded: 504,
|
|
33
|
+
domain: 422,
|
|
34
|
+
unknown: 500,
|
|
35
|
+
internal: 500,
|
|
36
|
+
data_loss: 500,
|
|
37
|
+
};
|
|
38
|
+
const RETRYABLE = new Set(["unavailable", "deadline_exceeded", "aborted"]);
|
|
39
|
+
/** Thrown by resolvers / runtime; converted to a WireError at the frame boundary. */
|
|
40
|
+
export class RayfoldError extends Error {
|
|
41
|
+
code;
|
|
42
|
+
type;
|
|
43
|
+
data;
|
|
44
|
+
path;
|
|
45
|
+
retryable;
|
|
46
|
+
constructor(code, message, opts = {}) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "RayfoldError";
|
|
49
|
+
this.code = code;
|
|
50
|
+
this.type = opts.type;
|
|
51
|
+
this.data = opts.data;
|
|
52
|
+
this.path = opts.path;
|
|
53
|
+
this.retryable = opts.retryable ?? RETRYABLE.has(code);
|
|
54
|
+
}
|
|
55
|
+
toWire() {
|
|
56
|
+
const w = { code: this.code, message: this.message };
|
|
57
|
+
if (this.type !== undefined)
|
|
58
|
+
w.type = this.type;
|
|
59
|
+
if (this.data !== undefined)
|
|
60
|
+
w.data = this.data;
|
|
61
|
+
if (this.path !== undefined)
|
|
62
|
+
w.path = this.path;
|
|
63
|
+
if (this.retryable !== RETRYABLE.has(this.code))
|
|
64
|
+
w.retryable = this.retryable;
|
|
65
|
+
return w;
|
|
66
|
+
}
|
|
67
|
+
withPath(path) {
|
|
68
|
+
return new RayfoldError(this.code, this.message, { type: this.type, data: this.data, path, retryable: this.retryable });
|
|
69
|
+
}
|
|
70
|
+
static domain(type, data, message) {
|
|
71
|
+
return new RayfoldError("domain", message ?? type, { type, data });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Raised through `ctx.checkVersion` when a conditional command's `ifVersion` does not match.
|
|
76
|
+
* The executor projects `current` through the op's shape so the client can repair its cache without a GET.
|
|
77
|
+
*/
|
|
78
|
+
export class VersionConflict extends RayfoldError {
|
|
79
|
+
key;
|
|
80
|
+
expected;
|
|
81
|
+
actual;
|
|
82
|
+
current;
|
|
83
|
+
constructor(key, expected, actual, current) {
|
|
84
|
+
super("failed_precondition", `${key} is at version ${String(actual)}, not ${String(expected)}`, { type: "VersionConflict", data: { key, expected, actual } });
|
|
85
|
+
this.key = key;
|
|
86
|
+
this.expected = expected;
|
|
87
|
+
this.actual = actual;
|
|
88
|
+
this.current = current;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function toWireError(e) {
|
|
92
|
+
if (e instanceof RayfoldError)
|
|
93
|
+
return e.toWire();
|
|
94
|
+
if (e instanceof Error && e.name === "AbortError")
|
|
95
|
+
return { code: "canceled", message: "Canceled" };
|
|
96
|
+
return { code: "internal", message: "Internal error" };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=protocol.js.map
|
package/protocol.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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","sourcesContent":["/** Wire-level request/frame types. Spec: spec/03, spec/04, spec/05. */\nimport type { JsonValue } from \"@rayfold/schema\";\n\nexport interface RequestOp {\n id: number;\n op: string;\n args?: Record<string, unknown>;\n shape?: string;\n vars?: Record<string, JsonValue>;\n key?: string;\n live?: boolean;\n deadline?: number;\n simulate?: boolean;\n /** Omit `$type` where the schema makes it redundant and omit `meta` unless it carries `replay`. Requires a schema-aware client. */\n compact?: boolean;\n /** Conditional write: the version the client last saw of the entity this command targets (spec 03 section 4a). */\n ifVersion?: string | number;\n}\n\nexport interface RequestMeta {\n client?: string;\n deadline?: number;\n /** W3C trace context (spec 04 section 4); the HTTP transport copies it from the headers of the same names. */\n traceparent?: string;\n tracestate?: string;\n}\n\nexport interface RequestEnvelope {\n rayfold?: string;\n ops: RequestOp[];\n meta?: RequestMeta;\n}\n\nexport const PROTOCOL_CODES = [\n \"canceled\",\n \"unknown\",\n \"invalid_argument\",\n \"deadline_exceeded\",\n \"not_found\",\n \"already_exists\",\n \"permission_denied\",\n \"resource_exhausted\",\n \"failed_precondition\",\n \"aborted\",\n \"out_of_range\",\n \"unimplemented\",\n \"internal\",\n \"unavailable\",\n \"data_loss\",\n \"unauthenticated\",\n] as const;\nexport type ProtocolCode = (typeof PROTOCOL_CODES)[number];\nexport type ErrorCode = ProtocolCode | \"domain\";\n\nexport interface WireError {\n code: ErrorCode;\n type?: string;\n message: string;\n data?: unknown;\n path?: string;\n retryable?: boolean;\n}\n\nexport type PatchOp =\n | { set: string; value: Record<string, unknown> }\n | { del: string }\n | { inv: string[] }\n | { invOp: string[] }\n /** Result-scoped (spec 04 section 2b): merge these fields into the plain object at this result path. */\n | { at: string; value: Record<string, unknown> }\n /** Result-scoped: remove these old positions, then insert these elements at these new positions. */\n | { list: string; del?: number[]; ins?: Array<{ at: number; value: unknown }> };\n\nexport interface FrameMeta {\n cost?: number;\n cache?: \"hit\" | \"miss\" | \"stale\";\n ms?: number;\n replay?: boolean;\n cursor?: string;\n [k: string]: unknown;\n}\n\nexport type Frame =\n | { id: number; data: unknown; meta?: FrameMeta; errors?: WireError[]; fin?: boolean }\n | { id: number; ok: unknown; patch?: PatchOp[]; meta?: FrameMeta; errors?: WireError[]; fin: true }\n | { id: number; item: unknown; meta?: FrameMeta }\n | { id: number; patch: PatchOp[]; meta?: FrameMeta }\n | { id: number; at: string; data: unknown; errors?: WireError[] }\n | { id: number; error: WireError; fin: true }\n | { id: number; fin: true }\n | { error: WireError; fin: true };\n\nexport const HTTP_STATUS: Record<ErrorCode, number> = {\n invalid_argument: 400,\n failed_precondition: 400,\n out_of_range: 400,\n unauthenticated: 401,\n permission_denied: 403,\n not_found: 404,\n already_exists: 409,\n aborted: 409,\n resource_exhausted: 429,\n canceled: 499,\n unimplemented: 501,\n unavailable: 503,\n deadline_exceeded: 504,\n domain: 422,\n unknown: 500,\n internal: 500,\n data_loss: 500,\n};\n\nconst RETRYABLE = new Set<ErrorCode>([\"unavailable\", \"deadline_exceeded\", \"aborted\"]);\n\n/** Thrown by resolvers / runtime; converted to a WireError at the frame boundary. */\nexport class RayfoldError extends Error {\n readonly code: ErrorCode;\n readonly type: string | undefined;\n readonly data: unknown;\n readonly path: string | undefined;\n readonly retryable: boolean;\n\n constructor(code: ErrorCode, message: string, opts: { type?: string | undefined; data?: unknown; path?: string | undefined; retryable?: boolean | undefined } = {}) {\n super(message);\n this.name = \"RayfoldError\";\n this.code = code;\n this.type = opts.type;\n this.data = opts.data;\n this.path = opts.path;\n this.retryable = opts.retryable ?? RETRYABLE.has(code);\n }\n\n toWire(): WireError {\n const w: WireError = { code: this.code, message: this.message };\n if (this.type !== undefined) w.type = this.type;\n if (this.data !== undefined) w.data = this.data;\n if (this.path !== undefined) w.path = this.path;\n if (this.retryable !== RETRYABLE.has(this.code)) w.retryable = this.retryable;\n return w;\n }\n\n withPath(path: string): RayfoldError {\n return new RayfoldError(this.code, this.message, { type: this.type, data: this.data, path, retryable: this.retryable });\n }\n\n static domain(type: string, data: unknown, message?: string): RayfoldError {\n return new RayfoldError(\"domain\", message ?? type, { type, data });\n }\n}\n\n/**\n * Raised through `ctx.checkVersion` when a conditional command's `ifVersion` does not match.\n * The executor projects `current` through the op's shape so the client can repair its cache without a GET.\n */\nexport class VersionConflict extends RayfoldError {\n constructor(\n readonly key: string,\n readonly expected: string | number,\n readonly actual: unknown,\n readonly current: unknown,\n ) {\n super(\"failed_precondition\", `${key} is at version ${String(actual)}, not ${String(expected)}`, { type: \"VersionConflict\", data: { key, expected, actual } });\n }\n}\n\nexport function toWireError(e: unknown): WireError {\n if (e instanceof RayfoldError) return e.toWire();\n if (e instanceof Error && e.name === \"AbortError\") return { code: \"canceled\", message: \"Canceled\" };\n return { code: \"internal\", message: \"Internal error\" };\n}\n"]}
|
package/server.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Instrumentation } from "./instrumentation.js";
|
|
2
|
+
import { type LoadedSchema, type RayfoldSchemaIR, type Shape } from "@rayfold/schema";
|
|
3
|
+
import { type BatchOptions, type ExecuteOptions } from "./batch.js";
|
|
4
|
+
import { EventBus, type IdempotencyStore } from "./context.js";
|
|
5
|
+
import { type Resolvers } from "./executor.js";
|
|
6
|
+
import type { Frame, RequestEnvelope } from "./protocol.js";
|
|
7
|
+
import { type ShapeRegistry } from "./views.js";
|
|
8
|
+
import { ChangeBus } from "./live.js";
|
|
9
|
+
import type { UsageSink } from "./usage.js";
|
|
10
|
+
export interface RayfoldServerOptions {
|
|
11
|
+
/** `.rayfold` text, a loaded schema, or a raw IR. */
|
|
12
|
+
schema: string | LoadedSchema | RayfoldSchemaIR;
|
|
13
|
+
resolvers: Resolvers;
|
|
14
|
+
/** Production mode: only registered shape ids are accepted (spec 02 §3). Default false. */
|
|
15
|
+
trustedShapes?: boolean;
|
|
16
|
+
/** Cost budget per batch (spec 06 §5). Default 1000. */
|
|
17
|
+
budget?: number;
|
|
18
|
+
maxOps?: number;
|
|
19
|
+
maxDepth?: number;
|
|
20
|
+
maxFields?: number;
|
|
21
|
+
/** Add `meta.ms` to frames. Default false (keeps frames deterministic). */
|
|
22
|
+
timing?: boolean;
|
|
23
|
+
idempotency?: IdempotencyStore;
|
|
24
|
+
shapes?: ShapeRegistry;
|
|
25
|
+
events?: EventBus;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
/** Hooks around batches, ops and loaders, for tracing and metrics (`@rayfold/otel` makes them OpenTelemetry spans). */
|
|
28
|
+
instrumentation?: Instrumentation;
|
|
29
|
+
/** Where to record which members each client asks for (spec 11). Without one, nothing is recorded. */
|
|
30
|
+
usage?: UsageSink;
|
|
31
|
+
}
|
|
32
|
+
export declare class RayfoldServer {
|
|
33
|
+
readonly ir: RayfoldSchemaIR;
|
|
34
|
+
readonly hash: string;
|
|
35
|
+
readonly events: EventBus;
|
|
36
|
+
/** Entity/op change notifications driving live queries (extension `live`). */
|
|
37
|
+
readonly changes: ChangeBus;
|
|
38
|
+
/** Field-usage telemetry, when the server was given a sink (spec 11). */
|
|
39
|
+
readonly usage: UsageSink | undefined;
|
|
40
|
+
readonly shapes: ShapeRegistry;
|
|
41
|
+
readonly options: BatchOptions;
|
|
42
|
+
private readonly rt;
|
|
43
|
+
constructor(opts: RayfoldServerOptions);
|
|
44
|
+
/** Execute a batch; frames arrive as they are produced. */
|
|
45
|
+
execute(envelope: RequestEnvelope, opts?: ExecuteOptions): AsyncIterable<Frame>;
|
|
46
|
+
/** Convenience: run a batch and collect every frame. */
|
|
47
|
+
collect(envelope: RequestEnvelope, opts?: ExecuteOptions): Promise<Frame[]>;
|
|
48
|
+
/** Register a shape (text or AST) so it can be referenced by id in trusted mode. */
|
|
49
|
+
registerShape(shape: string | Shape): string;
|
|
50
|
+
/** Discovery document (spec 10 outline). */
|
|
51
|
+
manifest(): {
|
|
52
|
+
rayfold: string;
|
|
53
|
+
schemaHash: string;
|
|
54
|
+
extensions: string[];
|
|
55
|
+
limits: Omit<BatchOptions, "now">;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export declare function createRayfoldServer(opts: RayfoldServerOptions): RayfoldServer;
|
package/server.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { loadSchema, schemaHash } from "@rayfold/schema";
|
|
2
|
+
import { executeBatch } from "./batch.js";
|
|
3
|
+
import { EventBus, MemoryIdempotencyStore } from "./context.js";
|
|
4
|
+
import { Executor } from "./executor.js";
|
|
5
|
+
import { MemoryShapeRegistry } from "./views.js";
|
|
6
|
+
import { ChangeBus } from "./live.js";
|
|
7
|
+
import { parseShapeText } from "@rayfold/schema";
|
|
8
|
+
export class RayfoldServer {
|
|
9
|
+
ir;
|
|
10
|
+
hash;
|
|
11
|
+
events;
|
|
12
|
+
/** Entity/op change notifications driving live queries (extension `live`). */
|
|
13
|
+
changes = new ChangeBus();
|
|
14
|
+
/** Field-usage telemetry, when the server was given a sink (spec 11). */
|
|
15
|
+
usage;
|
|
16
|
+
shapes;
|
|
17
|
+
options;
|
|
18
|
+
rt;
|
|
19
|
+
constructor(opts) {
|
|
20
|
+
this.ir = typeof opts.schema === "string" ? loadSchema(opts.schema).ir : "ir" in opts.schema ? opts.schema.ir : opts.schema;
|
|
21
|
+
this.hash = schemaHash(this.ir);
|
|
22
|
+
this.events = opts.events ?? new EventBus();
|
|
23
|
+
this.usage = opts.usage;
|
|
24
|
+
this.shapes = opts.shapes ?? new MemoryShapeRegistry(this.ir);
|
|
25
|
+
this.options = {
|
|
26
|
+
trustedShapes: opts.trustedShapes ?? false,
|
|
27
|
+
budget: opts.budget ?? 1000,
|
|
28
|
+
maxOps: opts.maxOps ?? 50,
|
|
29
|
+
maxDepth: opts.maxDepth ?? 8,
|
|
30
|
+
maxFields: opts.maxFields ?? 500,
|
|
31
|
+
timing: opts.timing ?? false,
|
|
32
|
+
now: opts.now ?? Date.now,
|
|
33
|
+
};
|
|
34
|
+
this.rt = {
|
|
35
|
+
ir: this.ir,
|
|
36
|
+
executor: new Executor(this.ir, opts.resolvers, {
|
|
37
|
+
maxDepth: this.options.maxDepth,
|
|
38
|
+
maxFields: this.options.maxFields,
|
|
39
|
+
...(opts.instrumentation ? { instrumentation: opts.instrumentation } : {}),
|
|
40
|
+
...(opts.usage ? { usage: opts.usage } : {}),
|
|
41
|
+
}),
|
|
42
|
+
registry: this.shapes,
|
|
43
|
+
idempotency: opts.idempotency ?? new MemoryIdempotencyStore(undefined, this.options.now),
|
|
44
|
+
events: this.events,
|
|
45
|
+
changes: this.changes,
|
|
46
|
+
options: this.options,
|
|
47
|
+
inflight: new Map(),
|
|
48
|
+
...(opts.usage ? { usage: opts.usage } : {}),
|
|
49
|
+
...(opts.instrumentation ? { instrumentation: opts.instrumentation } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Execute a batch; frames arrive as they are produced. */
|
|
53
|
+
execute(envelope, opts = {}) {
|
|
54
|
+
return executeBatch(this.rt, envelope, opts);
|
|
55
|
+
}
|
|
56
|
+
/** Convenience: run a batch and collect every frame. */
|
|
57
|
+
async collect(envelope, opts = {}) {
|
|
58
|
+
const out = [];
|
|
59
|
+
for await (const f of this.execute(envelope, opts))
|
|
60
|
+
out.push(f);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
/** Register a shape (text or AST) so it can be referenced by id in trusted mode. */
|
|
64
|
+
registerShape(shape) {
|
|
65
|
+
return this.shapes.register(typeof shape === "string" ? parseShapeText(shape) : shape, true);
|
|
66
|
+
}
|
|
67
|
+
/** Discovery document (spec 10 outline). */
|
|
68
|
+
manifest() {
|
|
69
|
+
const { now: _now, ...limits } = this.options;
|
|
70
|
+
const extensions = ["live", "rb"];
|
|
71
|
+
if (Object.values(this.ir.ops).some((o) => o.annotations.some((a) => a.name === "http")))
|
|
72
|
+
extensions.push("http");
|
|
73
|
+
return { rayfold: "0.1", schemaHash: this.hash, extensions, limits };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function createRayfoldServer(opts) {
|
|
77
|
+
return new RayfoldServer(opts);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=server.js.map
|
package/server.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Instrumentation } from \"./instrumentation.ts\";\nimport { loadSchema, schemaHash, type LoadedSchema, type RayfoldSchemaIR, type Shape } from \"@rayfold/schema\";\nimport { executeBatch, type BatchOptions, type BatchRuntime, type ExecuteOptions } from \"./batch.ts\";\nimport { EventBus, MemoryIdempotencyStore, type IdempotencyStore } from \"./context.ts\";\nimport { Executor, type Resolvers } from \"./executor.ts\";\nimport type { Frame, RequestEnvelope } from \"./protocol.ts\";\nimport { MemoryShapeRegistry, type ShapeRegistry } from \"./views.ts\";\nimport { ChangeBus } from \"./live.ts\";\nimport type { UsageSink } from \"./usage.ts\";\nimport { parseShapeText } from \"@rayfold/schema\";\n\nexport interface RayfoldServerOptions {\n /** `.rayfold` text, a loaded schema, or a raw IR. */\n schema: string | LoadedSchema | RayfoldSchemaIR;\n resolvers: Resolvers;\n /** Production mode: only registered shape ids are accepted (spec 02 §3). Default false. */\n trustedShapes?: boolean;\n /** Cost budget per batch (spec 06 §5). Default 1000. */\n budget?: number;\n maxOps?: number;\n maxDepth?: number;\n maxFields?: number;\n /** Add `meta.ms` to frames. Default false (keeps frames deterministic). */\n timing?: boolean;\n idempotency?: IdempotencyStore;\n shapes?: ShapeRegistry;\n events?: EventBus;\n now?: () => number;\n /** Hooks around batches, ops and loaders, for tracing and metrics (`@rayfold/otel` makes them OpenTelemetry spans). */\n instrumentation?: Instrumentation;\n /** Where to record which members each client asks for (spec 11). Without one, nothing is recorded. */\n usage?: UsageSink;\n}\n\nexport class RayfoldServer {\n readonly ir: RayfoldSchemaIR;\n readonly hash: string;\n readonly events: EventBus;\n /** Entity/op change notifications driving live queries (extension `live`). */\n readonly changes = new ChangeBus();\n /** Field-usage telemetry, when the server was given a sink (spec 11). */\n readonly usage: UsageSink | undefined;\n readonly shapes: ShapeRegistry;\n readonly options: BatchOptions;\n private readonly rt: BatchRuntime;\n\n constructor(opts: RayfoldServerOptions) {\n this.ir = typeof opts.schema === \"string\" ? loadSchema(opts.schema).ir : \"ir\" in opts.schema ? opts.schema.ir : opts.schema;\n this.hash = schemaHash(this.ir);\n this.events = opts.events ?? new EventBus();\n this.usage = opts.usage;\n this.shapes = opts.shapes ?? new MemoryShapeRegistry(this.ir);\n this.options = {\n trustedShapes: opts.trustedShapes ?? false,\n budget: opts.budget ?? 1000,\n maxOps: opts.maxOps ?? 50,\n maxDepth: opts.maxDepth ?? 8,\n maxFields: opts.maxFields ?? 500,\n timing: opts.timing ?? false,\n now: opts.now ?? Date.now,\n };\n this.rt = {\n ir: this.ir,\n executor: new Executor(this.ir, opts.resolvers, {\n maxDepth: this.options.maxDepth,\n maxFields: this.options.maxFields,\n ...(opts.instrumentation ? { instrumentation: opts.instrumentation } : {}),\n ...(opts.usage ? { usage: opts.usage } : {}),\n }),\n registry: this.shapes,\n idempotency: opts.idempotency ?? new MemoryIdempotencyStore(undefined, this.options.now),\n events: this.events,\n changes: this.changes,\n options: this.options,\n inflight: new Map(),\n ...(opts.usage ? { usage: opts.usage } : {}),\n ...(opts.instrumentation ? { instrumentation: opts.instrumentation } : {}),\n };\n }\n\n /** Execute a batch; frames arrive as they are produced. */\n execute(envelope: RequestEnvelope, opts: ExecuteOptions = {}): AsyncIterable<Frame> {\n return executeBatch(this.rt, envelope, opts);\n }\n\n /** Convenience: run a batch and collect every frame. */\n async collect(envelope: RequestEnvelope, opts: ExecuteOptions = {}): Promise<Frame[]> {\n const out: Frame[] = [];\n for await (const f of this.execute(envelope, opts)) out.push(f);\n return out;\n }\n\n /** Register a shape (text or AST) so it can be referenced by id in trusted mode. */\n registerShape(shape: string | Shape): string {\n return this.shapes.register(typeof shape === \"string\" ? parseShapeText(shape) : shape, true);\n }\n\n /** Discovery document (spec 10 outline). */\n manifest(): { rayfold: string; schemaHash: string; extensions: string[]; limits: Omit<BatchOptions, \"now\"> } {\n const { now: _now, ...limits } = this.options;\n const extensions = [\"live\", \"rb\"];\n if (Object.values(this.ir.ops).some((o) => o.annotations.some((a) => a.name === \"http\"))) extensions.push(\"http\");\n return { rayfold: \"0.1\", schemaHash: this.hash, extensions, limits };\n }\n}\n\nexport function createRayfoldServer(opts: RayfoldServerOptions): RayfoldServer {\n return new RayfoldServer(opts);\n}\n"]}
|
package/usage.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-usage telemetry (spec 11 "Field usage telemetry"): which members each client still asks for, so removing one
|
|
3
|
+
* is a fact rather than a guess. `rayfold check --unused` reads a snapshot of this and lists what no client has
|
|
4
|
+
* touched.
|
|
5
|
+
*
|
|
6
|
+
* A server records nothing unless it is given a sink. A sink keeps only what the question needs: the operation, the
|
|
7
|
+
* path of the member (`Book.author`, or empty for the operation itself), the client name from `Rayfold-Client`, when
|
|
8
|
+
* it was last seen and how often. No arguments, no values, no viewer.
|
|
9
|
+
*/
|
|
10
|
+
export interface UsageEvent {
|
|
11
|
+
/** the operation the request named */
|
|
12
|
+
op: string;
|
|
13
|
+
/** `Type.field`, or "" for the operation itself */
|
|
14
|
+
path: string;
|
|
15
|
+
/** the `Rayfold-Client` header, or "" when the caller did not name itself */
|
|
16
|
+
client: string;
|
|
17
|
+
}
|
|
18
|
+
export interface UsageSink {
|
|
19
|
+
record(event: UsageEvent, at: number): void;
|
|
20
|
+
}
|
|
21
|
+
export interface UsageEntry extends UsageEvent {
|
|
22
|
+
/** RFC 3339, UTC */
|
|
23
|
+
lastSeen: string;
|
|
24
|
+
count: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Usage in memory, for a single process. Real deployments hand the same events to whatever they already run for
|
|
28
|
+
* metrics; this is what `rayfold dev`, the tests and a small server use.
|
|
29
|
+
*/
|
|
30
|
+
export declare class MemoryUsage implements UsageSink {
|
|
31
|
+
private readonly max;
|
|
32
|
+
private readonly seen;
|
|
33
|
+
/** A full sink stops recording rather than growing without bound: telemetry must not be a way to exhaust memory. */
|
|
34
|
+
constructor(max?: number);
|
|
35
|
+
record(event: UsageEvent, at: number): void;
|
|
36
|
+
/** Everything recorded, oldest member first within an operation: the file `rayfold check --unused` reads. */
|
|
37
|
+
snapshot(): UsageEntry[];
|
|
38
|
+
get size(): number;
|
|
39
|
+
}
|
package/usage.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-usage telemetry (spec 11 "Field usage telemetry"): which members each client still asks for, so removing one
|
|
3
|
+
* is a fact rather than a guess. `rayfold check --unused` reads a snapshot of this and lists what no client has
|
|
4
|
+
* touched.
|
|
5
|
+
*
|
|
6
|
+
* A server records nothing unless it is given a sink. A sink keeps only what the question needs: the operation, the
|
|
7
|
+
* path of the member (`Book.author`, or empty for the operation itself), the client name from `Rayfold-Client`, when
|
|
8
|
+
* it was last seen and how often. No arguments, no values, no viewer.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Usage in memory, for a single process. Real deployments hand the same events to whatever they already run for
|
|
12
|
+
* metrics; this is what `rayfold dev`, the tests and a small server use.
|
|
13
|
+
*/
|
|
14
|
+
export class MemoryUsage {
|
|
15
|
+
max;
|
|
16
|
+
seen = new Map();
|
|
17
|
+
/** A full sink stops recording rather than growing without bound: telemetry must not be a way to exhaust memory. */
|
|
18
|
+
constructor(max = 100_000) {
|
|
19
|
+
this.max = max;
|
|
20
|
+
}
|
|
21
|
+
record(event, at) {
|
|
22
|
+
const key = `${event.client}|${event.op}|${event.path}`;
|
|
23
|
+
const seen = this.seen.get(key);
|
|
24
|
+
if (seen) {
|
|
25
|
+
if (at > seen.at)
|
|
26
|
+
seen.at = at;
|
|
27
|
+
seen.count++;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (this.seen.size >= this.max)
|
|
31
|
+
return;
|
|
32
|
+
this.seen.set(key, { event, at, count: 1 });
|
|
33
|
+
}
|
|
34
|
+
/** Everything recorded, oldest member first within an operation: the file `rayfold check --unused` reads. */
|
|
35
|
+
snapshot() {
|
|
36
|
+
return [...this.seen.values()]
|
|
37
|
+
.map(({ event, at, count }) => ({ ...event, lastSeen: new Date(at).toISOString(), count }))
|
|
38
|
+
.sort((a, b) => (a.op === b.op ? (a.path === b.path ? a.client.localeCompare(b.client) : a.path.localeCompare(b.path)) : a.op.localeCompare(b.op)));
|
|
39
|
+
}
|
|
40
|
+
get size() {
|
|
41
|
+
return this.seen.size;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=usage.js.map
|
package/usage.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqBH;;;GAGG;AACH,MAAM,OAAO,WAAW;IAIO,GAAG;IAHf,IAAI,GAAG,IAAI,GAAG,EAA4D,CAAC;IAE5F,oHAAoH;IACpH,YAA6B,GAAG,GAAG,OAAO;mBAAb,GAAG;IAAa,CAAC;IAE9C,MAAM,CAAC,KAAiB,EAAE,EAAU;QAClC,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACxD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,EAAE,GAAG,IAAI,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;YAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG;YAAE,OAAO;QACvC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,6GAA6G;IAC7G,QAAQ;QACN,OAAO,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;aAC3B,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;aAC1F,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxJ,CAAC;IAED,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;CACF","sourcesContent":["/**\n * Field-usage telemetry (spec 11 \"Field usage telemetry\"): which members each client still asks for, so removing one\n * is a fact rather than a guess. `rayfold check --unused` reads a snapshot of this and lists what no client has\n * touched.\n *\n * A server records nothing unless it is given a sink. A sink keeps only what the question needs: the operation, the\n * path of the member (`Book.author`, or empty for the operation itself), the client name from `Rayfold-Client`, when\n * it was last seen and how often. No arguments, no values, no viewer.\n */\n\nexport interface UsageEvent {\n /** the operation the request named */\n op: string;\n /** `Type.field`, or \"\" for the operation itself */\n path: string;\n /** the `Rayfold-Client` header, or \"\" when the caller did not name itself */\n client: string;\n}\n\nexport interface UsageSink {\n record(event: UsageEvent, at: number): void;\n}\n\nexport interface UsageEntry extends UsageEvent {\n /** RFC 3339, UTC */\n lastSeen: string;\n count: number;\n}\n\n/**\n * Usage in memory, for a single process. Real deployments hand the same events to whatever they already run for\n * metrics; this is what `rayfold dev`, the tests and a small server use.\n */\nexport class MemoryUsage implements UsageSink {\n private readonly seen = new Map<string, { event: UsageEvent; at: number; count: number }>();\n\n /** A full sink stops recording rather than growing without bound: telemetry must not be a way to exhaust memory. */\n constructor(private readonly max = 100_000) {}\n\n record(event: UsageEvent, at: number): void {\n const key = `${event.client}|${event.op}|${event.path}`;\n const seen = this.seen.get(key);\n if (seen) {\n if (at > seen.at) seen.at = at;\n seen.count++;\n return;\n }\n if (this.seen.size >= this.max) return;\n this.seen.set(key, { event, at, count: 1 });\n }\n\n /** Everything recorded, oldest member first within an operation: the file `rayfold check --unused` reads. */\n snapshot(): UsageEntry[] {\n return [...this.seen.values()]\n .map(({ event, at, count }) => ({ ...event, lastSeen: new Date(at).toISOString(), count }))\n .sort((a, b) => (a.op === b.op ? (a.path === b.path ? a.client.localeCompare(b.client) : a.path.localeCompare(b.path)) : a.op.localeCompare(b.op)));\n }\n\n get size(): number {\n return this.seen.size;\n }\n}\n"]}
|
package/views.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Default views and shape helpers. Spec: spec/01 §2.7, spec/02 §2. */
|
|
2
|
+
import { type FieldDef, type RayfoldSchemaIR, type Shape, type TypeRef } from "@rayfold/schema";
|
|
3
|
+
export declare function isScalarLike(ir: RayfoldSchemaIR, t: TypeRef): boolean;
|
|
4
|
+
export declare function isEntity(ir: RayfoldSchemaIR, t: TypeRef): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Default view of a type: the `Type.default` view if declared, else every scalar/enum field
|
|
7
|
+
* (nested entities and objects are omitted). Memoised per IR.
|
|
8
|
+
*/
|
|
9
|
+
export declare function defaultShape(ir: RayfoldSchemaIR, t: TypeRef): Shape;
|
|
10
|
+
export interface ShapeRegistry {
|
|
11
|
+
/** Resolve a `sha256:` id to a parsed shape; undefined if unknown. */
|
|
12
|
+
get(id: string): Shape | undefined;
|
|
13
|
+
/** Register an inline shape (dev mode) and return its id. */
|
|
14
|
+
/** Pinned shapes (registered by the server) are never evicted; shapes learned from requests may be. */
|
|
15
|
+
register(shape: Shape, pin?: boolean): string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Shapes by id. Shapes the server registers are pinned. Shapes learned from requests are kept up to `max`, least
|
|
19
|
+
* recently used out first, so clients cannot grow the server's memory without bound.
|
|
20
|
+
*/
|
|
21
|
+
export declare class MemoryShapeRegistry implements ShapeRegistry {
|
|
22
|
+
private readonly ir;
|
|
23
|
+
private readonly max;
|
|
24
|
+
private readonly pinned;
|
|
25
|
+
private readonly learned;
|
|
26
|
+
constructor(ir: RayfoldSchemaIR, max?: number);
|
|
27
|
+
get(id: string): Shape | undefined;
|
|
28
|
+
register(shape: Shape, pin?: boolean): string;
|
|
29
|
+
get size(): number;
|
|
30
|
+
}
|
|
31
|
+
/** Turn the request's `shape` string into a Shape, honouring trusted-shapes mode. */
|
|
32
|
+
export declare function resolveRequestShape(ir: RayfoldSchemaIR, shapeText: string | undefined, returns: TypeRef, registry: ShapeRegistry, trustedOnly: boolean): Shape;
|
|
33
|
+
export declare function findField(fields: FieldDef[], name: string): FieldDef | undefined;
|
package/views.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** Default views and shape helpers. Spec: spec/01 §2.7, spec/02 §2. */
|
|
2
|
+
import { baseName, canonicalShape, fieldsOf, isShapeId, parseShapeText, shapeIdOf, } from "@rayfold/schema";
|
|
3
|
+
import { RayfoldError } from "./protocol.js";
|
|
4
|
+
const SCALAR_LIKE = new Set(["scalar", "enum"]);
|
|
5
|
+
export function isScalarLike(ir, t) {
|
|
6
|
+
const d = ir.types[baseName(t)];
|
|
7
|
+
return !!d && SCALAR_LIKE.has(d.kind);
|
|
8
|
+
}
|
|
9
|
+
export function isEntity(ir, t) {
|
|
10
|
+
return ir.types[baseName(t)]?.kind === "entity";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Default view of a type: the `Type.default` view if declared, else every scalar/enum field
|
|
14
|
+
* (nested entities and objects are omitted). Memoised per IR.
|
|
15
|
+
*/
|
|
16
|
+
export function defaultShape(ir, t) {
|
|
17
|
+
const name = t.kind === "list" ? baseNameThroughLists(t) : t.name;
|
|
18
|
+
const key = `${name}.default`;
|
|
19
|
+
const declared = ir.views[key];
|
|
20
|
+
if (declared)
|
|
21
|
+
return declared.shape;
|
|
22
|
+
let memo = defaultMemo.get(ir);
|
|
23
|
+
if (!memo)
|
|
24
|
+
defaultMemo.set(ir, (memo = new Map()));
|
|
25
|
+
const cached = memo.get(name);
|
|
26
|
+
if (cached)
|
|
27
|
+
return cached;
|
|
28
|
+
const fields = fieldsOf(ir, t) ?? [];
|
|
29
|
+
const shape = {
|
|
30
|
+
items: fields
|
|
31
|
+
.filter((f) => f.args.length === 0 && (isScalarLike(ir, f.type) || (name === "Page" && f.name === "items")))
|
|
32
|
+
.map((f) => ({ kind: "field", name: f.name })),
|
|
33
|
+
};
|
|
34
|
+
memo.set(name, shape);
|
|
35
|
+
return shape;
|
|
36
|
+
}
|
|
37
|
+
const defaultMemo = new WeakMap();
|
|
38
|
+
function baseNameThroughLists(t) {
|
|
39
|
+
return t.kind === "list" ? baseNameThroughLists(t.of) : t.name;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Shapes by id. Shapes the server registers are pinned. Shapes learned from requests are kept up to `max`, least
|
|
43
|
+
* recently used out first, so clients cannot grow the server's memory without bound.
|
|
44
|
+
*/
|
|
45
|
+
export class MemoryShapeRegistry {
|
|
46
|
+
ir;
|
|
47
|
+
max;
|
|
48
|
+
pinned = new Map();
|
|
49
|
+
learned = new Map();
|
|
50
|
+
constructor(ir, max = 10_000) {
|
|
51
|
+
this.ir = ir;
|
|
52
|
+
this.max = max;
|
|
53
|
+
}
|
|
54
|
+
get(id) {
|
|
55
|
+
const p = this.pinned.get(id);
|
|
56
|
+
if (p)
|
|
57
|
+
return p;
|
|
58
|
+
const s = this.learned.get(id);
|
|
59
|
+
if (s) {
|
|
60
|
+
this.learned.delete(id);
|
|
61
|
+
this.learned.set(id, s);
|
|
62
|
+
}
|
|
63
|
+
return s;
|
|
64
|
+
}
|
|
65
|
+
register(shape, pin = false) {
|
|
66
|
+
const id = shapeIdOf(canonicalShape(shape, (t, v) => this.ir.views[`${t}.${v}`]));
|
|
67
|
+
if (pin) {
|
|
68
|
+
this.pinned.set(id, shape);
|
|
69
|
+
this.learned.delete(id);
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
if (this.pinned.has(id))
|
|
73
|
+
return id;
|
|
74
|
+
this.learned.delete(id);
|
|
75
|
+
this.learned.set(id, shape);
|
|
76
|
+
if (this.learned.size > this.max)
|
|
77
|
+
this.learned.delete(this.learned.keys().next().value);
|
|
78
|
+
return id;
|
|
79
|
+
}
|
|
80
|
+
get size() {
|
|
81
|
+
return this.pinned.size + this.learned.size;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Turn the request's `shape` string into a Shape, honouring trusted-shapes mode. */
|
|
85
|
+
export function resolveRequestShape(ir, shapeText, returns, registry, trustedOnly) {
|
|
86
|
+
if (shapeText === undefined)
|
|
87
|
+
return defaultShape(ir, returns);
|
|
88
|
+
if (isShapeId(shapeText)) {
|
|
89
|
+
const s = registry.get(shapeText);
|
|
90
|
+
if (!s)
|
|
91
|
+
throw new RayfoldError("not_found", `Unknown shape ${shapeText}`);
|
|
92
|
+
return s;
|
|
93
|
+
}
|
|
94
|
+
if (trustedOnly)
|
|
95
|
+
throw new RayfoldError("permission_denied", "Only registered shapes are accepted");
|
|
96
|
+
let parsed;
|
|
97
|
+
try {
|
|
98
|
+
parsed = parseShapeText(shapeText);
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
throw new RayfoldError("invalid_argument", `Bad shape: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
return parsed; // registered by the batch planner once depth, field and cost checks pass
|
|
104
|
+
}
|
|
105
|
+
export function findField(fields, name) {
|
|
106
|
+
return fields.find((f) => f.name === name);
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=views.js.map
|
package/views.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"views.js","sourceRoot":"","sources":["../src/views.ts"],"names":[],"mappings":"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","sourcesContent":["/** Default views and shape helpers. Spec: spec/01 §2.7, spec/02 §2. */\nimport {\n baseName,\n canonicalShape,\n fieldsOf,\n isShapeId,\n parseShapeText,\n shapeIdOf,\n type FieldDef,\n type RayfoldSchemaIR,\n type Shape,\n type TypeRef,\n} from \"@rayfold/schema\";\nimport { RayfoldError } from \"./protocol.ts\";\n\nconst SCALAR_LIKE = new Set([\"scalar\", \"enum\"]);\n\nexport function isScalarLike(ir: RayfoldSchemaIR, t: TypeRef): boolean {\n const d = ir.types[baseName(t)];\n return !!d && SCALAR_LIKE.has(d.kind);\n}\n\nexport function isEntity(ir: RayfoldSchemaIR, t: TypeRef): boolean {\n return ir.types[baseName(t)]?.kind === \"entity\";\n}\n\n/**\n * Default view of a type: the `Type.default` view if declared, else every scalar/enum field\n * (nested entities and objects are omitted). Memoised per IR.\n */\nexport function defaultShape(ir: RayfoldSchemaIR, t: TypeRef): Shape {\n const name = t.kind === \"list\" ? baseNameThroughLists(t) : t.name;\n const key = `${name}.default`;\n const declared = ir.views[key];\n if (declared) return declared.shape;\n let memo = defaultMemo.get(ir);\n if (!memo) defaultMemo.set(ir, (memo = new Map()));\n const cached = memo.get(name);\n if (cached) return cached;\n const fields = fieldsOf(ir, t) ?? [];\n const shape: Shape = {\n items: fields\n .filter((f) => f.args.length === 0 && (isScalarLike(ir, f.type) || (name === \"Page\" && f.name === \"items\")))\n .map((f) => ({ kind: \"field\", name: f.name })),\n };\n memo.set(name, shape);\n return shape;\n}\nconst defaultMemo = new WeakMap<RayfoldSchemaIR, Map<string, Shape>>();\n\nfunction baseNameThroughLists(t: TypeRef): string {\n return t.kind === \"list\" ? baseNameThroughLists(t.of) : t.name;\n}\n\nexport interface ShapeRegistry {\n /** Resolve a `sha256:` id to a parsed shape; undefined if unknown. */\n get(id: string): Shape | undefined;\n /** Register an inline shape (dev mode) and return its id. */\n /** Pinned shapes (registered by the server) are never evicted; shapes learned from requests may be. */\n register(shape: Shape, pin?: boolean): string;\n}\n\n/**\n * Shapes by id. Shapes the server registers are pinned. Shapes learned from requests are kept up to `max`, least\n * recently used out first, so clients cannot grow the server's memory without bound.\n */\nexport class MemoryShapeRegistry implements ShapeRegistry {\n private readonly pinned = new Map<string, Shape>();\n private readonly learned = new Map<string, Shape>();\n constructor(\n private readonly ir: RayfoldSchemaIR,\n private readonly max = 10_000,\n ) {}\n get(id: string): Shape | undefined {\n const p = this.pinned.get(id);\n if (p) return p;\n const s = this.learned.get(id);\n if (s) {\n this.learned.delete(id);\n this.learned.set(id, s);\n }\n return s;\n }\n register(shape: Shape, pin = false): string {\n const id = shapeIdOf(canonicalShape(shape, (t, v) => this.ir.views[`${t}.${v}`]));\n if (pin) {\n this.pinned.set(id, shape);\n this.learned.delete(id);\n return id;\n }\n if (this.pinned.has(id)) return id;\n this.learned.delete(id);\n this.learned.set(id, shape);\n if (this.learned.size > this.max) this.learned.delete(this.learned.keys().next().value!);\n return id;\n }\n get size(): number {\n return this.pinned.size + this.learned.size;\n }\n}\n\n/** Turn the request's `shape` string into a Shape, honouring trusted-shapes mode. */\nexport function resolveRequestShape(\n ir: RayfoldSchemaIR,\n shapeText: string | undefined,\n returns: TypeRef,\n registry: ShapeRegistry,\n trustedOnly: boolean,\n): Shape {\n if (shapeText === undefined) return defaultShape(ir, returns);\n if (isShapeId(shapeText)) {\n const s = registry.get(shapeText);\n if (!s) throw new RayfoldError(\"not_found\", `Unknown shape ${shapeText}`);\n return s;\n }\n if (trustedOnly) throw new RayfoldError(\"permission_denied\", \"Only registered shapes are accepted\");\n let parsed: Shape;\n try {\n parsed = parseShapeText(shapeText);\n } catch (e) {\n throw new RayfoldError(\"invalid_argument\", `Bad shape: ${(e as Error).message}`);\n }\n return parsed; // registered by the batch planner once depth, field and cost checks pass\n}\n\nexport function findField(fields: FieldDef[], name: string): FieldDef | undefined {\n return fields.find((f) => f.name === name);\n}\n"]}
|
package/wiring.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Do the resolvers cover the schema?
|
|
3
|
+
*
|
|
4
|
+
* The runtime answers that one call at a time: an operation with no resolver fails with `unimplemented` when someone
|
|
5
|
+
* calls it, and a field that takes arguments fails the same way when a shape asks for it. Both are found in
|
|
6
|
+
* production, by a user. This finds them at build time instead, and also finds the opposite - a resolver the schema
|
|
7
|
+
* has no place for, which is what a rename leaves behind and what nothing reports at all.
|
|
8
|
+
*
|
|
9
|
+
* const findings = checkWiring(schema.ir, resolvers);
|
|
10
|
+
* expect(findings).toEqual([]); // in your own test suite
|
|
11
|
+
*
|
|
12
|
+
* `rayfold check schema.rayfold --resolvers ./src/resolvers.ts` runs the same check from the command line.
|
|
13
|
+
*/
|
|
14
|
+
import type { Diagnostic, RayfoldSchemaIR } from "@rayfold/schema";
|
|
15
|
+
import type { Resolvers } from "./executor.js";
|
|
16
|
+
export declare function checkWiring(ir: RayfoldSchemaIR, resolvers: Resolvers): Diagnostic[];
|
package/wiring.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const ROOT = { query: "Query", command: "Command", stream: "Stream" };
|
|
2
|
+
const ROOTS = new Set(["Query", "Command", "Stream"]);
|
|
3
|
+
export function checkWiring(ir, resolvers) {
|
|
4
|
+
const out = [];
|
|
5
|
+
const err = (code, at, message) => void out.push({ severity: "error", code, at, message });
|
|
6
|
+
const warn = (code, at, message) => void out.push({ severity: "warning", code, at, message });
|
|
7
|
+
const entries = resolvers;
|
|
8
|
+
// what the schema declares, and the runtime would refuse
|
|
9
|
+
for (const op of Object.values(ir.ops)) {
|
|
10
|
+
if (!entries[ROOT[op.kind]]?.[op.name]) {
|
|
11
|
+
err("missing-resolver", `${op.name}()`, `No ${op.kind} resolver: every call fails with unimplemented. Add ${ROOT[op.kind]}.${op.name}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
for (const type of Object.values(ir.types)) {
|
|
15
|
+
if (type.builtin || !("fields" in type))
|
|
16
|
+
continue;
|
|
17
|
+
for (const field of type.fields) {
|
|
18
|
+
// a field with no arguments is read off the parent when there is no loader, which is a resolver shape of its own
|
|
19
|
+
if (!field.args.length)
|
|
20
|
+
continue;
|
|
21
|
+
if (!entries[type.name]?.[field.name]) {
|
|
22
|
+
err("missing-loader", `${type.name}.${field.name}`, `The field takes arguments, so it needs a loader: any shape asking for it fails with unimplemented`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// what the resolvers wire that the schema no longer has: a rename leaves this behind and nothing says so
|
|
27
|
+
for (const [key, members] of Object.entries(entries)) {
|
|
28
|
+
if (!members)
|
|
29
|
+
continue;
|
|
30
|
+
if (ROOTS.has(key)) {
|
|
31
|
+
const kind = key === "Query" ? "query" : key === "Command" ? "command" : "stream";
|
|
32
|
+
for (const name of Object.keys(members)) {
|
|
33
|
+
const op = ir.ops[name];
|
|
34
|
+
if (!op)
|
|
35
|
+
warn("unknown-resolver", `${name}()`, `${key}.${name} is wired, but the schema has no such operation`);
|
|
36
|
+
else if (op.kind !== kind)
|
|
37
|
+
warn("unknown-resolver", `${name}()`, `${name} is a ${op.kind} in the schema, but it is wired under ${key}`);
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const type = ir.types[key];
|
|
42
|
+
if (!type || type.builtin) {
|
|
43
|
+
warn("unknown-resolver", key, `Loaders are wired for ${key}, but the schema has no such type`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!("fields" in type)) {
|
|
47
|
+
warn("unknown-resolver", key, `Loaders are wired for ${key}, but a ${type.kind} has no fields to load`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const name of Object.keys(members)) {
|
|
51
|
+
if (!type.fields.some((f) => f.name === name))
|
|
52
|
+
warn("unknown-resolver", `${key}.${name}`, `${key}.${name} is wired, but the schema has no such field`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=wiring.js.map
|
package/wiring.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wiring.js","sourceRoot":"","sources":["../src/wiring.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Do the resolvers cover the schema?\n *\n * The runtime answers that one call at a time: an operation with no resolver fails with `unimplemented` when someone\n * calls it, and a field that takes arguments fails the same way when a shape asks for it. Both are found in\n * production, by a user. This finds them at build time instead, and also finds the opposite - a resolver the schema\n * has no place for, which is what a rename leaves behind and what nothing reports at all.\n *\n * const findings = checkWiring(schema.ir, resolvers);\n * expect(findings).toEqual([]); // in your own test suite\n *\n * `rayfold check schema.rayfold --resolvers ./src/resolvers.ts` runs the same check from the command line.\n */\nimport type { Diagnostic, OpKind, RayfoldSchemaIR } from \"@rayfold/schema\";\nimport type { Resolvers } from \"./executor.ts\";\n\nconst ROOT: Record<OpKind, \"Query\" | \"Command\" | \"Stream\"> = { query: \"Query\", command: \"Command\", stream: \"Stream\" };\nconst ROOTS = new Set([\"Query\", \"Command\", \"Stream\"]);\n\nexport function checkWiring(ir: RayfoldSchemaIR, resolvers: Resolvers): Diagnostic[] {\n const out: Diagnostic[] = [];\n const err = (code: string, at: string, message: string): void => void out.push({ severity: \"error\", code, at, message });\n const warn = (code: string, at: string, message: string): void => void out.push({ severity: \"warning\", code, at, message });\n const entries = resolvers as Record<string, Record<string, unknown> | undefined>;\n\n // what the schema declares, and the runtime would refuse\n for (const op of Object.values(ir.ops)) {\n if (!entries[ROOT[op.kind]]?.[op.name]) {\n err(\"missing-resolver\", `${op.name}()`, `No ${op.kind} resolver: every call fails with unimplemented. Add ${ROOT[op.kind]}.${op.name}`);\n }\n }\n for (const type of Object.values(ir.types)) {\n if (type.builtin || !(\"fields\" in type)) continue;\n for (const field of type.fields) {\n // a field with no arguments is read off the parent when there is no loader, which is a resolver shape of its own\n if (!field.args.length) continue;\n if (!entries[type.name]?.[field.name]) {\n err(\"missing-loader\", `${type.name}.${field.name}`, `The field takes arguments, so it needs a loader: any shape asking for it fails with unimplemented`);\n }\n }\n }\n\n // what the resolvers wire that the schema no longer has: a rename leaves this behind and nothing says so\n for (const [key, members] of Object.entries(entries)) {\n if (!members) continue;\n if (ROOTS.has(key)) {\n const kind = key === \"Query\" ? \"query\" : key === \"Command\" ? \"command\" : \"stream\";\n for (const name of Object.keys(members)) {\n const op = ir.ops[name];\n if (!op) warn(\"unknown-resolver\", `${name}()`, `${key}.${name} is wired, but the schema has no such operation`);\n else if (op.kind !== kind) warn(\"unknown-resolver\", `${name}()`, `${name} is a ${op.kind} in the schema, but it is wired under ${key}`);\n }\n continue;\n }\n const type = ir.types[key];\n if (!type || type.builtin) {\n warn(\"unknown-resolver\", key, `Loaders are wired for ${key}, but the schema has no such type`);\n continue;\n }\n if (!(\"fields\" in type)) {\n warn(\"unknown-resolver\", key, `Loaders are wired for ${key}, but a ${type.kind} has no fields to load`);\n continue;\n }\n for (const name of Object.keys(members)) {\n if (!type.fields.some((f) => f.name === name)) warn(\"unknown-resolver\", `${key}.${name}`, `${key}.${name} is wired, but the schema has no such field`);\n }\n }\n\n return out;\n}\n"]}
|
package/ws.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IncomingMessage, Server } from "node:http";
|
|
2
|
+
import type { RayfoldServer } from "./server.js";
|
|
3
|
+
import { type Frame } from "./protocol.js";
|
|
4
|
+
import { type OriginOptions } from "./guard.js";
|
|
5
|
+
export declare const SUBPROTOCOL = "rayfold.0.1";
|
|
6
|
+
export interface WsOptions extends OriginOptions {
|
|
7
|
+
/** Largest frame or assembled message accepted, in bytes. Default 1 MiB. */
|
|
8
|
+
maxMessage?: number;
|
|
9
|
+
path?: string;
|
|
10
|
+
viewer?: (req: IncomingMessage) => unknown | Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** Attach the Rayfold WebSocket endpoint to a Node HTTP server. */
|
|
13
|
+
export declare function attachWebSocket(http: Server, server: RayfoldServer, opts?: WsOptions): void;
|
|
14
|
+
export declare function encodeFrame(payload: Buffer, opcode: number): Buffer;
|
|
15
|
+
export declare function decodeFrame(buf: Buffer): {
|
|
16
|
+
opcode: number;
|
|
17
|
+
fin: boolean;
|
|
18
|
+
payload: Buffer;
|
|
19
|
+
length: number;
|
|
20
|
+
} | null;
|
|
21
|
+
export type { Frame };
|