@volter/world-core 3.0.153 → 3.0.155
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/dist/generated/pack-facts.json +12 -3
- package/dist/src/client-bundle.js +15 -15
- package/dist/src/event-delivery.d.ts +4 -0
- package/dist/src/event-delivery.js +70 -0
- package/dist/src/events.d.ts +5 -4
- package/dist/src/events.js +23 -8
- package/dist/src/graphql-wire.d.ts +5 -0
- package/dist/src/graphql-wire.js +6 -6
- package/dist/src/pack-fetch.d.ts +2 -1
- package/dist/src/pack-fetch.js +1 -1
- package/generated/pack-facts.json +12 -3
- package/package.json +1 -1
- package/src/client-bundle.ts +13 -14
- package/src/event-delivery.ts +76 -0
- package/src/events.ts +28 -12
- package/src/graphql-wire.ts +9 -6
- package/src/pack-fetch.ts +3 -3
|
@@ -270,6 +270,9 @@
|
|
|
270
270
|
{
|
|
271
271
|
"host": "api.clerk.com"
|
|
272
272
|
},
|
|
273
|
+
{
|
|
274
|
+
"host": "dashboard.clerk.com"
|
|
275
|
+
},
|
|
273
276
|
{
|
|
274
277
|
"suffix": ".clerk.accounts.dev"
|
|
275
278
|
},
|
|
@@ -1309,8 +1312,7 @@
|
|
|
1309
1312
|
"host": "api.linear.app"
|
|
1310
1313
|
},
|
|
1311
1314
|
{
|
|
1312
|
-
"host": "linear.app"
|
|
1313
|
-
"pathPattern": "^/oauth/"
|
|
1315
|
+
"host": "linear.app"
|
|
1314
1316
|
}
|
|
1315
1317
|
],
|
|
1316
1318
|
"archetype": "crud",
|
|
@@ -1330,7 +1332,7 @@
|
|
|
1330
1332
|
"comment",
|
|
1331
1333
|
"issue_label"
|
|
1332
1334
|
],
|
|
1333
|
-
"endpointEnvNone": "Linear uses api.linear.app/graphql and linear.app
|
|
1335
|
+
"endpointEnvNone": "Linear uses api.linear.app/graphql and linear.app for its workspace and OAuth authorize page; the World claims those hosts",
|
|
1334
1336
|
"credentialDoor": {
|
|
1335
1337
|
"path": "/_twin/local-credentials",
|
|
1336
1338
|
"body": {},
|
|
@@ -2023,6 +2025,10 @@
|
|
|
2023
2025
|
},
|
|
2024
2026
|
{
|
|
2025
2027
|
"host": "inbound-cdn.resend.com"
|
|
2028
|
+
},
|
|
2029
|
+
{
|
|
2030
|
+
"host": "resend.com",
|
|
2031
|
+
"pathPattern": "^/emails(?:/|$)"
|
|
2026
2032
|
}
|
|
2027
2033
|
],
|
|
2028
2034
|
"archetype": "crud",
|
|
@@ -2179,6 +2185,9 @@
|
|
|
2179
2185
|
]
|
|
2180
2186
|
},
|
|
2181
2187
|
"hosts": [
|
|
2188
|
+
{
|
|
2189
|
+
"host": "app.slack.com"
|
|
2190
|
+
},
|
|
2182
2191
|
{
|
|
2183
2192
|
"host": "slack.com"
|
|
2184
2193
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// A page's browser client from its `client/<name>.tsx` entry (the console's, the platform's), runtime-neutral (the
|
|
2
|
-
// serve seam's helpers, docs/contributing/architecture.md#the-serve-seam):
|
|
3
|
-
// by scripts/publish/build.mjs into dist/client/.
|
|
4
|
-
//
|
|
2
|
+
// serve seam's helpers, docs/contributing/architecture.md#the-serve-seam): serve an adjacent `<name>.bundle.js`
|
|
3
|
+
// under either runtime, written at pack time by scripts/publish/build.mjs into dist/client/. Without one, Bun
|
|
4
|
+
// builds the source at serve time and Node refuses it. Kept once per process; a failed attempt is retried on
|
|
5
|
+
// the next call. Nothing here runs at module scope.
|
|
5
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
7
|
/** A file URL's path on this machine, without `node:url` (the modules that call it ride into browser
|
|
7
8
|
* bundles): percent-escapes decoded, and without the slash `URL.pathname` puts before a Windows drive
|
|
@@ -18,19 +19,18 @@ const PROCESS_SHIM = 'globalThis.process=globalThis.process||{env:{NODE_ENV:"pro
|
|
|
18
19
|
export function bundleClient(entry) {
|
|
19
20
|
let pending = bundles.get(entry);
|
|
20
21
|
if (!pending) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!result.success)
|
|
25
|
-
throw new Error(result.logs.map((l) => l.message).join('\n') || `client build failed: ${entry}`);
|
|
26
|
-
return PROCESS_SHIM + await result.outputs[0].text();
|
|
27
|
-
})
|
|
28
|
-
: Promise.resolve().then(() => {
|
|
29
|
-
const prebuilt = entry.replace(/\.tsx?$/, '.bundle.js');
|
|
30
|
-
if (!existsSync(prebuilt))
|
|
31
|
-
throw new Error(`client bundle missing at ${prebuilt} — the package's \`build\` writes it (scripts/publish/build.mjs); Node serves the prebuilt client`);
|
|
22
|
+
pending = Promise.resolve().then(async () => {
|
|
23
|
+
const prebuilt = entry.replace(/\.tsx?$/, '.bundle.js');
|
|
24
|
+
if (existsSync(prebuilt))
|
|
32
25
|
return PROCESS_SHIM + readFileSync(prebuilt, 'utf8');
|
|
33
|
-
|
|
26
|
+
const bun = globalThis.Bun;
|
|
27
|
+
if (!bun || typeof bun.build !== 'function')
|
|
28
|
+
throw new Error(`client bundle missing at ${prebuilt} — the package's \`build\` writes it (scripts/publish/build.mjs); Node serves the prebuilt client`);
|
|
29
|
+
const result = await bun.build({ entrypoints: [entry], target: 'browser', minify: true });
|
|
30
|
+
if (!result.success)
|
|
31
|
+
throw new Error(result.logs.map((l) => l.message).join('\n') || `client build failed: ${entry}`);
|
|
32
|
+
return PROCESS_SHIM + await result.outputs[0].text();
|
|
33
|
+
})
|
|
34
34
|
.catch((error) => { bundles.delete(entry); throw error; });
|
|
35
35
|
bundles.set(entry, pending);
|
|
36
36
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { EventTransport } from './events.js';
|
|
2
|
+
/** Dispatch one newly recorded packet once. Legacy rows, existing attempts and
|
|
3
|
+
* interrupted sending claims never turn into an unrequested second send. */
|
|
4
|
+
export declare function sendRecordedEvent(service: string, deliveryType: string, deliveryId: string, root: string | undefined, transport: EventTransport, trace: Record<string, string>): Promise<void>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// FIRST DECLARED-EVENT OUTCOMES (architecture, "What an author writes, and how").
|
|
2
|
+
// A queued packet is not evidence its receiver answered. Keep the first attempt
|
|
3
|
+
// under the shared action lock; this is not the originating-write outbox or a
|
|
4
|
+
// substitute for a vendor's declared retry/redelivery policy.
|
|
5
|
+
import { applyTwinWriteAtomic } from "./serve.js";
|
|
6
|
+
import { worldNow } from "./world-clock.js";
|
|
7
|
+
const ATTEMPT = '_world_event_delivery_attempt';
|
|
8
|
+
/** Dispatch one newly recorded packet once. Legacy rows, existing attempts and
|
|
9
|
+
* interrupted sending claims never turn into an unrequested second send. */
|
|
10
|
+
export async function sendRecordedEvent(service, deliveryType, deliveryId, root, transport, trace) {
|
|
11
|
+
const { value: claim } = await applyTwinWriteAtomic(service, (resources) => {
|
|
12
|
+
const row = resources.find((r) => r.type === deliveryType && r.id === deliveryId);
|
|
13
|
+
if (!row || row.deleted === true || row.delivery_protocol !== 1 || row.delivery_state !== 'queued'
|
|
14
|
+
|| row.attempt_count !== 0 || row.current_attempt !== null)
|
|
15
|
+
return { kind: 'skip', value: undefined };
|
|
16
|
+
const attemptId = `${deliveryType}:${deliveryId}:1`;
|
|
17
|
+
if (resources.some((r) => r.type === ATTEMPT && r.id === attemptId))
|
|
18
|
+
return { kind: 'skip', value: undefined };
|
|
19
|
+
if (typeof row.url !== 'string' || typeof row.body !== 'string'
|
|
20
|
+
|| !row.headers || typeof row.headers !== 'object' || Array.isArray(row.headers)
|
|
21
|
+
|| Object.values(row.headers).some((v) => typeof v !== 'string')) {
|
|
22
|
+
throw new Error('A retained declared-event packet is incomplete; it cannot be dispatched.');
|
|
23
|
+
}
|
|
24
|
+
const occurredAt = worldNow();
|
|
25
|
+
return { kind: 'write', value: {
|
|
26
|
+
attemptId, url: row.url, body: row.body, headers: { ...row.headers },
|
|
27
|
+
}, write: {
|
|
28
|
+
operation: 'world_event_delivery.claim', subjectType: deliveryType, subjectId: deliveryId,
|
|
29
|
+
fields: { delivery_state: 'sending', attempt_count: 1, current_attempt: attemptId },
|
|
30
|
+
projection: { creates: [{ type: ATTEMPT, id: attemptId, fields: {
|
|
31
|
+
delivery_type: deliveryType, delivery_id: deliveryId, attempt: 1,
|
|
32
|
+
state: 'sending', started_at: Date.parse(occurredAt), finished_at: null, transport_status: null,
|
|
33
|
+
} }] },
|
|
34
|
+
occurredAt, actor: { kind: 'system' },
|
|
35
|
+
} };
|
|
36
|
+
}, root);
|
|
37
|
+
if (!claim)
|
|
38
|
+
return;
|
|
39
|
+
let status = null;
|
|
40
|
+
try {
|
|
41
|
+
const answer = await transport(claim.url, claim.body, { ...claim.headers, ...trace });
|
|
42
|
+
if (typeof answer === 'number' && Number.isInteger(answer) && answer >= 100 && answer <= 599)
|
|
43
|
+
status = answer;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A thrown request can have reached the receiver. Preserve uncertainty and
|
|
47
|
+
// never copy exception strings, response bodies or credentials into history.
|
|
48
|
+
}
|
|
49
|
+
const state = status === null ? 'unknown' : status >= 200 && status < 300 ? 'succeeded' : 'failed';
|
|
50
|
+
const occurredAt = worldNow(), finishedAt = Date.parse(occurredAt);
|
|
51
|
+
await applyTwinWriteAtomic(service, (resources) => {
|
|
52
|
+
const attempt = resources.find((r) => r.type === ATTEMPT && r.id === claim.attemptId);
|
|
53
|
+
if (!attempt || attempt.deleted === true || attempt.state !== 'sending'
|
|
54
|
+
|| attempt.delivery_type !== deliveryType || attempt.delivery_id !== deliveryId)
|
|
55
|
+
return { kind: 'skip', value: undefined };
|
|
56
|
+
const row = resources.find((r) => r.type === deliveryType && r.id === deliveryId);
|
|
57
|
+
const current = row && row.deleted !== true && row.delivery_protocol === 1
|
|
58
|
+
&& row.delivery_state === 'sending' && row.current_attempt === claim.attemptId;
|
|
59
|
+
return { kind: 'write', value: undefined, write: {
|
|
60
|
+
operation: 'world_event_delivery.finish', subjectType: ATTEMPT, subjectId: claim.attemptId,
|
|
61
|
+
fields: { state, finished_at: finishedAt, transport_status: status },
|
|
62
|
+
...(current && row ? { projection: { updates: [{ type: deliveryType, id: deliveryId, fields: {
|
|
63
|
+
delivery_state: state, finished_at: finishedAt, transport_status: status,
|
|
64
|
+
// A future manual attempt must not erase earlier positive acceptance.
|
|
65
|
+
accepted_at: state === 'succeeded' ? row.accepted_at ?? finishedAt : row.accepted_at ?? null,
|
|
66
|
+
} }] } } : {}),
|
|
67
|
+
occurredAt, actor: { kind: 'system' },
|
|
68
|
+
} };
|
|
69
|
+
}, root);
|
|
70
|
+
}
|
package/dist/src/events.d.ts
CHANGED
|
@@ -110,7 +110,8 @@ export type EventsDecl = {
|
|
|
110
110
|
envelope: unknown;
|
|
111
111
|
/** Where the World's endpoints are kept: one kind, or several (GitHub's repository and organization hooks). */
|
|
112
112
|
endpoints: EndpointsDecl | ReadonlyArray<EndpointsDecl>;
|
|
113
|
-
/** The bookkeeping type each delivery is kept as (`_webhook_message`)
|
|
113
|
+
/** The bookkeeping type each delivery is kept as (`_webhook_message`), otherwise `_world_event_delivery`.
|
|
114
|
+
* Queuing and the first transport attempt/outcome are separate retained facts. */
|
|
114
115
|
record?: string;
|
|
115
116
|
};
|
|
116
117
|
/** An event's further envelope values (`$`-named) the pack computes for a write, from the vendor's own state (the
|
|
@@ -131,7 +132,7 @@ export type EventWrite = {
|
|
|
131
132
|
* it: the pack's `semantics/events.ts`, under the handler contract's read-only context, for each type the write sends
|
|
132
133
|
* (a Slack message renders as `message` and as `app_mention`). */
|
|
133
134
|
export type EventRender<C> = (ctx: C, write: EventWrite, type: string) => Record<string, unknown>;
|
|
134
|
-
/** A delivery
|
|
135
|
+
/** A rendered delivery packet. Its transport attempt and acceptance are separate retained facts. */
|
|
135
136
|
export type EventDelivery = {
|
|
136
137
|
url: string;
|
|
137
138
|
body: string;
|
|
@@ -163,8 +164,8 @@ export declare function eventTypeOf(decl: Pick<EventsDecl, 'types' | 'known'>, o
|
|
|
163
164
|
/** Whether an endpoint's list of event types takes a type: every type when the list is empty, `*` or `<family>.*`
|
|
164
165
|
* its patterns, else the type itself. */
|
|
165
166
|
export declare function takesEvent(list: unknown, type: string): boolean;
|
|
166
|
-
/** Who watches
|
|
167
|
-
*
|
|
167
|
+
/** Who watches signed packets before dispatch (a walk reading each as its receiver would). This is not evidence of
|
|
168
|
+
* transport or acceptance. Set and restored by the walk, as its World store is; unset, nothing watches. */
|
|
168
169
|
export type DeliveryObserver = (delivery: EventDelivery & {
|
|
169
170
|
service: string;
|
|
170
171
|
}) => void;
|
package/dist/src/events.js
CHANGED
|
@@ -10,6 +10,9 @@ import { createHash, createHmac } from 'node:crypto';
|
|
|
10
10
|
import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from "./serve.js";
|
|
11
11
|
import { equalSecrets as same, ksuidFrom } from "./signing.js";
|
|
12
12
|
import { deliveryTraceHeaders } from "./trace-context.js";
|
|
13
|
+
import { sendRecordedEvent } from "./event-delivery.js";
|
|
14
|
+
import { getActiveWorldStore, withWorldStore } from "./world-store.js";
|
|
15
|
+
import { worldPaths } from "./storage.js";
|
|
13
16
|
/** Whether an endpoint takes an event by its `match`: every field of one match (any, when several) equal to the event's
|
|
14
17
|
* value, or, a list, holding it. */
|
|
15
18
|
function matches(row, match, filled) {
|
|
@@ -328,6 +331,7 @@ async function deliverEventType(service, decl, write, data, transport, values, t
|
|
|
328
331
|
const shared = JSON.stringify(envelope);
|
|
329
332
|
const each = perEndpoint(decl.envelope);
|
|
330
333
|
const sent = [];
|
|
334
|
+
const recorded = [];
|
|
331
335
|
for (const [i, { row, kind }] of endpoints.entries()) {
|
|
332
336
|
const body = each ? JSON.stringify(fillEnvelope(decl.envelope, filled, row)) : shared;
|
|
333
337
|
const url = String(at(row, kind.url));
|
|
@@ -338,22 +342,33 @@ async function deliverEventType(service, decl, write, data, transport, values, t
|
|
|
338
342
|
const headers = { ...extraHeaders, ...signEventAll(decl.scheme, String(at(row, kind.secret)), body, seconds, url, seq, identity) };
|
|
339
343
|
sent.push({ url, body, headers });
|
|
340
344
|
// kept with the write, by an id of the delivery itself (two made at once never take the same one)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
const record = decl.record ?? '_world_event_delivery';
|
|
346
|
+
const id = `${record.slice(1)}_v1_${seq}`;
|
|
347
|
+
await applyTwinWrite(service, {
|
|
348
|
+
operation: `${record.slice(1)}.record`, subjectType: record, subjectId: id,
|
|
349
|
+
fields: { url, body, headers, event_type: type, sent_at: millis,
|
|
350
|
+
delivery_protocol: 1, event_id: String(filled.$id),
|
|
351
|
+
endpoint_type: kind.storedAs, endpoint_id: String(row.id),
|
|
352
|
+
delivery_state: 'queued', attempt_count: 0, current_attempt: null,
|
|
353
|
+
transport_status: null, finished_at: null, accepted_at: null,
|
|
354
|
+
}, occurredAt: write.occurredAt, actor: { kind: 'system' },
|
|
355
|
+
}, write.root);
|
|
356
|
+
recorded.push({ type: record, id });
|
|
347
357
|
}
|
|
348
358
|
// sent after the write's answer, as a vendor sends it: the caller never waits on its receiver, and a receiver that is
|
|
349
359
|
// slow or down fails no request
|
|
350
360
|
// the write's trace is read now, inside its request, and carried on each delivery (W3C trace continuation)
|
|
351
361
|
const trace = deliveryTraceHeaders();
|
|
362
|
+
// A timer must retain the request's store/root even if another World becomes
|
|
363
|
+
// active before it runs. Transport completion writes to the same retained state.
|
|
364
|
+
const store = getActiveWorldStore(), deliveryRoot = worldPaths(service, write.root).root;
|
|
352
365
|
for (const d of sent)
|
|
353
366
|
deliveryObserver?.({ ...d, service });
|
|
354
|
-
for (const
|
|
367
|
+
for (const packet of recorded)
|
|
355
368
|
setTimeout(() => {
|
|
356
|
-
|
|
369
|
+
// Persistence failure leaves a queued/sending claim for explicit recovery;
|
|
370
|
+
// it must not fail the already-committed vendor write or cause a blind resend.
|
|
371
|
+
void withWorldStore(store, () => sendRecordedEvent(service, packet.type, packet.id, deliveryRoot, transport, trace)).catch(() => { });
|
|
357
372
|
}, 0);
|
|
358
373
|
return sent;
|
|
359
374
|
}
|
|
@@ -20,12 +20,16 @@ export declare class GraphqlError extends Error {
|
|
|
20
20
|
readonly type?: string | undefined;
|
|
21
21
|
constructor(message: string, type?: string | undefined);
|
|
22
22
|
}
|
|
23
|
+
type GraphqlLib = typeof import('graphql');
|
|
24
|
+
export type GraphqlLibraryLoader = () => Promise<GraphqlLib>;
|
|
23
25
|
/** A GraphQL endpoint over a schema and a pack's part: answers a request's `{query, variables, operationName}`. The
|
|
24
26
|
* context each resolver receives is made per request (the handler context). `unmodeled` answers a query selecting a
|
|
25
27
|
* field nothing models; `mutationRefused`, when given, a mutation the World may not make (a read-only twin). */
|
|
26
28
|
export declare function graphqlEndpoint<C>(options: {
|
|
27
29
|
sdl: string;
|
|
28
30
|
part: GraphqlPart<C>;
|
|
31
|
+
/** The generated pack fetch's lazy import, resolving the library from the pack that declares it. */
|
|
32
|
+
loadLibrary?: GraphqlLibraryLoader;
|
|
29
33
|
context: (request: Request) => Promise<C>;
|
|
30
34
|
unmodeled: (field: {
|
|
31
35
|
type: string;
|
|
@@ -33,3 +37,4 @@ export declare function graphqlEndpoint<C>(options: {
|
|
|
33
37
|
}) => Response;
|
|
34
38
|
mutationRefused?: (request: Request) => Response | undefined;
|
|
35
39
|
}): (request: Request) => Promise<Response>;
|
|
40
|
+
export {};
|
package/dist/src/graphql-wire.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// modeled; any other is answered by the manifest's `graphql.unmodeled`, naming the first field nothing models. An error
|
|
8
8
|
// a resolver throws (`GraphqlError`) carries its `type`, as GitHub's GraphQL errors do.
|
|
9
9
|
//
|
|
10
|
-
// The
|
|
11
|
-
//
|
|
12
|
-
// loads nothing.
|
|
10
|
+
// The generated pack fetch supplies a lazy pack-local loader for the `graphql` protocol library installed by the pack's
|
|
11
|
+
// package.json. Each endpoint caches its library on the first request; callers without a loader keep the kernel-relative
|
|
12
|
+
// lazy import fallback. The kernel's own dependencies stay B1's, and importing the kernel loads nothing.
|
|
13
13
|
/** An error a resolver answers, with the vendor's `type` for it (GitHub's NOT_FOUND, UNPROCESSABLE). */
|
|
14
14
|
export class GraphqlError extends Error {
|
|
15
15
|
type;
|
|
@@ -19,8 +19,6 @@ export class GraphqlError extends Error {
|
|
|
19
19
|
this.name = 'GraphqlError';
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
-
let lib;
|
|
23
|
-
const graphqlLib = () => (lib ??= import('graphql'));
|
|
24
22
|
const snake = (s) => s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
25
23
|
/** A list as a Relay connection, paged by `first`/`last` and `after`. */
|
|
26
24
|
function connection(items, args) {
|
|
@@ -43,6 +41,8 @@ function connection(items, args) {
|
|
|
43
41
|
* context each resolver receives is made per request (the handler context). `unmodeled` answers a query selecting a
|
|
44
42
|
* field nothing models; `mutationRefused`, when given, a mutation the World may not make (a read-only twin). */
|
|
45
43
|
export function graphqlEndpoint(options) {
|
|
44
|
+
let lib;
|
|
45
|
+
const loadLibrary = options.loadLibrary ?? (() => import('graphql'));
|
|
46
46
|
let schema;
|
|
47
47
|
const { resolvers, aliases, stored } = options.part;
|
|
48
48
|
// introspection (`__typename`, `__type`, `__schema` and the `__Type`/`__Field`/… meta types) is the vendor's schema
|
|
@@ -50,7 +50,7 @@ export function graphqlEndpoint(options) {
|
|
|
50
50
|
// before it merges)
|
|
51
51
|
const modeled = (type, field) => field.startsWith('__') || type.startsWith('__') || `${type}.${field}` in resolvers || !!aliases?.[type]?.[field] || !!stored?.[type]?.includes(field) || !!stored?.[type]?.includes(snake(field));
|
|
52
52
|
return async (request) => {
|
|
53
|
-
const g = await
|
|
53
|
+
const g = await (lib ??= loadLibrary());
|
|
54
54
|
schema ??= g.buildSchema(options.sdl, { assumeValidSDL: true });
|
|
55
55
|
const payload = (await request.clone().json().catch(() => ({})));
|
|
56
56
|
let document;
|
package/dist/src/pack-fetch.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ApplicationStandIn } from './events.js';
|
|
|
5
5
|
import { type SocketEngine } from './sockets.js';
|
|
6
6
|
import type { LiveEngine } from './live/types.js';
|
|
7
7
|
import type { WebSocketUpgrade } from './serve-http.js';
|
|
8
|
-
import { type GraphqlPart } from './graphql-wire.js';
|
|
8
|
+
import { type GraphqlPart, type GraphqlLibraryLoader } from './graphql-wire.js';
|
|
9
9
|
/** What a pack declares and writes, and nothing it assembles itself. */
|
|
10
10
|
export type PackParts = {
|
|
11
11
|
manifest: DerivedManifest;
|
|
@@ -39,6 +39,7 @@ export type PackParts = {
|
|
|
39
39
|
* state, served at the manifest's `graphql.paths` (graphql-wire.ts). */
|
|
40
40
|
graphql?: GraphqlPart<HandlerContext> & {
|
|
41
41
|
sdl: string;
|
|
42
|
+
loadLibrary?: GraphqlLibraryLoader;
|
|
42
43
|
};
|
|
43
44
|
/** `semantics/sockets.ts`: each socket's engine, by the id the manifest's `sockets` gives it (sockets.ts). */
|
|
44
45
|
sockets?: Record<string, SocketEngine<HandlerContext>>;
|
package/dist/src/pack-fetch.js
CHANGED
|
@@ -160,7 +160,7 @@ export function createPackFetch(parts, options = {}) {
|
|
|
160
160
|
// the GraphQL wire over the same state, at the paths the manifest names (graphql-wire.ts)
|
|
161
161
|
const gql = m.graphql && parts.graphql ? m.graphql : undefined;
|
|
162
162
|
const graphql = gql && parts.graphql ? graphqlEndpoint({
|
|
163
|
-
sdl: parts.graphql.sdl, part: parts.graphql,
|
|
163
|
+
sdl: parts.graphql.sdl, part: parts.graphql, loadLibrary: parts.graphql.loadLibrary,
|
|
164
164
|
context: async (request) => contextFor(m, { request: request.clone(), operation: { id: 'graphql', method: 'POST', path: gql.paths[0], class: 'action' }, params: {} }, scope),
|
|
165
165
|
unmodeled: ({ type, field }) => Response.json({ errors: [{ ...(gql.unmodeled.type ? { type: gql.unmodeled.type } : {}), message: gql.unmodeled.message.replace('{field}', field).replace('{type}', type) }] }, { status: gql.unmodeled.status ?? 200 }),
|
|
166
166
|
mutationRefused: (request) => ((options.readOnly ?? false) || isReadOnlyRequest(request) ? vendorError(m, m.readOnly) : undefined),
|
|
@@ -270,6 +270,9 @@
|
|
|
270
270
|
{
|
|
271
271
|
"host": "api.clerk.com"
|
|
272
272
|
},
|
|
273
|
+
{
|
|
274
|
+
"host": "dashboard.clerk.com"
|
|
275
|
+
},
|
|
273
276
|
{
|
|
274
277
|
"suffix": ".clerk.accounts.dev"
|
|
275
278
|
},
|
|
@@ -1309,8 +1312,7 @@
|
|
|
1309
1312
|
"host": "api.linear.app"
|
|
1310
1313
|
},
|
|
1311
1314
|
{
|
|
1312
|
-
"host": "linear.app"
|
|
1313
|
-
"pathPattern": "^/oauth/"
|
|
1315
|
+
"host": "linear.app"
|
|
1314
1316
|
}
|
|
1315
1317
|
],
|
|
1316
1318
|
"archetype": "crud",
|
|
@@ -1330,7 +1332,7 @@
|
|
|
1330
1332
|
"comment",
|
|
1331
1333
|
"issue_label"
|
|
1332
1334
|
],
|
|
1333
|
-
"endpointEnvNone": "Linear uses api.linear.app/graphql and linear.app
|
|
1335
|
+
"endpointEnvNone": "Linear uses api.linear.app/graphql and linear.app for its workspace and OAuth authorize page; the World claims those hosts",
|
|
1334
1336
|
"credentialDoor": {
|
|
1335
1337
|
"path": "/_twin/local-credentials",
|
|
1336
1338
|
"body": {},
|
|
@@ -2023,6 +2025,10 @@
|
|
|
2023
2025
|
},
|
|
2024
2026
|
{
|
|
2025
2027
|
"host": "inbound-cdn.resend.com"
|
|
2028
|
+
},
|
|
2029
|
+
{
|
|
2030
|
+
"host": "resend.com",
|
|
2031
|
+
"pathPattern": "^/emails(?:/|$)"
|
|
2026
2032
|
}
|
|
2027
2033
|
],
|
|
2028
2034
|
"archetype": "crud",
|
|
@@ -2179,6 +2185,9 @@
|
|
|
2179
2185
|
]
|
|
2180
2186
|
},
|
|
2181
2187
|
"hosts": [
|
|
2188
|
+
{
|
|
2189
|
+
"host": "app.slack.com"
|
|
2190
|
+
},
|
|
2182
2191
|
{
|
|
2183
2192
|
"host": "slack.com"
|
|
2184
2193
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.155",
|
|
4
4
|
"description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
package/src/client-bundle.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// A page's browser client from its `client/<name>.tsx` entry (the console's, the platform's), runtime-neutral (the
|
|
2
|
-
// serve seam's helpers, docs/contributing/architecture.md#the-serve-seam):
|
|
3
|
-
// by scripts/publish/build.mjs into dist/client/.
|
|
4
|
-
//
|
|
2
|
+
// serve seam's helpers, docs/contributing/architecture.md#the-serve-seam): serve an adjacent `<name>.bundle.js`
|
|
3
|
+
// under either runtime, written at pack time by scripts/publish/build.mjs into dist/client/. Without one, Bun
|
|
4
|
+
// builds the source at serve time and Node refuses it. Kept once per process; a failed attempt is retried on
|
|
5
|
+
// the next call. Nothing here runs at module scope.
|
|
5
6
|
import { existsSync, readFileSync } from 'node:fs';
|
|
6
7
|
|
|
7
8
|
/** A file URL's path on this machine, without `node:url` (the modules that call it ride into browser
|
|
@@ -23,17 +24,15 @@ const PROCESS_SHIM = 'globalThis.process=globalThis.process||{env:{NODE_ENV:"pro
|
|
|
23
24
|
export function bundleClient(entry: string): Promise<string> {
|
|
24
25
|
let pending = bundles.get(entry);
|
|
25
26
|
if (!pending) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return PROCESS_SHIM + readFileSync(prebuilt, 'utf8');
|
|
36
|
-
}))
|
|
27
|
+
pending = Promise.resolve().then(async () => {
|
|
28
|
+
const prebuilt = entry.replace(/\.tsx?$/, '.bundle.js');
|
|
29
|
+
if (existsSync(prebuilt)) return PROCESS_SHIM + readFileSync(prebuilt, 'utf8');
|
|
30
|
+
const bun = (globalThis as { Bun?: Partial<BunBuild> }).Bun;
|
|
31
|
+
if (!bun || typeof bun.build !== 'function') throw new Error(`client bundle missing at ${prebuilt} — the package's \`build\` writes it (scripts/publish/build.mjs); Node serves the prebuilt client`);
|
|
32
|
+
const result = await (bun as BunBuild).build({ entrypoints: [entry], target: 'browser', minify: true });
|
|
33
|
+
if (!result.success) throw new Error(result.logs.map((l) => l.message).join('\n') || `client build failed: ${entry}`);
|
|
34
|
+
return PROCESS_SHIM + await result.outputs[0]!.text();
|
|
35
|
+
})
|
|
37
36
|
.catch((error) => { bundles.delete(entry); throw error; });
|
|
38
37
|
bundles.set(entry, pending);
|
|
39
38
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// FIRST DECLARED-EVENT OUTCOMES (architecture, "What an author writes, and how").
|
|
2
|
+
// A queued packet is not evidence its receiver answered. Keep the first attempt
|
|
3
|
+
// under the shared action lock; this is not the originating-write outbox or a
|
|
4
|
+
// substitute for a vendor's declared retry/redelivery policy.
|
|
5
|
+
import { applyTwinWriteAtomic } from './serve.ts';
|
|
6
|
+
import { worldNow } from './world-clock.ts';
|
|
7
|
+
import type { EventDelivery, EventTransport } from './events.ts';
|
|
8
|
+
|
|
9
|
+
const ATTEMPT = '_world_event_delivery_attempt';
|
|
10
|
+
type Claimed = EventDelivery & { attemptId: string };
|
|
11
|
+
|
|
12
|
+
/** Dispatch one newly recorded packet once. Legacy rows, existing attempts and
|
|
13
|
+
* interrupted sending claims never turn into an unrequested second send. */
|
|
14
|
+
export async function sendRecordedEvent(
|
|
15
|
+
service: string,
|
|
16
|
+
deliveryType: string,
|
|
17
|
+
deliveryId: string,
|
|
18
|
+
root: string | undefined,
|
|
19
|
+
transport: EventTransport,
|
|
20
|
+
trace: Record<string, string>,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
const { value: claim } = await applyTwinWriteAtomic<Claimed | undefined>(service, (resources) => {
|
|
23
|
+
const row = resources.find((r) => r.type === deliveryType && r.id === deliveryId);
|
|
24
|
+
if (!row || row.deleted === true || row.delivery_protocol !== 1 || row.delivery_state !== 'queued'
|
|
25
|
+
|| row.attempt_count !== 0 || row.current_attempt !== null) return { kind: 'skip', value: undefined };
|
|
26
|
+
const attemptId = `${deliveryType}:${deliveryId}:1`;
|
|
27
|
+
if (resources.some((r) => r.type === ATTEMPT && r.id === attemptId)) return { kind: 'skip', value: undefined };
|
|
28
|
+
if (typeof row.url !== 'string' || typeof row.body !== 'string'
|
|
29
|
+
|| !row.headers || typeof row.headers !== 'object' || Array.isArray(row.headers)
|
|
30
|
+
|| Object.values(row.headers).some((v) => typeof v !== 'string')) {
|
|
31
|
+
throw new Error('A retained declared-event packet is incomplete; it cannot be dispatched.');
|
|
32
|
+
}
|
|
33
|
+
const occurredAt = worldNow();
|
|
34
|
+
return { kind: 'write', value: {
|
|
35
|
+
attemptId, url: row.url, body: row.body, headers: { ...row.headers } as Record<string, string>,
|
|
36
|
+
}, write: {
|
|
37
|
+
operation: 'world_event_delivery.claim', subjectType: deliveryType, subjectId: deliveryId,
|
|
38
|
+
fields: { delivery_state: 'sending', attempt_count: 1, current_attempt: attemptId },
|
|
39
|
+
projection: { creates: [{ type: ATTEMPT, id: attemptId, fields: {
|
|
40
|
+
delivery_type: deliveryType, delivery_id: deliveryId, attempt: 1,
|
|
41
|
+
state: 'sending', started_at: Date.parse(occurredAt), finished_at: null, transport_status: null,
|
|
42
|
+
} }] },
|
|
43
|
+
occurredAt, actor: { kind: 'system' as const },
|
|
44
|
+
} };
|
|
45
|
+
}, root);
|
|
46
|
+
if (!claim) return;
|
|
47
|
+
|
|
48
|
+
let status: number | null = null;
|
|
49
|
+
try {
|
|
50
|
+
const answer = await transport(claim.url, claim.body, { ...claim.headers, ...trace });
|
|
51
|
+
if (typeof answer === 'number' && Number.isInteger(answer) && answer >= 100 && answer <= 599) status = answer;
|
|
52
|
+
} catch {
|
|
53
|
+
// A thrown request can have reached the receiver. Preserve uncertainty and
|
|
54
|
+
// never copy exception strings, response bodies or credentials into history.
|
|
55
|
+
}
|
|
56
|
+
const state = status === null ? 'unknown' : status >= 200 && status < 300 ? 'succeeded' : 'failed';
|
|
57
|
+
const occurredAt = worldNow(), finishedAt = Date.parse(occurredAt);
|
|
58
|
+
await applyTwinWriteAtomic(service, (resources) => {
|
|
59
|
+
const attempt = resources.find((r) => r.type === ATTEMPT && r.id === claim.attemptId);
|
|
60
|
+
if (!attempt || attempt.deleted === true || attempt.state !== 'sending'
|
|
61
|
+
|| attempt.delivery_type !== deliveryType || attempt.delivery_id !== deliveryId) return { kind: 'skip', value: undefined };
|
|
62
|
+
const row = resources.find((r) => r.type === deliveryType && r.id === deliveryId);
|
|
63
|
+
const current = row && row.deleted !== true && row.delivery_protocol === 1
|
|
64
|
+
&& row.delivery_state === 'sending' && row.current_attempt === claim.attemptId;
|
|
65
|
+
return { kind: 'write', value: undefined, write: {
|
|
66
|
+
operation: 'world_event_delivery.finish', subjectType: ATTEMPT, subjectId: claim.attemptId,
|
|
67
|
+
fields: { state, finished_at: finishedAt, transport_status: status },
|
|
68
|
+
...(current && row ? { projection: { updates: [{ type: deliveryType, id: deliveryId, fields: {
|
|
69
|
+
delivery_state: state, finished_at: finishedAt, transport_status: status,
|
|
70
|
+
// A future manual attempt must not erase earlier positive acceptance.
|
|
71
|
+
accepted_at: state === 'succeeded' ? row.accepted_at ?? finishedAt : row.accepted_at ?? null,
|
|
72
|
+
} }] } } : {}),
|
|
73
|
+
occurredAt, actor: { kind: 'system' as const },
|
|
74
|
+
} };
|
|
75
|
+
}, root);
|
|
76
|
+
}
|
package/src/events.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { createHash, createHmac } from 'node:crypto';
|
|
|
10
10
|
import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from './serve.ts';
|
|
11
11
|
import { equalSecrets as same, ksuidFrom } from './signing.ts';
|
|
12
12
|
import { deliveryTraceHeaders } from './trace-context.ts';
|
|
13
|
+
import { sendRecordedEvent } from './event-delivery.ts';
|
|
14
|
+
import { getActiveWorldStore, withWorldStore } from './world-store.ts';
|
|
15
|
+
import { worldPaths } from './storage.ts';
|
|
13
16
|
|
|
14
17
|
/** How the vendor signs a delivery. */
|
|
15
18
|
export type EventScheme =
|
|
@@ -99,7 +102,8 @@ export type EventsDecl = {
|
|
|
99
102
|
envelope: unknown;
|
|
100
103
|
/** Where the World's endpoints are kept: one kind, or several (GitHub's repository and organization hooks). */
|
|
101
104
|
endpoints: EndpointsDecl | ReadonlyArray<EndpointsDecl>;
|
|
102
|
-
/** The bookkeeping type each delivery is kept as (`_webhook_message`)
|
|
105
|
+
/** The bookkeeping type each delivery is kept as (`_webhook_message`), otherwise `_world_event_delivery`.
|
|
106
|
+
* Queuing and the first transport attempt/outcome are separate retained facts. */
|
|
103
107
|
record?: string;
|
|
104
108
|
};
|
|
105
109
|
|
|
@@ -117,7 +121,7 @@ export type EventWrite = { operation: string; storedType: string; body: Record<s
|
|
|
117
121
|
* (a Slack message renders as `message` and as `app_mention`). */
|
|
118
122
|
export type EventRender<C> = (ctx: C, write: EventWrite, type: string) => Record<string, unknown>;
|
|
119
123
|
|
|
120
|
-
/** A delivery
|
|
124
|
+
/** A rendered delivery packet. Its transport attempt and acceptance are separate retained facts. */
|
|
121
125
|
export type EventDelivery = { url: string; body: string; headers: Record<string, string> };
|
|
122
126
|
|
|
123
127
|
/** The transport a delivery is made over; the World's by default, a test's own when given. */
|
|
@@ -302,8 +306,8 @@ const worldTransport: EventTransport = async (url, body, headers) => {
|
|
|
302
306
|
try { return ((await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', ...headers }, body })) as { status: number }).status; } catch { return 0; }
|
|
303
307
|
};
|
|
304
308
|
|
|
305
|
-
/** Who watches
|
|
306
|
-
*
|
|
309
|
+
/** Who watches signed packets before dispatch (a walk reading each as its receiver would). This is not evidence of
|
|
310
|
+
* transport or acceptance. Set and restored by the walk, as its World store is; unset, nothing watches. */
|
|
307
311
|
export type DeliveryObserver = (delivery: EventDelivery & { service: string }) => void;
|
|
308
312
|
let deliveryObserver: DeliveryObserver | undefined;
|
|
309
313
|
export function setDeliveryObserver(observer: DeliveryObserver | undefined): DeliveryObserver | undefined {
|
|
@@ -430,6 +434,7 @@ async function deliverEventType(
|
|
|
430
434
|
const shared = JSON.stringify(envelope);
|
|
431
435
|
const each = perEndpoint(decl.envelope);
|
|
432
436
|
const sent: EventDelivery[] = [];
|
|
437
|
+
const recorded: Array<{ type: string; id: string }> = [];
|
|
433
438
|
for (const [i, { row, kind }] of endpoints.entries()) {
|
|
434
439
|
const body = each ? JSON.stringify(fillEnvelope(decl.envelope, filled, row)) : shared;
|
|
435
440
|
const url = String(at(row, kind.url));
|
|
@@ -440,20 +445,31 @@ async function deliverEventType(
|
|
|
440
445
|
const headers = { ...extraHeaders, ...signEventAll(decl.scheme, String(at(row, kind.secret)), body, seconds, url, seq, identity) };
|
|
441
446
|
sent.push({ url, body, headers });
|
|
442
447
|
// kept with the write, by an id of the delivery itself (two made at once never take the same one)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
448
|
+
const record = decl.record ?? '_world_event_delivery';
|
|
449
|
+
const id = `${record.slice(1)}_v1_${seq}`;
|
|
450
|
+
await applyTwinWrite(service, {
|
|
451
|
+
operation: `${record.slice(1)}.record`, subjectType: record, subjectId: id,
|
|
452
|
+
fields: { url, body, headers, event_type: type, sent_at: millis,
|
|
453
|
+
delivery_protocol: 1, event_id: String(filled.$id),
|
|
454
|
+
endpoint_type: kind.storedAs, endpoint_id: String(row.id),
|
|
455
|
+
delivery_state: 'queued', attempt_count: 0, current_attempt: null,
|
|
456
|
+
transport_status: null, finished_at: null, accepted_at: null,
|
|
457
|
+
}, occurredAt: write.occurredAt, actor: { kind: 'system' },
|
|
458
|
+
}, write.root);
|
|
459
|
+
recorded.push({ type: record, id });
|
|
449
460
|
}
|
|
450
461
|
// sent after the write's answer, as a vendor sends it: the caller never waits on its receiver, and a receiver that is
|
|
451
462
|
// slow or down fails no request
|
|
452
463
|
// the write's trace is read now, inside its request, and carried on each delivery (W3C trace continuation)
|
|
453
464
|
const trace = deliveryTraceHeaders();
|
|
465
|
+
// A timer must retain the request's store/root even if another World becomes
|
|
466
|
+
// active before it runs. Transport completion writes to the same retained state.
|
|
467
|
+
const store = getActiveWorldStore(), deliveryRoot = worldPaths(service, write.root).root;
|
|
454
468
|
for (const d of sent) deliveryObserver?.({ ...d, service });
|
|
455
|
-
for (const
|
|
456
|
-
|
|
469
|
+
for (const packet of recorded) setTimeout(() => {
|
|
470
|
+
// Persistence failure leaves a queued/sending claim for explicit recovery;
|
|
471
|
+
// it must not fail the already-committed vendor write or cause a blind resend.
|
|
472
|
+
void withWorldStore(store, () => sendRecordedEvent(service, packet.type, packet.id, deliveryRoot, transport, trace)).catch(() => {});
|
|
457
473
|
}, 0);
|
|
458
474
|
return sent;
|
|
459
475
|
}
|
package/src/graphql-wire.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// modeled; any other is answered by the manifest's `graphql.unmodeled`, naming the first field nothing models. An error
|
|
8
8
|
// a resolver throws (`GraphqlError`) carries its `type`, as GitHub's GraphQL errors do.
|
|
9
9
|
//
|
|
10
|
-
// The
|
|
11
|
-
//
|
|
12
|
-
// loads nothing.
|
|
10
|
+
// The generated pack fetch supplies a lazy pack-local loader for the `graphql` protocol library installed by the pack's
|
|
11
|
+
// package.json. Each endpoint caches its library on the first request; callers without a loader keep the kernel-relative
|
|
12
|
+
// lazy import fallback. The kernel's own dependencies stay B1's, and importing the kernel loads nothing.
|
|
13
13
|
|
|
14
14
|
/** A resolver of one field, `Type.field`: its parent object, its arguments, and the request's handler context. */
|
|
15
15
|
export type GraphqlResolver<C = unknown> = (source: Record<string, unknown>, args: Record<string, unknown>, ctx: C) => unknown;
|
|
@@ -32,8 +32,7 @@ export class GraphqlError extends Error {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
type GraphqlLib = typeof import('graphql');
|
|
35
|
-
|
|
36
|
-
const graphqlLib = (): Promise<GraphqlLib> => (lib ??= import('graphql') as Promise<GraphqlLib>);
|
|
35
|
+
export type GraphqlLibraryLoader = () => Promise<GraphqlLib>;
|
|
37
36
|
|
|
38
37
|
const snake = (s: string): string => s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
39
38
|
|
|
@@ -59,10 +58,14 @@ function connection(items: unknown[], args: Record<string, unknown>): Record<str
|
|
|
59
58
|
export function graphqlEndpoint<C>(options: {
|
|
60
59
|
sdl: string;
|
|
61
60
|
part: GraphqlPart<C>;
|
|
61
|
+
/** The generated pack fetch's lazy import, resolving the library from the pack that declares it. */
|
|
62
|
+
loadLibrary?: GraphqlLibraryLoader;
|
|
62
63
|
context: (request: Request) => Promise<C>;
|
|
63
64
|
unmodeled: (field: { type: string; field: string }) => Response;
|
|
64
65
|
mutationRefused?: (request: Request) => Response | undefined;
|
|
65
66
|
}): (request: Request) => Promise<Response> {
|
|
67
|
+
let lib: Promise<GraphqlLib> | undefined;
|
|
68
|
+
const loadLibrary = options.loadLibrary ?? (() => import('graphql'));
|
|
66
69
|
let schema: import('graphql').GraphQLSchema | undefined;
|
|
67
70
|
const { resolvers, aliases, stored } = options.part;
|
|
68
71
|
// introspection (`__typename`, `__type`, `__schema` and the `__Type`/`__Field`/… meta types) is the vendor's schema
|
|
@@ -71,7 +74,7 @@ export function graphqlEndpoint<C>(options: {
|
|
|
71
74
|
const modeled = (type: string, field: string): boolean =>
|
|
72
75
|
field.startsWith('__') || type.startsWith('__') || `${type}.${field}` in resolvers || !!aliases?.[type]?.[field] || !!stored?.[type]?.includes(field) || !!stored?.[type]?.includes(snake(field));
|
|
73
76
|
return async (request) => {
|
|
74
|
-
const g = await
|
|
77
|
+
const g = await (lib ??= loadLibrary());
|
|
75
78
|
schema ??= g.buildSchema(options.sdl, { assumeValidSDL: true });
|
|
76
79
|
const payload = (await request.clone().json().catch(() => ({}))) as { query?: string; variables?: Record<string, unknown>; operationName?: string };
|
|
77
80
|
let document: import('graphql').DocumentNode;
|
package/src/pack-fetch.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { worldNow } from './world-clock.ts';
|
|
|
35
35
|
import type { WebSocketUpgrade, WebSocketPeer } from './serve-http.ts';
|
|
36
36
|
import { MACHINE_POOL, POOL_KINDS, resetFakePool } from './machines.ts';
|
|
37
37
|
import { applyTwinWrite } from './serve.ts';
|
|
38
|
-
import { graphqlEndpoint, type GraphqlPart } from './graphql-wire.ts';
|
|
38
|
+
import { graphqlEndpoint, type GraphqlPart, type GraphqlLibraryLoader } from './graphql-wire.ts';
|
|
39
39
|
import { gzipSync } from 'node:zlib';
|
|
40
40
|
import { decompress } from './bytes.ts';
|
|
41
41
|
import { decodedBodyLimit, oversizedBody, RequestBodyTooLargeError } from './request-body.ts';
|
|
@@ -71,7 +71,7 @@ export type PackParts = {
|
|
|
71
71
|
lanes?: Record<string, LanePart>;
|
|
72
72
|
/** `semantics/graphql.ts` with the vendor's schema (`generated/graphql-sdl.gen.json`): the GraphQL wire over the same
|
|
73
73
|
* state, served at the manifest's `graphql.paths` (graphql-wire.ts). */
|
|
74
|
-
graphql?: GraphqlPart<HandlerContext> & { sdl: string };
|
|
74
|
+
graphql?: GraphqlPart<HandlerContext> & { sdl: string; loadLibrary?: GraphqlLibraryLoader };
|
|
75
75
|
/** `semantics/sockets.ts`: each socket's engine, by the id the manifest's `sockets` gives it (sockets.ts). */
|
|
76
76
|
sockets?: Record<string, SocketEngine<HandlerContext>>;
|
|
77
77
|
/** `semantics/board.ts`: the vendor's frames on the World's board, read from its state (`GET /twin/board`). */
|
|
@@ -229,7 +229,7 @@ export function createPackFetch(parts: PackParts, options: PackFetchOptions = {}
|
|
|
229
229
|
// the GraphQL wire over the same state, at the paths the manifest names (graphql-wire.ts)
|
|
230
230
|
const gql = m.graphql && parts.graphql ? m.graphql : undefined;
|
|
231
231
|
const graphql = gql && parts.graphql ? graphqlEndpoint<HandlerContext>({
|
|
232
|
-
sdl: parts.graphql.sdl, part: parts.graphql,
|
|
232
|
+
sdl: parts.graphql.sdl, part: parts.graphql, loadLibrary: parts.graphql.loadLibrary,
|
|
233
233
|
context: async (request) => contextFor(m, { request: request.clone(), operation: { id: 'graphql', method: 'POST', path: gql.paths[0]!, class: 'action' } as DerivedOperation, params: {} }, scope) as Promise<HandlerContext>,
|
|
234
234
|
unmodeled: ({ type, field }) => Response.json({ errors: [{ ...(gql.unmodeled.type ? { type: gql.unmodeled.type } : {}), message: gql.unmodeled.message.replace('{field}', field).replace('{type}', type) }] }, { status: gql.unmodeled.status ?? 200 }),
|
|
235
235
|
mutationRefused: (request) => ((options.readOnly ?? false) || isReadOnlyRequest(request) ? vendorError(m, m.readOnly) : undefined),
|