@volter/world-core 3.0.154 → 3.0.156
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 +140 -0
- package/dist/inject.cjs +150 -86
- package/dist/src/actions.d.ts +15 -0
- package/dist/src/actions.js +82 -21
- package/dist/src/ancestry.d.ts +5 -4
- package/dist/src/ancestry.js +53 -28
- package/dist/src/changeset.js +1 -1
- package/dist/src/clickhouse/compression.d.ts +28 -0
- package/dist/src/clickhouse/compression.js +281 -0
- package/dist/src/clickhouse/index.d.ts +1 -0
- package/dist/src/clickhouse/index.js +1 -0
- package/dist/src/client-bundle.js +15 -15
- package/dist/src/derived-core.d.ts +12 -1
- package/dist/src/derived-core.js +145 -11
- package/dist/src/derived.js +3 -0
- package/dist/src/fork.js +1 -1
- package/dist/src/graphql-wire.d.ts +5 -0
- package/dist/src/graphql-wire.js +6 -6
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/h2.js +63 -10
- package/dist/src/head.d.ts +3 -0
- package/dist/src/head.js +9 -0
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +7 -7
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.js +5 -1
- package/dist/src/log.d.ts +23 -3
- package/dist/src/log.js +160 -37
- package/dist/src/managed-original-inputs.d.ts +101 -0
- package/dist/src/managed-original-inputs.js +125 -0
- package/dist/src/mongodb/client.d.ts +8 -0
- package/dist/src/mongodb/client.js +27 -0
- package/dist/src/mongodb/index.d.ts +2 -0
- package/dist/src/mongodb/index.js +5 -0
- package/dist/src/mongodb/stream.d.ts +74 -0
- package/dist/src/mongodb/stream.js +206 -0
- package/dist/src/observe.js +1 -1
- package/dist/src/pack-fetch.d.ts +2 -1
- package/dist/src/pack-fetch.js +21 -9
- package/dist/src/packRegistry.d.ts +10 -14
- package/dist/src/packRegistry.js +2 -13
- package/dist/src/protocol.d.ts +13 -0
- package/dist/src/protocol.js +13 -0
- package/dist/src/redis/engine.d.ts +10 -4
- package/dist/src/redis/engine.js +103 -32
- package/dist/src/redis/member-storage.js +20 -8
- package/dist/src/redis/service-times.d.ts +25 -0
- package/dist/src/redis/service-times.js +87 -0
- package/dist/src/redis/stream.d.ts +2 -1
- package/dist/src/redis/stream.js +109 -20
- package/dist/src/runtime-state.d.ts +11 -0
- package/dist/src/runtime-state.js +13 -0
- package/dist/src/runtime.d.ts +4 -2
- package/dist/src/runtime.js +5 -2
- package/dist/src/serve-http.d.ts +2 -0
- package/dist/src/serve-http.js +21 -4
- package/dist/src/serve.d.ts +10 -0
- package/dist/src/serve.js +43 -3
- package/dist/src/storage.d.ts +2 -1
- package/dist/src/storage.js +6 -3
- package/dist/src/world-boot.d.ts +1 -0
- package/dist/src/world-boot.js +6 -0
- package/dist/src/world-clock.js +6 -2
- package/dist/src/world-store.d.ts +108 -7
- package/dist/src/world-store.js +883 -103
- package/dist/src/xml.d.ts +30 -0
- package/dist/src/xml.js +74 -0
- package/generated/pack-facts.json +140 -0
- package/inject.cjs +150 -86
- package/package.json +20 -1
- package/src/actions.ts +78 -21
- package/src/ancestry.ts +46 -24
- package/src/changeset.ts +1 -1
- package/src/clickhouse/compression.ts +172 -0
- package/src/clickhouse/index.ts +2 -0
- package/src/client-bundle.ts +13 -14
- package/src/derived-core.ts +142 -11
- package/src/derived.ts +3 -0
- package/src/fork.ts +1 -1
- package/src/graphql-wire.ts +9 -6
- package/src/grpc-wire.ts +2 -1
- package/src/h2.ts +46 -5
- package/src/head.ts +9 -0
- package/src/history.ts +7 -7
- package/src/index.ts +6 -1
- package/src/log.ts +147 -32
- package/src/managed-original-inputs.ts +163 -0
- package/src/mongodb/client.ts +27 -0
- package/src/mongodb/index.ts +5 -0
- package/src/mongodb/stream.ts +213 -0
- package/src/observe.ts +1 -1
- package/src/pack-fetch.ts +23 -11
- package/src/packRegistry.ts +3 -15
- package/src/protocol.ts +13 -0
- package/src/redis/engine.ts +100 -57
- package/src/redis/member-storage.ts +17 -9
- package/src/redis/service-times.ts +81 -0
- package/src/redis/stream.ts +66 -14
- package/src/runtime-state.ts +14 -0
- package/src/runtime.ts +6 -1
- package/src/serve-http.ts +23 -4
- package/src/serve.ts +45 -3
- package/src/storage.ts +5 -3
- package/src/world-boot.ts +6 -0
- package/src/world-clock.ts +6 -2
- package/src/world-store.ts +704 -91
- package/src/xml.ts +71 -0
package/src/derived-core.ts
CHANGED
|
@@ -6,8 +6,9 @@ import { parseExactJson, compareExactNumbers, exactInteger, isExactIntegerValue
|
|
|
6
6
|
// serves CRUD generically and applies declared transitions, over the kernel's tree and write path.
|
|
7
7
|
// Nothing here knows a vendor: every vendor difference is a manifest value.
|
|
8
8
|
|
|
9
|
-
import { applyTwinWrite, applyTwinWriteAtomic, twinResources, type TwinResource } from './serve.ts';
|
|
10
|
-
import { copyResource, ownFields, parentStamp, subjectHistory, treeChangesSince, treeStamp } from './log.ts';
|
|
9
|
+
import { applyTwinWrite, applyTwinWriteAtomic, applyTwinWrites, twinResources, type TwinResource } from './serve.ts';
|
|
10
|
+
import { copyResource, foldLocalWrite, heldSubject, ownFields, parentStamp, subjectHistory, treeChangesSince, treeResources, treeStamp, type Subject } from './log.ts';
|
|
11
|
+
import { performsAtHeadNow } from './head.ts';
|
|
11
12
|
import { getActiveWorldStore } from './world-store.ts';
|
|
12
13
|
import { getActiveBlobStore } from './blob-store.ts';
|
|
13
14
|
import { worldPaths } from './storage.ts';
|
|
@@ -26,6 +27,7 @@ import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom, type Han
|
|
|
26
27
|
import { hashFieldValue } from './hash.ts';
|
|
27
28
|
import { readResourceBlob, removeResourceBlob, writeResourceBlob } from './resource-blob.ts';
|
|
28
29
|
import { managedDatabase, unboundDatabase, type ManagedDatabase } from './managed-database.ts';
|
|
30
|
+
import { redisServiceTimes } from './redis/service-times.ts';
|
|
29
31
|
import { execRedisRun, type RedisDialect, type RunItem } from './redis/engine.ts';
|
|
30
32
|
import { sendMail, type Mail, type SmtpRoute } from './smtp.ts';
|
|
31
33
|
import { ownField, parseBracketForm } from './form.ts';
|
|
@@ -738,13 +740,15 @@ export function resourcesOfType(service: string, type: string, root?: string): T
|
|
|
738
740
|
for (const [k, r] of byKey) if (!delta.appended.includes(k)) (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)!).set(k, r);
|
|
739
741
|
for (const k of delta.appended) { const r = byKey.get(k)!; (held.byType.get(r.type) ?? held.byType.set(r.type, new Map()).get(r.type)!).set(k, r); }
|
|
740
742
|
held.stamp = delta.stamp;
|
|
741
|
-
} else
|
|
742
|
-
// the stamp is read before the fold, so a write between them can only leave a newer fold under an older stamp
|
|
743
|
+
} else {
|
|
744
|
+
// the stamp is read once, before the fold, so a write between them can only leave a newer fold under an older stamp
|
|
743
745
|
const stamp = treeStamp(service, root);
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
746
|
+
if (!held || held.stamp !== stamp) {
|
|
747
|
+
const byType = new Map<string, Map<string, TwinResource>>();
|
|
748
|
+
for (const r of twinResources(service, root)) (byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)!).set(`${r.type}:${r.id}`, r);
|
|
749
|
+
held = { stamp, byType };
|
|
750
|
+
indexes.set(key, held);
|
|
751
|
+
}
|
|
748
752
|
}
|
|
749
753
|
return [...(held.byType.get(type)?.values() ?? [])].map(copyResource);
|
|
750
754
|
}
|
|
@@ -1808,6 +1812,17 @@ export type SemanticsContext = {
|
|
|
1808
1812
|
/** Decide an update from the current live stored row under the action lock; undefined leaves it unchanged. */
|
|
1809
1813
|
change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string): Promise<Record<string, unknown> | undefined>;
|
|
1810
1814
|
write(resource: string, id: string, fields: Record<string, unknown>, operation: string): Promise<Record<string, unknown>>;
|
|
1815
|
+
/**
|
|
1816
|
+
* One command's writes as one group (S2 R17; docs/contributing/architecture.md, "A command's writes as one group").
|
|
1817
|
+
* Inside `fn`, `write` stages its write and answers the row as it will stand; `row` and `rowsRaw` read the tree with
|
|
1818
|
+
* the staged writes folded over it, as it will be once they land. When `fn` returns or throws, every staged write
|
|
1819
|
+
* lands, in order, as one append; then its value is answered or its error thrown. Every other way to write, or to
|
|
1820
|
+
* wait on a write, lands the staged writes first; any other read of the tree (`get`, `rows`, `tree`, `history`, ...)
|
|
1821
|
+
* while writes are staged is refused, never answered without them. Each write keeps its own action, id, ordinal,
|
|
1822
|
+
* moment and recorded request. Where the pack has write hooks, events, sockets or counts, a state observer is on,
|
|
1823
|
+
* a head performs, or ids are aliased, `fn` simply runs, each write landing as it is made.
|
|
1824
|
+
*/
|
|
1825
|
+
batch<T>(fn: () => Promise<T>): Promise<T>;
|
|
1811
1826
|
/** Add numeric deltas to an existing bookkeeping row under the kernel's atomic write seam. */
|
|
1812
1827
|
accumulate(resource: string, id: string, deltas: Record<string, number>, operation: string): Promise<void>;
|
|
1813
1828
|
/** `write`, with what a live vendor answered when the head performed it */
|
|
@@ -1917,7 +1932,7 @@ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically
|
|
|
1917
1932
|
};
|
|
1918
1933
|
/** What a write hook reads: the writing call's context without its ways to write, so a hook that renders an event
|
|
1919
1934
|
* cannot write again (and run itself again). */
|
|
1920
|
-
export type WriteHookContext = Omit<HandlerContext, 'write' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
|
|
1935
|
+
export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
|
|
1921
1936
|
/** A derived pack's handler: one operation, by its operationId, over the contract's context. */
|
|
1922
1937
|
export type Handler = (ctx: HandlerContext) => Promise<Response>;
|
|
1923
1938
|
|
|
@@ -1963,7 +1978,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
|
|
|
1963
1978
|
rawResources.set(row as Record<string, unknown>, name ?? resource);
|
|
1964
1979
|
return row as Record<string, unknown>;
|
|
1965
1980
|
};
|
|
1966
|
-
|
|
1981
|
+
const context: SemanticsContext = {
|
|
1967
1982
|
call,
|
|
1968
1983
|
params,
|
|
1969
1984
|
id: Object.values(call.params).at(-1),
|
|
@@ -2172,13 +2187,129 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
|
|
|
2172
2187
|
over: (other) => contextFor(other, { ...asAsked, request: asAsked.request.clone() }, { ...scope, clock: () => at }),
|
|
2173
2188
|
core: async () => { const out = await serveCore(m, { ...call, request: pristine.clone() }, { ...scope, clock: () => at }); return 'served' in out ? out.served : undefined; },
|
|
2174
2189
|
own: (row) => ownFields(row as unknown as TwinResource),
|
|
2190
|
+
batch: (fn) => fn(),
|
|
2191
|
+
};
|
|
2192
|
+
if (batchable(m, root)) bindBatch(m, call, context, params, root, at, rawRow);
|
|
2193
|
+
return context;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
/**
|
|
2197
|
+
* Whether a call's writes may be grouped (`ctx.batch`) with nothing about them changing: every effect a write has
|
|
2198
|
+
* besides its action and the tree (write hooks, events, sockets, a parent's counts, a state observer, a head that
|
|
2199
|
+
* performs it, an alias a read resolves through) is one the grouped path would move, so where any is present the
|
|
2200
|
+
* writes land one at a time, as they always have.
|
|
2201
|
+
*/
|
|
2202
|
+
function batchable(m: DerivedManifest, root: string | undefined): boolean {
|
|
2203
|
+
return !m.onWrite && !m.events && !m.sockets?.length && !transitionObserver
|
|
2204
|
+
&& !Object.values(m.resources).some((decl) => decl.counts && Object.keys(decl.counts).length > 0)
|
|
2205
|
+
&& !performsAtHeadNow(m.service, root) && subjectAliases(m.service, root).size === 0;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
/** A staged write of `ctx.batch`, as `write` was asked it. */
|
|
2209
|
+
type StagedWrite = { resource: string; id: string; fields: Record<string, unknown>; operation: string };
|
|
2210
|
+
|
|
2211
|
+
/** The members of a context that read the tree other than `row`/`rowsRaw`: refused while writes are staged. */
|
|
2212
|
+
const BATCH_REFUSED_READS = ['get', 'rows', 'tree', 'history', 'find', 'list', 'expand', 'mint', 'conflict', 'ownerRow', 'render'] as const;
|
|
2213
|
+
/** The members that write, or wait on writes, or answer as another context: each lands the staged writes first. */
|
|
2214
|
+
const BATCH_LANDING_MEMBERS = ['create', 'createUnique', 'change', 'accumulate', 'writeDetailed', 'record', 'atomically', 'remove', 'issue', 'at', 'over', 'core', 'awaitWrite', 'awaitCondition',
|
|
2215
|
+
// a World's seed and signing keys are written when first asked (worldSeed, worldSigningKey, as a vendor's move)
|
|
2216
|
+
'secret', 'signingKey'] as const;
|
|
2217
|
+
|
|
2218
|
+
/** `ctx.batch` over a context whose writes may be grouped (see `batchable`). */
|
|
2219
|
+
function bindBatch(m: DerivedManifest, call: DerivedCall, context: SemanticsContext, params: Record<string, unknown>, root: string | undefined, at: string,
|
|
2220
|
+
rawRow: (resource: string, row: TwinResource) => Record<string, unknown>): void {
|
|
2221
|
+
let staged: StagedWrite[] | undefined;
|
|
2222
|
+
// each staged subject as it will stand, by `type:id`, in the order first written
|
|
2223
|
+
const subjects = new Map<string, Subject>();
|
|
2224
|
+
const land = async (): Promise<void> => {
|
|
2225
|
+
if (!staged?.length) return;
|
|
2226
|
+
const writes = staged;
|
|
2227
|
+
staged = [];
|
|
2228
|
+
subjects.clear();
|
|
2229
|
+
await writeGroup(m, call, writes, params, root, at);
|
|
2230
|
+
};
|
|
2231
|
+
const ctx = context as unknown as Record<string, unknown>;
|
|
2232
|
+
for (const name of BATCH_LANDING_MEMBERS) {
|
|
2233
|
+
const plain = ctx[name];
|
|
2234
|
+
if (typeof plain !== 'function') continue;
|
|
2235
|
+
ctx[name] = async (...args: unknown[]) => { await land(); return (plain as (...a: unknown[]) => unknown)(...args); };
|
|
2236
|
+
}
|
|
2237
|
+
for (const name of BATCH_REFUSED_READS) {
|
|
2238
|
+
const plain = ctx[name];
|
|
2239
|
+
if (typeof plain !== 'function') continue;
|
|
2240
|
+
ctx[name] = (...args: unknown[]) => {
|
|
2241
|
+
if (staged?.length) throw new Error(`ctx.${name} reads the tree while ctx.batch holds staged writes: only row and rowsRaw see them`);
|
|
2242
|
+
return (plain as (...a: unknown[]) => unknown)(...args);
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
const plainWrite = context.write, plainRow = context.row, plainRowsRaw = context.rowsRaw;
|
|
2246
|
+
context.write = async (resource, id, fields, operation) => {
|
|
2247
|
+
// a vendor's own move (ctx.asVendor) is the clock's write, recorded as the system's: it lands alone, in its place
|
|
2248
|
+
if (!staged || inVendorMove()) { await land(); return plainWrite(resource, id, fields, operation); }
|
|
2249
|
+
const type = storedType(m, resource), key = `${type}:${id}`;
|
|
2250
|
+
// the one subject, as the memoized tree holds it (readTreeMap would copy the whole tree on every staged write)
|
|
2251
|
+
const subject = foldLocalWrite(subjects.get(key) ?? heldSubject(m.service, type, id, root), type, id, fields as SubjectFields, at);
|
|
2252
|
+
subjects.set(key, subject);
|
|
2253
|
+
staged.push({ resource, id, fields, operation });
|
|
2254
|
+
return view(m, resource, treeResources(new Map([[key, subject]]))[0]!);
|
|
2255
|
+
};
|
|
2256
|
+
// the stored rows of a resource's type with the staged subjects folded over them, filtered as `stored` filters
|
|
2257
|
+
const overlaid = (resource: string, opts?: { withDeleted?: boolean }): TwinResource[] | undefined => {
|
|
2258
|
+
if (!staged?.length) return undefined;
|
|
2259
|
+
const type = storedType(m, resource), prefix = `${type}:`;
|
|
2260
|
+
const mine = [...subjects].filter(([key]) => key.startsWith(prefix));
|
|
2261
|
+
if (!mine.length) return undefined;
|
|
2262
|
+
const replaced = new Map(mine.map(([key, subject]) => [key, treeResources(new Map([[key, subject]]))[0]!]));
|
|
2263
|
+
const base = resourcesOfType(m.service, type, root);
|
|
2264
|
+
// a subject the tree holds stays where it stands; one new to it goes to the end, in the order first written
|
|
2265
|
+
const rows = base.map((row) => replaced.get(`${prefix}${row.id}`) ?? row);
|
|
2266
|
+
const held = new Set(base.map((row) => `${prefix}${row.id}`));
|
|
2267
|
+
for (const [key, row] of replaced) if (!held.has(key)) rows.push(row);
|
|
2268
|
+
const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
|
|
2269
|
+
return rows.filter((r) => (opts?.withDeleted || r.deleted !== true) && when.every(([k, v]) => (r as Record<string, unknown>)[k] === v));
|
|
2175
2270
|
};
|
|
2271
|
+
context.row = (resource, id, opts) => {
|
|
2272
|
+
const rows = overlaid(resource, opts);
|
|
2273
|
+
if (!rows) return plainRow(resource, id, opts);
|
|
2274
|
+
const row = rows.find((r) => r.id === id);
|
|
2275
|
+
return row ? rawRow(resource, row) : undefined;
|
|
2276
|
+
};
|
|
2277
|
+
context.rowsRaw = (resource, opts) => {
|
|
2278
|
+
const rows = overlaid(resource, opts);
|
|
2279
|
+
return rows ? rows.map((row) => rawRow(resource, row)) : plainRowsRaw(resource, opts);
|
|
2280
|
+
};
|
|
2281
|
+
context.batch = async (fn) => {
|
|
2282
|
+
// a batch inside a batch is the outer one's
|
|
2283
|
+
if (staged !== undefined) return fn();
|
|
2284
|
+
staged = [];
|
|
2285
|
+
try { return await fn(); }
|
|
2286
|
+
finally {
|
|
2287
|
+
// every write `write` answered lands, whether fn returned or threw: an ordered insert's prefix stays written
|
|
2288
|
+
try { await land(); } finally { staged = undefined; subjects.clear(); }
|
|
2289
|
+
}
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
/**
|
|
2294
|
+
* A command's staged writes landed as one group: each recorded with the request as `writeDetailed` records it (one
|
|
2295
|
+
* recordedInput, the same for every write of the call), the call's moment and actor, in order, under one lock and one
|
|
2296
|
+
* append (serve.ts applyTwinWrites); then a held read on each written type is told, as each write tells it.
|
|
2297
|
+
*/
|
|
2298
|
+
async function writeGroup(m: DerivedManifest, call: DerivedCall, writes: readonly StagedWrite[], params: Record<string, unknown>,
|
|
2299
|
+
root: string | undefined, occurredAt: string): Promise<void> {
|
|
2300
|
+
const input = recordedInput(m, call, params);
|
|
2301
|
+
const files = input.operationId ? await keptFiles(m, call, root) : undefined;
|
|
2302
|
+
const recorded = files ? { ...input, files } : input;
|
|
2303
|
+
const actor = { kind: 'agent' as const, ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) };
|
|
2304
|
+
await applyTwinWrites(m.service, writes.map((write) => ({ operation: write.operation, subjectType: storedType(m, write.resource),
|
|
2305
|
+
subjectId: write.id, fields: write.fields, input: recorded, occurredAt, actor })), root);
|
|
2306
|
+
for (const type of new Set(writes.map((write) => storedType(m, write.resource)))) heldReadsWrite(m.service, root, type);
|
|
2176
2307
|
}
|
|
2177
2308
|
|
|
2178
2309
|
/** A pack's semantics handlers as the dispatch's handlers. */
|
|
2179
2310
|
export function bindSemantics(m: DerivedManifest, handlers: Record<string, Semantics>, scope: CoreScope = {}): Record<string, DerivedHandler> {
|
|
2180
2311
|
prepareViewOmissions(m);
|
|
2181
|
-
return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call: DerivedCall) =>
|
|
2312
|
+
return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call: DerivedCall) => { const ctx = await contextFor(m, call, scope); redisServiceTimes.lap('6c the handler\'s own context opened'); return h(ctx); }]));
|
|
2182
2313
|
}
|
|
2183
2314
|
|
|
2184
2315
|
/** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
|
package/src/derived.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { parseExactJson } from './exact-json.ts';
|
|
|
9
9
|
import type { WebSocketUpgrade } from './serve-http.ts';
|
|
10
10
|
import type { TwinStream } from './twin-fetch.ts';
|
|
11
11
|
import { runWithRequestTrace } from './trace-context.ts';
|
|
12
|
+
import { redisServiceTimes } from './redis/service-times.ts';
|
|
12
13
|
|
|
13
14
|
/** The slice of a generated surface the dispatch reads. */
|
|
14
15
|
export type DerivedOperation = {
|
|
@@ -285,7 +286,9 @@ export function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch {
|
|
|
285
286
|
if (!matched) return fallback(request, undefined, 'no operation at this path');
|
|
286
287
|
for (const observer of dispatchObservers ?? []) observer({ surface: options.surface, operation: matched.operation, owner: handlers[matched.operation.id] ? 'handler' : options.core?.owns(matched.operation) ? 'core' : 'gap' });
|
|
287
288
|
// the body read once, as text, for everything the dispatch does with it (bodyTextOf)
|
|
289
|
+
redisServiceTimes.lap('6a around entered, guards and scenario passed, operation matched again');
|
|
288
290
|
if (request.method !== 'GET' && request.method !== 'HEAD' && request.body && !(request.headers.get('content-type') ?? '').includes('multipart')) tagBody(request, await request.clone().text());
|
|
291
|
+
redisServiceTimes.lap('6b the dispatch read the body as text');
|
|
289
292
|
// the path prefix's named groups (pack-fetch.ts, `pathPrefix`) are the call's parameters too
|
|
290
293
|
const prefixed = request.headers.get(PREFIX_PARAMS_HEADER);
|
|
291
294
|
const call: DerivedCall = { request, operation: matched.operation, params: prefixed ? { ...(parseExactJson(prefixed) as Record<string, string>), ...matched.params } : matched.params };
|
package/src/fork.ts
CHANGED
|
@@ -76,7 +76,7 @@ export function forkTwin(opts: {
|
|
|
76
76
|
};
|
|
77
77
|
getActiveWorldStore().write(forkMetaPath(service, toRoot), `${JSON.stringify(meta, null, 2)}\n`);
|
|
78
78
|
return meta;
|
|
79
|
-
});
|
|
79
|
+
}, { operation: 'pointer publication' });
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function readForkMeta(service: string, root: string): ForkMeta {
|
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/grpc-wire.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { parseExactJson } from './exact-json.ts';
|
|
|
7
7
|
// and what they answer is the pack's. Importing it does nothing.
|
|
8
8
|
import { acceptH2Connection, type H2Handler } from './h2.ts';
|
|
9
9
|
import { nodeBuiltin, WORLD_BOOT_PATH } from './serve-http.ts';
|
|
10
|
+
import { worldBootAnswer } from './world-boot.ts';
|
|
10
11
|
import { decodeMessage, encodeMessage, type ProtoSchema } from './protobuf.ts';
|
|
11
12
|
|
|
12
13
|
/** gRPC's status codes (https://grpc.github.io/grpc/core/md_doc_statuscodes.html). */
|
|
@@ -151,7 +152,7 @@ export function grpcOverFetch(fetch: (request: Request) => Promise<Response>): G
|
|
|
151
152
|
export async function serveGrpc(options: { port?: number; fetch: (request: Request) => Promise<Response>; schema: ProtoSchema }): Promise<{ port: number; stop: () => void }> {
|
|
152
153
|
const net = nodeBuiltin<typeof import('node:net')>('node:net');
|
|
153
154
|
const worldBoot = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.VOLTER_WORLD_BOOT_ID;
|
|
154
|
-
const fetch = worldBoot ? (request: Request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Promise.resolve(
|
|
155
|
+
const fetch = worldBoot ? (request: Request) => (new URL(request.url).pathname === WORLD_BOOT_PATH ? Promise.resolve(worldBootAnswer(worldBoot)) : options.fetch(request)) : options.fetch;
|
|
155
156
|
const handler = grpcStreamHandler(options.schema, grpcOverFetch(fetch));
|
|
156
157
|
const server = net.createServer((socket) => { acceptH2Connection(socket, handler, fetch); });
|
|
157
158
|
await new Promise<void>((resolve, reject) => { server.once('error', reject); server.listen(options.port ?? 0, '127.0.0.1', () => resolve()); });
|
package/src/h2.ts
CHANGED
|
@@ -25,6 +25,36 @@ const MAX_CONCURRENT_STREAMS = 1000;
|
|
|
25
25
|
/** a request body larger than this resets its stream: a memory bound, not Temporal's message limit (the
|
|
26
26
|
* SDKs' gRPC default is 4 MiB; this server does not enforce that one) */
|
|
27
27
|
const MAX_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
28
|
+
|
|
29
|
+
/** A chunked body (RFC 9112 §7.1) from `from`: its bytes once the last chunk and the trailer section have arrived,
|
|
30
|
+
* 'incomplete' before, 'bad' for a malformed size line or one past MAX_REQUEST_BYTES. Extensions and trailers are
|
|
31
|
+
* read past and not kept. */
|
|
32
|
+
function chunkedBody(buffer: Buffer, from: number): Uint8Array | 'incomplete' | 'bad' {
|
|
33
|
+
const parts: Buffer[] = [];
|
|
34
|
+
let at = from, total = 0;
|
|
35
|
+
for (;;) {
|
|
36
|
+
const line = buffer.indexOf('\r\n', at);
|
|
37
|
+
if (line < 0) return buffer.length - at > 4096 ? 'bad' : 'incomplete';
|
|
38
|
+
const size = /^([0-9A-Fa-f]{1,8})[ \t]*(?:;.*)?$/su.exec(buffer.subarray(at, line).toString('latin1'));
|
|
39
|
+
if (!size) return 'bad';
|
|
40
|
+
const length = parseInt(size[1]!, 16);
|
|
41
|
+
at = line + 2;
|
|
42
|
+
if (length === 0) break;
|
|
43
|
+
total += length;
|
|
44
|
+
if (total > MAX_REQUEST_BYTES) return 'bad';
|
|
45
|
+
if (buffer.length < at + length + 2) return 'incomplete';
|
|
46
|
+
if (buffer[at + length] !== 0x0d || buffer[at + length + 1] !== 0x0a) return 'bad';
|
|
47
|
+
parts.push(buffer.subarray(at, at + length));
|
|
48
|
+
at += length + 2;
|
|
49
|
+
}
|
|
50
|
+
// The trailer section: field lines up to an empty line.
|
|
51
|
+
for (;;) {
|
|
52
|
+
const line = buffer.indexOf('\r\n', at);
|
|
53
|
+
if (line < 0) return buffer.length - at > 64 * 1024 ? 'bad' : 'incomplete';
|
|
54
|
+
if (line === at) return Buffer.concat(parts);
|
|
55
|
+
at = line + 2;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
28
58
|
/** a header block (HEADERS + CONTINUATION) larger than this closes the connection */
|
|
29
59
|
const MAX_HEADER_BLOCK = 256 * 1024;
|
|
30
60
|
|
|
@@ -293,11 +323,22 @@ class H2Connection {
|
|
|
293
323
|
this.respondHttp1(new Response('bad header', { status: 400 }));
|
|
294
324
|
return;
|
|
295
325
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
326
|
+
// A request with a Transfer-Encoding has a body framed by it, and its Content-Length is not read (RFC 9112 §6.3):
|
|
327
|
+
// undici sends a streamed body (a World's fetch forwarding a request's own body, duplex 'half') chunked.
|
|
328
|
+
const coding = headers.get('transfer-encoding');
|
|
329
|
+
let body: Uint8Array;
|
|
330
|
+
if (coding !== null) {
|
|
331
|
+
if (coding.split(',').map((each) => each.trim().toLowerCase()).at(-1) !== 'chunked') { this.respondHttp1(new Response('a transfer coding other than chunked is not read', { status: 400 })); return; }
|
|
332
|
+
const read = chunkedBody(this.buffer, end + 4);
|
|
333
|
+
if (read === 'bad') { this.respondHttp1(new Response('bad chunked body', { status: 400 })); return; }
|
|
334
|
+
if (read === 'incomplete') return;
|
|
335
|
+
body = read;
|
|
336
|
+
} else {
|
|
337
|
+
const length = Number(headers.get('content-length') ?? 0);
|
|
338
|
+
if (!Number.isFinite(length) || length < 0 || length > MAX_REQUEST_BYTES) { this.respondHttp1(new Response('bad content-length', { status: 400 })); return; }
|
|
339
|
+
if (this.buffer.length < end + 4 + length) return;
|
|
340
|
+
body = this.buffer.subarray(end + 4, end + 4 + length);
|
|
341
|
+
}
|
|
301
342
|
this.buffer = Buffer.alloc(0);
|
|
302
343
|
this.socket.removeAllListeners('data');
|
|
303
344
|
let request: Request;
|
package/src/head.ts
CHANGED
|
@@ -461,6 +461,15 @@ async function performNow(opts: Parameters<typeof performEntries>[0]): Promise<D
|
|
|
461
461
|
* `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
|
|
462
462
|
* Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
|
|
463
463
|
*/
|
|
464
|
+
/** Whether `performAtHead` would perform an entry now (a twin whose root says `auto`, outside a pack fetch's deferred
|
|
465
|
+
* group): a caller appending several writes at once appends them one at a time then, each performed before the next. */
|
|
466
|
+
export function performsAtHeadNow(service: string, root?: string): boolean {
|
|
467
|
+
const request = currentCorrelationId();
|
|
468
|
+
if (request !== undefined && deferred.has(request)) return false;
|
|
469
|
+
const head = headOf(service, root);
|
|
470
|
+
return head.kind === 'real' && head.deploy === 'auto';
|
|
471
|
+
}
|
|
472
|
+
|
|
464
473
|
export async function performAtHead(service: string, action: Pick<TwinAction, 'id'>, root?: string): Promise<{ performed: boolean; externalId?: string; data?: unknown }> {
|
|
465
474
|
// a pack fetch performs its request's entries as one group after the handler (performRequestGroup)
|
|
466
475
|
const request = currentCorrelationId();
|
package/src/history.ts
CHANGED
|
@@ -91,14 +91,14 @@ export function saveHistoryView(directory: string, view: HistoryView): string {
|
|
|
91
91
|
getActiveWorldStore().writeAtomic(file, JSON.stringify({ ...view, dependencies }));
|
|
92
92
|
for (const dependency of dependencies) commitParentPin(dependency.directory, file);
|
|
93
93
|
return id;
|
|
94
|
-
});
|
|
94
|
+
}, { operation: 'view pin publication' });
|
|
95
95
|
}
|
|
96
96
|
/** What an inherited history's content stands on, as file facts: every segment file the view names
|
|
97
97
|
* (and the view document itself) and each named directory's ancestry record (its generation). A read cache keyed by this sees an
|
|
98
98
|
* ancestor's retained history truncated, replaced or its directory re-created, which the full read
|
|
99
99
|
* refuses (`historyEntries`), instead of serving a tree folded from the history it replaced. */
|
|
100
|
-
export function inheritedHistoryKey(service: string, root?: string): string {
|
|
101
|
-
const parent = readBranchMeta(service, root)?.parent;
|
|
100
|
+
export function inheritedHistoryKey(service: string, root?: string, meta?: string | null): string {
|
|
101
|
+
const parent = readBranchMeta(service, root, meta)?.parent;
|
|
102
102
|
if (!parent?.view) return '';
|
|
103
103
|
const store = getActiveWorldStore();
|
|
104
104
|
const fact = (path: string): string => { const st = store.stat(path); return st ? `${st.size}:${st.mtimeMs}` : '-'; };
|
|
@@ -136,7 +136,7 @@ export function captureHistory(service: string, root?: string): { view: string;
|
|
|
136
136
|
if (entries.length) descriptor.segments.push({ directory, generation, file, start: 0, count: entries.length, digest: historyDigest(entries) });
|
|
137
137
|
}
|
|
138
138
|
return { view: saveHistoryView(directory, descriptor), position: historyLength(descriptor.layout), descriptor };
|
|
139
|
-
});
|
|
139
|
+
}, { operation: 'capture' });
|
|
140
140
|
}
|
|
141
141
|
export function originHead(service: string, root?: string): { view: string; remoteView: string } | null {
|
|
142
142
|
const raw = getActiveWorldStore().read(join(worldPaths(service, root).dir, 'origin-head.json'));
|
|
@@ -162,7 +162,7 @@ export function publishOriginHistory(service: string, root: string, remoteView:
|
|
|
162
162
|
const view = saveHistoryView(directory, { version: 1, owner: { directory, generation }, layout, segments });
|
|
163
163
|
store.writeAtomic(join(directory, 'origin-head.json'), JSON.stringify({ view, remoteView }));
|
|
164
164
|
return { view, appended };
|
|
165
|
-
});
|
|
165
|
+
}, { operation: 'origin fetch' });
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/** A changeset binds the complete inherited view, including this root's observations. */
|
|
@@ -171,7 +171,7 @@ export function captureParentHistory(service: string, root?: string): HistoryRef
|
|
|
171
171
|
const head = captureHistory(service, root);
|
|
172
172
|
const position = head.position - head.descriptor.layout.branch;
|
|
173
173
|
return { view: saveHistoryView(worldPaths(service, root).dir, historyPrefix(head.descriptor, position)), position };
|
|
174
|
-
});
|
|
174
|
+
}, { operation: 'capture' });
|
|
175
175
|
}
|
|
176
176
|
/** Compare evaluated views, so removals, reordered rows and layout changes count as drift. */
|
|
177
177
|
export function historyChanges(service: string, before: HistoryView, after: HistoryView): Entry[] {
|
|
@@ -222,7 +222,7 @@ export function historyAtInstant(service: string, instant: string, root?: string
|
|
|
222
222
|
}
|
|
223
223
|
const view = saveHistoryView(directory, { version: 1, owner: source.owner, layout, segments, ...(source.origin ? { origin: { ...source.origin, ...(keep.slice(0, source.origin.position).some(v => !v) ? { incomplete: true } : {}) } } : {}) });
|
|
224
224
|
return { view, position: historyLength(layout) };
|
|
225
|
-
});
|
|
225
|
+
}, { operation: 'capture' });
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
/** Replace only the tracked origin layer; inherited local layers keep their exact projection order. */
|
package/src/index.ts
CHANGED
|
@@ -53,7 +53,7 @@ export { VendorUnreachableError } from './vendor-call.ts';
|
|
|
53
53
|
export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER } from './request-scope.ts';
|
|
54
54
|
|
|
55
55
|
// ── the serve factory ──────────────────────────────────────────────────────────────────────────
|
|
56
|
-
export { serveHttp, serveStream, StreamListenerUnavailable, type HttpHandler, type HttpServer, type ServeHttpOptions } from './serve-http.ts';
|
|
56
|
+
export { serveHttp, serveStream, nodeBuiltin, StreamListenerUnavailable, type HttpHandler, type HttpServer, type ServeHttpOptions } from './serve-http.ts';
|
|
57
57
|
export type { TwinStream, TwinStreamConnection, TwinStreamSink } from './twin-fetch.ts';
|
|
58
58
|
export { privateTransportKey } from './private-transport.ts';
|
|
59
59
|
// where a page links a hostname its twin answers, inside the World that shows it
|
|
@@ -79,6 +79,8 @@ export type { Batch as EngineBatch, ManagedDatabase, Statement as EngineStatemen
|
|
|
79
79
|
export type { Mail, SmtpRoute } from './smtp.ts';
|
|
80
80
|
// S3's wire (contract "S3's wire is a kernel library too") and the git plane (contract "The git plane is a kernel library")
|
|
81
81
|
export * as s3 from './s3/wire.ts';
|
|
82
|
+
// Generic strict XML grammar; vendor interpretation remains in its consumer.
|
|
83
|
+
export * as xml from './xml.ts';
|
|
82
84
|
export * as clickhouse from './clickhouse/index.ts';
|
|
83
85
|
export { sampleFromSchema } from './schema-sample.ts';
|
|
84
86
|
export * as openaiWire from './openai-wire.ts';
|
|
@@ -91,6 +93,7 @@ export { presignSigV4, signSigV4, verifySigV4, type SigV4Verdict } from './sigv4
|
|
|
91
93
|
export * as redis from './redis/index.ts';
|
|
92
94
|
export * as mysql from './mysql/index.ts';
|
|
93
95
|
export * as live from './live/index.ts';
|
|
96
|
+
export * as mongodb from './mongodb/index.ts';
|
|
94
97
|
export * as git from './git/index.ts';
|
|
95
98
|
|
|
96
99
|
// ── the scenario grammar (architecture, "Behaviour") ───────────────────────────────────────────
|
|
@@ -103,3 +106,5 @@ export { browserSessionToken, startBrowserSession, endBrowserSession } from './b
|
|
|
103
106
|
|
|
104
107
|
export { readHostPort } from './host-port.ts';
|
|
105
108
|
export { assertBrowserName, browserOrigin, validateBrowserState, validateBrowserStorageState, validateBrowserContext, browserAcceptLanguage, cookieMatchesHost, type BrowserCookie, type BrowserState, type BrowserStorageState, type BrowserContext, type BrowserIndexedDB } from './browser-state.ts';
|
|
109
|
+
|
|
110
|
+
export * as managedOriginalInputs from './managed-original-inputs.ts';
|