@volter/world-core 2.0.37 → 3.0.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/README.md +4 -5
- package/app-route.cjs +12 -6
- package/app-route.d.cts +1 -1
- package/dist/app-route.cjs +12 -6
- package/dist/app-route.d.cts +1 -1
- package/dist/generated/pack-facts.json +1410 -3069
- package/dist/inject.cjs +64 -9
- package/dist/pack-facts.cjs +44 -0
- package/dist/src/actions.d.ts +3 -3
- package/dist/src/actions.js +22 -16
- package/dist/src/ancestry.d.ts +14 -2
- package/dist/src/ancestry.js +92 -2
- package/dist/src/anthropic-wire.d.ts +39 -0
- package/dist/src/anthropic-wire.js +136 -0
- package/dist/src/bytes.d.ts +7 -0
- package/dist/src/bytes.js +35 -0
- package/dist/src/changeset.d.ts +1 -1
- package/dist/src/changeset.js +0 -0
- package/dist/src/clickhouse/index.d.ts +3 -0
- package/dist/src/clickhouse/index.js +6 -0
- package/dist/src/clickhouse/sql.d.ts +233 -0
- package/dist/src/clickhouse/sql.js +4329 -0
- package/dist/src/clickhouse/types.d.ts +18 -0
- package/dist/src/clickhouse/types.js +47 -0
- package/dist/src/clickhouse/values.d.ts +146 -0
- package/dist/src/clickhouse/values.js +858 -0
- package/dist/src/client-bundle.js +2 -3
- package/dist/src/cors.d.ts +15 -0
- package/dist/src/cors.js +31 -0
- package/dist/src/derived-core.d.ts +487 -24
- package/dist/src/derived-core.js +788 -144
- package/dist/src/derived-real.d.ts +13 -0
- package/dist/src/derived-real.js +518 -0
- package/dist/src/derived.d.ts +35 -1
- package/dist/src/derived.js +61 -9
- package/dist/src/emit.js +1 -2
- package/dist/src/events.d.ts +206 -0
- package/dist/src/events.js +341 -0
- package/dist/src/executor.d.ts +3 -0
- package/dist/src/executor.js +19 -2
- package/dist/src/file-response.d.ts +6 -0
- package/dist/src/file-response.js +30 -0
- package/dist/src/fork.js +3 -2
- package/dist/src/git/history.d.ts +7 -0
- package/dist/src/git/history.js +24 -0
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/lfs.d.ts +28 -0
- package/dist/src/git/lfs.js +66 -0
- package/dist/src/git/objects.js +3 -8
- package/dist/src/git/smart-http.d.ts +3 -1
- package/dist/src/git/smart-http.js +67 -6
- package/dist/src/graphql-wire.d.ts +29 -0
- package/dist/src/graphql-wire.js +101 -0
- package/dist/src/grpc-wire.d.ts +67 -0
- package/dist/src/grpc-wire.js +170 -0
- package/dist/src/h2.d.ts +40 -0
- package/dist/src/h2.js +656 -0
- package/dist/src/head.d.ts +32 -3
- package/dist/src/head.js +161 -40
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +6 -6
- package/dist/src/hpack.json +1 -0
- package/dist/src/index.d.ts +64 -75
- package/dist/src/index.js +58 -101
- package/dist/src/log.js +28 -19
- package/dist/src/machines.d.ts +50 -0
- package/dist/src/machines.js +151 -0
- package/dist/src/managed-database.d.ts +86 -0
- package/dist/src/managed-database.js +283 -0
- package/dist/src/multipart.d.ts +11 -0
- package/dist/src/multipart.js +51 -0
- package/dist/src/observe.d.ts +15 -5
- package/dist/src/observe.js +23 -9
- package/dist/src/openai-wire.d.ts +108 -0
- package/dist/src/openai-wire.js +337 -0
- package/dist/src/pack-assets.d.ts +3 -4
- package/dist/src/pack-assets.js +15 -10
- package/dist/src/pack-fetch.d.ts +77 -0
- package/dist/src/pack-fetch.js +449 -0
- package/dist/src/pack-paths.d.ts +12 -0
- package/dist/src/pack-paths.js +86 -0
- package/dist/src/packRegistry.d.ts +69 -162
- package/dist/src/packRegistry.js +55 -20
- package/dist/src/people.d.ts +13 -0
- package/dist/src/people.js +18 -0
- package/dist/src/placeholder-image.d.ts +5 -0
- package/dist/src/placeholder-image.js +114 -0
- package/dist/src/protobuf.d.ts +28 -0
- package/dist/src/protobuf.js +332 -0
- package/dist/src/redis/engine.js +1 -1
- package/dist/src/request-scope.d.ts +1 -1
- package/dist/src/request-scope.js +6 -4
- package/dist/src/resource-blob.d.ts +5 -0
- package/dist/src/resource-blob.js +11 -0
- package/dist/src/runtime.d.ts +85 -0
- package/dist/src/runtime.js +104 -0
- package/dist/src/s3/wire.d.ts +60 -0
- package/dist/src/s3/wire.js +157 -0
- package/dist/src/scenario.d.ts +3 -0
- package/dist/src/scenario.js +2 -0
- package/dist/src/schema-sample.d.ts +1 -0
- package/dist/src/schema-sample.js +21 -0
- package/dist/src/sealed-box.d.ts +14 -0
- package/dist/src/sealed-box.js +225 -0
- package/dist/src/serve-http.d.ts +14 -0
- package/dist/src/serve-http.js +27 -3
- package/dist/src/serve.d.ts +6 -0
- package/dist/src/serve.js +69 -14
- package/dist/src/signing.d.ts +135 -0
- package/dist/src/signing.js +222 -0
- package/dist/src/sigv4.d.ts +48 -0
- package/dist/src/sigv4.js +167 -0
- package/dist/src/smtp.d.ts +16 -0
- package/dist/src/smtp.js +72 -0
- package/dist/src/sockets.d.ts +51 -0
- package/dist/src/sockets.js +90 -0
- package/dist/src/state-system.d.ts +1 -0
- package/dist/src/state-system.js +1 -1
- package/dist/src/storage.d.ts +1 -1
- package/dist/src/storage.js +3 -3
- package/dist/src/trace-context.js +1 -1
- package/dist/src/twin-fetch.d.ts +0 -7
- package/dist/src/twin-fetch.js +0 -14
- package/dist/src/vendor-call.d.ts +6 -0
- package/dist/src/vendor-call.js +41 -0
- package/dist/src/world-store.js +1 -1
- package/dist/vendor-hosts.cjs +36 -125
- package/dist/vendor-hosts.d.cts +8 -0
- package/generated/pack-facts.json +1410 -3069
- package/inject.cjs +64 -9
- package/pack-facts.cjs +44 -0
- package/package.json +17 -3
- package/src/actions.ts +23 -16
- package/src/ancestry.ts +74 -2
- package/src/anthropic-wire.ts +137 -0
- package/src/bytes.ts +42 -0
- package/src/changeset.ts +5 -5
- package/src/clickhouse/index.ts +6 -0
- package/src/clickhouse/sql.ts +3059 -0
- package/src/clickhouse/types.ts +44 -0
- package/src/clickhouse/values.ts +697 -0
- package/src/client-bundle.ts +2 -3
- package/src/cors.ts +34 -0
- package/src/derived-core.ts +1013 -146
- package/src/derived-real.ts +434 -0
- package/src/derived.ts +73 -3
- package/src/emit.ts +1 -2
- package/src/events.ts +449 -0
- package/src/executor.ts +24 -2
- package/src/file-response.ts +27 -0
- package/src/fork.ts +3 -2
- package/src/git/history.ts +19 -0
- package/src/git/index.ts +1 -0
- package/src/git/lfs.ts +67 -0
- package/src/git/objects.ts +3 -5
- package/src/git/smart-http.ts +56 -6
- package/src/graphql-wire.ts +106 -0
- package/src/grpc-wire.ts +159 -0
- package/src/h2.ts +627 -0
- package/src/head.ts +132 -41
- package/src/history.ts +6 -6
- package/src/hpack.json +1 -0
- package/src/index.ts +82 -329
- package/src/log.ts +27 -18
- package/src/machines.ts +151 -0
- package/src/managed-database.ts +299 -0
- package/src/multipart.ts +51 -0
- package/src/observe.ts +31 -15
- package/src/openai-wire.ts +371 -0
- package/src/pack-assets.ts +15 -11
- package/src/pack-fetch.ts +458 -0
- package/src/pack-paths.ts +72 -0
- package/src/packRegistry.ts +79 -167
- package/src/people.ts +31 -0
- package/src/placeholder-image.ts +88 -0
- package/src/protobuf.ts +251 -0
- package/src/redis/engine.ts +1 -1
- package/src/request-scope.ts +8 -4
- package/src/resource-blob.ts +13 -0
- package/src/runtime.ts +344 -0
- package/src/s3/wire.ts +172 -0
- package/src/scenario.ts +4 -0
- package/src/schema-sample.ts +24 -0
- package/src/sealed-box.ts +182 -0
- package/src/serve-http.ts +31 -3
- package/src/serve.ts +58 -14
- package/src/signing.ts +231 -0
- package/src/sigv4.ts +158 -0
- package/src/smtp.ts +76 -0
- package/src/sockets.ts +140 -0
- package/src/state-system.ts +2 -2
- package/src/storage.ts +3 -3
- package/src/trace-context.ts +1 -1
- package/src/twin-fetch.ts +0 -20
- package/src/vendor-call.ts +41 -0
- package/src/world-store.ts +1 -1
- package/vendor-hosts.cjs +36 -125
- package/vendor-hosts.d.cts +8 -0
- package/dist/src/mirror-shell.d.ts +0 -2
- package/dist/src/mirror-shell.js +0 -13
- package/dist/src/v1-removed.d.ts +0 -159
- package/dist/src/v1-removed.js +0 -124
- package/src/mirror-shell.ts +0 -15
- package/src/v1-removed.ts +0 -172
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type DerivedManifest, type VendorManifest } from './derived-core.js';
|
|
2
|
+
import type { DerivedSurface } from './derived.js';
|
|
3
|
+
import type { StateSystemAdapters } from './state-system.js';
|
|
4
|
+
/** A derived pack's adapters, from its manifest and its generated surface. */
|
|
5
|
+
export declare function deriveStateSystem(manifest: DerivedManifest, surface: DerivedSurface, lanes?: Record<string, {
|
|
6
|
+
manifest: DerivedManifest;
|
|
7
|
+
surface: DerivedSurface;
|
|
8
|
+
}>): StateSystemAdapters;
|
|
9
|
+
/** A vendor of lanes' adapters: one state system over every lane's surface (the lanes keep the vendor's one store). */
|
|
10
|
+
export declare function deriveVendorStateSystem(vendor: VendorManifest, lanes: Record<string, {
|
|
11
|
+
manifest: DerivedManifest;
|
|
12
|
+
surface: DerivedSurface;
|
|
13
|
+
}>): StateSystemAdapters;
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { aliasesFrom, parentEntries, readParentTreeMap, readTree } from "./log.js";
|
|
2
|
+
import { storedType } from "./derived-core.js";
|
|
3
|
+
import { verifyEvent } from "./events.js";
|
|
4
|
+
import { hashFieldValue } from "./hash.js";
|
|
5
|
+
import { readResourceBlob } from "./resource-blob.js";
|
|
6
|
+
import { RefusedWriteError } from "./head.js";
|
|
7
|
+
import { observeResources } from "./observe.js";
|
|
8
|
+
/** A JSON path's value: `a.b`, `a[0].b`, `$body.a` (the leading `$body.` read as the root). */
|
|
9
|
+
function at(value, path) {
|
|
10
|
+
let cur = value;
|
|
11
|
+
for (const part of path.replace(/^\$body\.?/, '').split(/\.|\[(\d+)\]/).filter((p) => p !== undefined && p !== '')) {
|
|
12
|
+
if (cur === null || typeof cur !== 'object')
|
|
13
|
+
return undefined;
|
|
14
|
+
cur = cur[part];
|
|
15
|
+
}
|
|
16
|
+
return cur;
|
|
17
|
+
}
|
|
18
|
+
const object = (v) => (v !== null && typeof v === 'object' && !Array.isArray(v) ? v : undefined);
|
|
19
|
+
const parse = (r) => { try {
|
|
20
|
+
return r.body ? JSON.parse(r.body) : undefined;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return r.body;
|
|
24
|
+
} };
|
|
25
|
+
/** The manifest's resource a surface schema answers (the operation's `answers.resource` is the spec's schema name). */
|
|
26
|
+
function resourceOf(m, schema) {
|
|
27
|
+
if (!schema)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (m.resources[schema])
|
|
30
|
+
return schema;
|
|
31
|
+
const lower = schema.toLowerCase();
|
|
32
|
+
return Object.keys(m.resources).find((r) => r.toLowerCase() === lower || storedType(m, r).toLowerCase() === lower);
|
|
33
|
+
}
|
|
34
|
+
/** The error template's fixed leaves (Slack's `ok: false`): an answer holding them all is the vendor's refusal, whatever
|
|
35
|
+
* its status (a vendor that answers its errors 200). */
|
|
36
|
+
function refusalShape(m) {
|
|
37
|
+
const out = [];
|
|
38
|
+
const walk = (v, path) => {
|
|
39
|
+
if (typeof v === 'string' && /\{\w+\}/.test(v))
|
|
40
|
+
return;
|
|
41
|
+
const o = object(v);
|
|
42
|
+
if (o) {
|
|
43
|
+
for (const [k, x] of Object.entries(o))
|
|
44
|
+
walk(x, path ? `${path}.${k}` : k);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (path)
|
|
48
|
+
out.push([path, v]);
|
|
49
|
+
};
|
|
50
|
+
walk(m.error, '');
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** The words of a refusal, at the template's `{message}` (else `{code}`) place. */
|
|
54
|
+
function refusalWords(m, body) {
|
|
55
|
+
let found;
|
|
56
|
+
const walk = (v, path) => {
|
|
57
|
+
if (typeof v === 'string' && (v.includes('{message}') || (found === undefined && v.includes('{code}')))) {
|
|
58
|
+
const said = at(body, path);
|
|
59
|
+
if (said !== undefined)
|
|
60
|
+
found = String(said);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const o = object(v);
|
|
64
|
+
if (o)
|
|
65
|
+
for (const [k, x] of Object.entries(o))
|
|
66
|
+
walk(x, path ? `${path}.${k}` : k);
|
|
67
|
+
};
|
|
68
|
+
walk(m.error, '');
|
|
69
|
+
return found ?? (typeof body === 'string' ? body : JSON.stringify(body ?? null)).slice(0, 300);
|
|
70
|
+
}
|
|
71
|
+
/** Every string in a body that names a subject the vendor knows by another id, as the vendor names it. */
|
|
72
|
+
function resolvedDeep(value, resolve) {
|
|
73
|
+
if (typeof value === 'string')
|
|
74
|
+
return resolve(value);
|
|
75
|
+
if (Array.isArray(value))
|
|
76
|
+
return value.map((v) => resolvedDeep(v, resolve));
|
|
77
|
+
const o = object(value);
|
|
78
|
+
return o ? Object.fromEntries(Object.entries(o).map(([k, v]) => [k, resolvedDeep(v, resolve)])) : value;
|
|
79
|
+
}
|
|
80
|
+
/** A multipart body: the recorded fields, then each recorded file part read back from the World's blob store. */
|
|
81
|
+
async function multipart(body, files, ctx) {
|
|
82
|
+
const boundary = `----volter${hashFieldValue({ body, files }).slice(0, 24)}`;
|
|
83
|
+
const chunks = [];
|
|
84
|
+
const text = (t) => { chunks.push(new TextEncoder().encode(t)); };
|
|
85
|
+
for (const [k, v] of Object.entries(object(body) ?? {}))
|
|
86
|
+
if (v !== undefined && v !== null)
|
|
87
|
+
text(`--${boundary}\r\ncontent-disposition: form-data; name="${k}"\r\n\r\n${typeof v === 'object' ? JSON.stringify(v) : String(v)}\r\n`);
|
|
88
|
+
for (const f of files) {
|
|
89
|
+
const bytes = await readResourceBlob(ctx.service ?? '', String(f.blob), ctx.root);
|
|
90
|
+
if (!bytes)
|
|
91
|
+
throw new Error(`the recorded file ${String(f.name)} is not in the World's blob store (${String(f.blob)})`);
|
|
92
|
+
text(`--${boundary}\r\ncontent-disposition: form-data; name="${String(f.field)}"; filename="${String(f.name)}"\r\n${f.type ? `content-type: ${String(f.type)}\r\n` : ''}\r\n`);
|
|
93
|
+
chunks.push(bytes);
|
|
94
|
+
text('\r\n');
|
|
95
|
+
}
|
|
96
|
+
text(`--${boundary}--\r\n`);
|
|
97
|
+
const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0));
|
|
98
|
+
let at = 0;
|
|
99
|
+
for (const c of chunks) {
|
|
100
|
+
out.set(c, at);
|
|
101
|
+
at += c.length;
|
|
102
|
+
}
|
|
103
|
+
return { body: out, type: `multipart/form-data; boundary=${boundary}` };
|
|
104
|
+
}
|
|
105
|
+
/** The body as the operation's encoding sends it. */
|
|
106
|
+
function encode(op, body) {
|
|
107
|
+
if (body === undefined || op.bodyEncoding === 'none')
|
|
108
|
+
return {};
|
|
109
|
+
if (op.bodyEncoding === 'form') {
|
|
110
|
+
const form = new URLSearchParams();
|
|
111
|
+
for (const [k, v] of Object.entries(object(body) ?? {}))
|
|
112
|
+
if (v !== undefined && v !== null)
|
|
113
|
+
form.set(k, typeof v === 'object' ? JSON.stringify(v) : String(v));
|
|
114
|
+
return { body: form.toString(), type: 'application/x-www-form-urlencoded' };
|
|
115
|
+
}
|
|
116
|
+
return { body: JSON.stringify(body), type: 'application/json' };
|
|
117
|
+
}
|
|
118
|
+
/** The unit an entry was written through: its recorded lane, else the one whose surface names its operation. */
|
|
119
|
+
function unitOf(units, input) {
|
|
120
|
+
const id = input.operationId;
|
|
121
|
+
return (typeof input.lane === 'string' ? units.find((u) => u.lane === input.lane) : undefined)
|
|
122
|
+
?? units.find((u) => u.surface.operations.some((o) => o.id === id))
|
|
123
|
+
?? (input.graphql ? units[0] : undefined);
|
|
124
|
+
}
|
|
125
|
+
function performOf(units) {
|
|
126
|
+
return async (execute, group, ctx) => {
|
|
127
|
+
const first = group[0];
|
|
128
|
+
const input = (first.input ?? {});
|
|
129
|
+
const unit = unitOf(units, input);
|
|
130
|
+
if (!unit)
|
|
131
|
+
throw new RefusedWriteError('vendor', `the write records no operation of the vendor's surface (${String(input.operationId ?? 'none')})`, first.id);
|
|
132
|
+
const { manifest: m, surface } = unit;
|
|
133
|
+
// ids the World minted are sent as the vendor's: every stored type's adopted aliases, tried in turn
|
|
134
|
+
const types = [...new Set(Object.keys(m.resources).map((r) => storedType(m, r)))];
|
|
135
|
+
const resolve = (v) => { for (const t of types) {
|
|
136
|
+
const r = ctx.resolve(t, v);
|
|
137
|
+
if (r !== v)
|
|
138
|
+
return r;
|
|
139
|
+
} return v; };
|
|
140
|
+
let method;
|
|
141
|
+
let path;
|
|
142
|
+
let body;
|
|
143
|
+
let op;
|
|
144
|
+
if (input.graphql) {
|
|
145
|
+
const gql = m.graphql;
|
|
146
|
+
method = 'POST';
|
|
147
|
+
path = gql?.paths[0] ?? '/graphql';
|
|
148
|
+
body = resolvedDeep(input.graphql, resolve);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
op = surface.operations.find((o) => o.id === input.operationId);
|
|
152
|
+
method = op.method.toUpperCase();
|
|
153
|
+
const params = { ...(object(input.path) ?? {}) };
|
|
154
|
+
const queried = { ...(object(input.query) ?? {}) };
|
|
155
|
+
const sentBody = input.body === undefined ? undefined : object(input.body) ? { ...object(input.body) } : input.body;
|
|
156
|
+
// a subject named by its key's parts (Slack's message: its `channel` and its `ts`): the parts the request sends,
|
|
157
|
+
// resolved as one id and sent as the vendor's parts
|
|
158
|
+
for (const [resource, decl] of Object.entries(m.resources)) {
|
|
159
|
+
if (!decl.key)
|
|
160
|
+
continue;
|
|
161
|
+
const names = [...decl.key.matchAll(/\{([^}]+)\}/g)].map((x) => x[1]);
|
|
162
|
+
const where = (n) => [params, queried, object(sentBody)].find((o) => o !== undefined && typeof o[n] === 'string');
|
|
163
|
+
if (!names.every((n) => where(n)))
|
|
164
|
+
continue;
|
|
165
|
+
const local = decl.key.replace(/\{([^}]+)\}/g, (_, n) => String(where(n)[n]));
|
|
166
|
+
const vendorId = ctx.resolve(storedType(m, resource), local);
|
|
167
|
+
if (vendorId === local)
|
|
168
|
+
continue;
|
|
169
|
+
const parts = new RegExp(`^${decl.key.replace(/[.*+?^$()|[\]\\]/g, '\\$&').replace(/\\\{[^}]+\\\}|\{[^}]+\}/g, '(.+?)')}$`).exec(vendorId);
|
|
170
|
+
if (parts)
|
|
171
|
+
names.forEach((n, i) => { where(n)[n] = parts[i + 1]; });
|
|
172
|
+
}
|
|
173
|
+
if (op.resource && ctx.service !== undefined)
|
|
174
|
+
vendorFieldParams(m, op.resource, params, ctx);
|
|
175
|
+
path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, name) => encodeURIComponent(resolve(String(params[name] ?? ''))));
|
|
176
|
+
const query = new URLSearchParams(Object.entries(queried).flatMap(([k, v]) => (Array.isArray(v) ? v : [v]).map((x) => [k, resolve(String(x))])));
|
|
177
|
+
if ([...query].length)
|
|
178
|
+
path += `?${query}`;
|
|
179
|
+
body = sentBody === undefined ? undefined : resolvedDeep(sentBody, resolve);
|
|
180
|
+
}
|
|
181
|
+
const files = Array.isArray(input.files) ? input.files : [];
|
|
182
|
+
// a body recorded as it came (not JSON) goes as it came
|
|
183
|
+
const asCame = object(input.body);
|
|
184
|
+
const raw = asCame && typeof asCame.$text === 'string' ? { body: asCame.$text } : asCame && typeof asCame.$base64 === 'string' ? { body: new Uint8Array(Buffer.from(asCame.$base64, 'base64')) } : undefined;
|
|
185
|
+
const sent = raw ? { ...raw, ...(typeof input.contentType === 'string' ? { type: input.contentType } : {}) }
|
|
186
|
+
: files.length ? await multipart(body, files, ctx) : op ? encode(op, body) : { body: JSON.stringify(body), type: 'application/json' };
|
|
187
|
+
const headers = { ...(object(input.headers) ?? {}), ...(sent.type ? { 'content-type': sent.type } : {}) };
|
|
188
|
+
// a retried deploy makes nothing twice, and two Worlds deploying into one account never collide
|
|
189
|
+
if (m.idempotency)
|
|
190
|
+
headers[m.idempotency.header] = hashFieldValue({ credential: ctx.credential ?? '', entry: first.id });
|
|
191
|
+
const answer = await execute({ method, path, headers, ...(sent.body !== undefined ? { body: sent.body } : {}) });
|
|
192
|
+
const parsed = parse(answer);
|
|
193
|
+
const shape = refusalShape(m);
|
|
194
|
+
const refused = answer.status >= 400 || (shape.length > 0 && shape.every(([p, v]) => at(parsed, p) === v));
|
|
195
|
+
if (answer.status === 408 || answer.status === 429 || answer.status >= 500)
|
|
196
|
+
throw new Error(`${answer.status}: ${refusalWords(m, parsed)}${answer.headers['retry-after'] ? ` (retry after ${answer.headers['retry-after']}s)` : ''}`);
|
|
197
|
+
if (refused)
|
|
198
|
+
throw new RefusedWriteError('vendor', `${answer.status >= 400 ? answer.status : 400}: ${refusalWords(m, parsed)}`, first.id);
|
|
199
|
+
// the subject the answer is: under the operation's key, inside the success envelope when the vendor has one
|
|
200
|
+
const key = op?.answers?.key;
|
|
201
|
+
const obj = object(key ? at(parsed, key) : parsed) ?? object(parsed) ?? {};
|
|
202
|
+
// the answered resource: the spec's schema name, else the envelope key the answer is under (Slack's `channel`,
|
|
203
|
+
// `message`), when the manifest has a resource of that name
|
|
204
|
+
const answerKey = op?.answers?.key;
|
|
205
|
+
const resource = resourceOf(m, op?.answers?.resource) ?? (answerKey && m.resources[answerKey] ? answerKey : undefined);
|
|
206
|
+
const decl = resource ? m.resources[resource] : undefined;
|
|
207
|
+
const idOf = (o, d) => { const v = o?.[d?.idAs ?? 'id']; return typeof v === 'string' ? v : undefined; };
|
|
208
|
+
// the group's entry the answer is: of the answered resource's stored type (a cascade writes children first), else
|
|
209
|
+
// the last; a delete answers its subject's own vendor id
|
|
210
|
+
const answered = resource ? [...group].reverse().find((e) => e.subject.type === storedType(m, resource)) : undefined;
|
|
211
|
+
const primary = answered ?? group.at(-1);
|
|
212
|
+
// an answer about a subject no entry of the group is (Slack's conversations.join answers the channel; the group is
|
|
213
|
+
// the membership and its channel_join message) gives no entry its id or its fields: each settles deployed with no
|
|
214
|
+
// vendor id, and refresh adopts it ("Perform", step 2)
|
|
215
|
+
if (resource && !answered)
|
|
216
|
+
return { externalId: '', actionId: primary.id };
|
|
217
|
+
const deleted = primary.fields?.deleted === true;
|
|
218
|
+
// a keyed subject's id is its key filled from the answer (its envelope's fields, Slack's `channel` and `ts`, among them)
|
|
219
|
+
const keyed = decl?.key ? decl : Object.entries(m.resources).find(([r]) => storedType(m, r) === primary.subject.type)?.[1];
|
|
220
|
+
const externalId = deleted ? ctx.resolve(primary.subject.type, primary.subject.id)
|
|
221
|
+
: keyed?.key ? storedIdOf(keyed, { ...(object(parsed) ?? {}), ...obj }) ?? '' : idOf(obj, decl) ?? '';
|
|
222
|
+
const also = [];
|
|
223
|
+
for (const [companion, where] of Object.entries(decl?.companions ?? {})) {
|
|
224
|
+
const id = at(parsed, where) ?? at(obj, where);
|
|
225
|
+
const entry = group.find((e) => e !== primary && e.subject.type === storedType(m, companion));
|
|
226
|
+
if (entry && typeof id === 'string')
|
|
227
|
+
also.push({ actionId: entry.id, externalId: id });
|
|
228
|
+
}
|
|
229
|
+
return { externalId, actionId: primary.id, data: obj, ...(also.length ? { also } : {}) };
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** Path parameters that name a subject by a field rather than its id, sent as the vendor's value of that field, read
|
|
233
|
+
* from the adopted landed copy ("Perform", step 1): a parent found by `parent.where` (a repository addressed as
|
|
234
|
+
* `{owner}/{repo}`), and a subject named by the number it counts per parent (`number`, an issue's `issue_number`).
|
|
235
|
+
* A subject the vendor has not given another id keeps the World's values, which are then the vendor's. */
|
|
236
|
+
function vendorFieldParams(m, resource, params, ctx) {
|
|
237
|
+
const decl = m.resources[resource];
|
|
238
|
+
if (!decl || (!decl.number && !decl.parent?.where))
|
|
239
|
+
return;
|
|
240
|
+
const tree = readTree(ctx.service, ctx.root);
|
|
241
|
+
const landed = readParentTreeMap(ctx.service, ctx.root);
|
|
242
|
+
const vendorCopy = (type, local) => {
|
|
243
|
+
if (!local)
|
|
244
|
+
return undefined;
|
|
245
|
+
const id = ctx.resolve(type, String(local.id));
|
|
246
|
+
return id === String(local.id) ? undefined : landed.get(`${type}:${id}`)?.fields;
|
|
247
|
+
};
|
|
248
|
+
const fill = (template) => template.replace(/\{([^}]+)\}/g, (_, n) => params[n] ?? '');
|
|
249
|
+
// the parent as the World stores it, read before any parameter becomes the vendor's
|
|
250
|
+
const parent = decl.parent;
|
|
251
|
+
const parentType = parent ? storedType(m, parent.resource) : undefined;
|
|
252
|
+
const where = Object.entries(parent?.where ?? {}).map(([field, template]) => [field, template, fill(template)]);
|
|
253
|
+
const localParent = where.length ? tree.find((r) => r.type === parentType && where.every(([f, , v]) => String(r[f]) === v)) : undefined;
|
|
254
|
+
const parentId = parent ? (where.length ? (localParent ? String(localParent.id) : undefined) : parent.params ? fill(parent.value ?? parent.params.map((p) => `{${p}}`).join('/')) : parent.param !== undefined ? params[parent.param] : undefined) : undefined;
|
|
255
|
+
if (decl.number && !decl.key) {
|
|
256
|
+
const type = storedType(m, resource);
|
|
257
|
+
const name = Object.keys(params).at(-1);
|
|
258
|
+
const field = decl.number.field;
|
|
259
|
+
const local = name === undefined ? undefined : tree.find((r) => r.type === type && String(r[field]) === params[name] && (!parent || String(r[parent.field]) === parentId));
|
|
260
|
+
const copy = vendorCopy(type, local);
|
|
261
|
+
if (name !== undefined && copy?.[field] !== undefined)
|
|
262
|
+
params[name] = String(copy[field]);
|
|
263
|
+
}
|
|
264
|
+
const parentCopy = vendorCopy(parentType ?? '', localParent);
|
|
265
|
+
if (parentCopy)
|
|
266
|
+
for (const [field, template] of where) {
|
|
267
|
+
const one = /^\{([^}]+)\}$/.exec(template);
|
|
268
|
+
if (one && parentCopy[field] !== undefined)
|
|
269
|
+
params[one[1]] = String(parentCopy[field]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** The items of a list's answer: under the operation's key, else the answer when it is a list. */
|
|
273
|
+
function itemsOf(op, parsed) {
|
|
274
|
+
const v = op.answers?.key ? at(parsed, op.answers.key) : parsed;
|
|
275
|
+
const list = Array.isArray(v) ? v : Array.isArray(at(parsed, 'data')) ? at(parsed, 'data') : Array.isArray(at(parsed, 'results')) ? at(parsed, 'results') : [];
|
|
276
|
+
return list.map(object).filter((x) => x !== undefined);
|
|
277
|
+
}
|
|
278
|
+
function refreshOf(units, service) {
|
|
279
|
+
return async (execute, opts) => {
|
|
280
|
+
const observed = [];
|
|
281
|
+
const complete = [];
|
|
282
|
+
for (const { manifest: m, surface } of units) {
|
|
283
|
+
// parents before children, however deep
|
|
284
|
+
const scoped = Object.entries(m.resources).filter(([, d]) => d.refresh && !('none' in d.refresh));
|
|
285
|
+
const depth = (r, seen = new Set()) => { const p = m.resources[r]?.parent?.resource; return p && !seen.has(p) ? 1 + depth(p, seen.add(r)) : 0; };
|
|
286
|
+
const order = scoped.sort(([a], [b]) => depth(a) - depth(b));
|
|
287
|
+
for (const [resource, decl] of order) {
|
|
288
|
+
const scope = decl.refresh;
|
|
289
|
+
const opId = 'list' in scope ? scope.list : 'get' in scope ? scope.get : '';
|
|
290
|
+
const op = surface.operations.find((o) => o.id === opId);
|
|
291
|
+
if (!op)
|
|
292
|
+
throw new Error(`${m.vendor}: ${resource}'s refresh names ${opId}, no operation of the surface`);
|
|
293
|
+
const type = storedType(m, resource);
|
|
294
|
+
const parents = decl.parent ? [...new Set(observed.filter((o) => o.type === storedType(m, decl.parent.resource)).map((o) => o.id))] : [undefined];
|
|
295
|
+
let whole = 'list' in scope && scope.complete !== false;
|
|
296
|
+
for (const parent of parents) {
|
|
297
|
+
const limit = m.list?.limit && (op.query ?? []).some((q) => q.name === m.list.limit.param) ? { [m.list.limit.param]: String(m.list.limit.max) } : {};
|
|
298
|
+
let page = { ...limit };
|
|
299
|
+
for (let taken = 0;;) {
|
|
300
|
+
const path = `${op.basePath ?? surface.basePath ?? ''}${op.path}`.replace(/\{([^}]+)\}/g, (_, n) => encodeURIComponent(n === decl.parent?.param ? String(parent) : ''));
|
|
301
|
+
// a parent the operation names in its query, not its path (Slack's conversations.history `channel`)
|
|
302
|
+
const byQuery = decl.parent?.param && parent !== undefined && !op.path.includes(`{${decl.parent.param}}`) ? { [decl.parent.param]: String(parent) } : {};
|
|
303
|
+
const q = new URLSearchParams({ ...page, ...byQuery });
|
|
304
|
+
const answer = await execute({ method: op.method.toUpperCase(), path: [...q].length ? `${path}?${q}` : path });
|
|
305
|
+
if (answer.status === 401 || answer.status === 403)
|
|
306
|
+
throw new Error(`${answer.status}: the vendor refused the root's credential; nothing folded`);
|
|
307
|
+
if (answer.status === 429)
|
|
308
|
+
throw new Error(`429: rate limited${answer.headers['retry-after'] ? `, retry after ${answer.headers['retry-after']}s` : ''}`);
|
|
309
|
+
if (answer.status >= 400)
|
|
310
|
+
throw new Error(`${answer.status}: ${refusalWords(m, parse(answer))}`);
|
|
311
|
+
const parsed = parse(answer);
|
|
312
|
+
const items = 'get' in scope ? [object(op.answers?.key ? at(parsed, op.answers.key) : parsed) ?? {}]
|
|
313
|
+
: 'items' in scope && scope.items ? (at(parsed, scope.items) ?? []).map((x) => object(x)).filter((x) => x !== undefined) : itemsOf(op, parsed);
|
|
314
|
+
for (const item of items) {
|
|
315
|
+
const fields = { ...item, ...(decl.parent && parent !== undefined ? { [decl.parent.field]: parent } : {}) };
|
|
316
|
+
const id = storedIdOf(decl, fields);
|
|
317
|
+
if (id === undefined)
|
|
318
|
+
continue;
|
|
319
|
+
observed.push({ type, id, fields: fields });
|
|
320
|
+
}
|
|
321
|
+
taken += items.length;
|
|
322
|
+
if ('get' in scope)
|
|
323
|
+
break;
|
|
324
|
+
if (taken >= 10_000) {
|
|
325
|
+
whole = false;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
// the next page by the manifest's list model. The list is whole only where its end shows: a cursor model
|
|
329
|
+
// gives no next cursor (a short page may still name one), or a page is empty or shorter than the limit
|
|
330
|
+
// asked; a list the twin cannot page on, or whose page size it does not know, is not known whole.
|
|
331
|
+
if (m.list?.cursor) {
|
|
332
|
+
const next = at(parsed, 'next_cursor') ?? at(parsed, 'response_metadata.next_cursor');
|
|
333
|
+
if (!next)
|
|
334
|
+
break;
|
|
335
|
+
page = { ...page, [m.list.cursor.param]: String(next) };
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const size = Number(limit[m.list?.limit.param ?? ''] ?? 0);
|
|
339
|
+
if (!items.length || (size && items.length < size))
|
|
340
|
+
break;
|
|
341
|
+
if (!size) {
|
|
342
|
+
whole = false;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
const last = items.at(-1)[decl.idAs ?? 'id'];
|
|
346
|
+
if (m.list?.after)
|
|
347
|
+
page = { ...page, [m.list.after]: String(last) };
|
|
348
|
+
else if (m.list?.page)
|
|
349
|
+
page = { ...page, [m.list.page.param]: String(Number(page[m.list.page.param] ?? 1) + 1) };
|
|
350
|
+
else if (m.list?.offset)
|
|
351
|
+
page = { ...page, [m.list.offset.param]: String(taken) };
|
|
352
|
+
else {
|
|
353
|
+
whole = false;
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// a type under a parent is whole only for the parents observed: what it held under another parent stands. A root
|
|
359
|
+
// scoped to one resource (`RootConfig.scope`, GitHub's `repos/o/r`) lists only what is under it, so a type with no
|
|
360
|
+
// parent is never the account's whole list there
|
|
361
|
+
if (whole && !decl.parent && !opts.scope)
|
|
362
|
+
complete.push(type);
|
|
363
|
+
else if (whole && parents.length)
|
|
364
|
+
complete.push({ type, field: decl.parent.field, within: parents.map(String) });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
adopt(service, opts.root, units, observed);
|
|
368
|
+
return observeResources(service, observed, { ...(opts.root !== undefined ? { root: opts.root } : {}), complete });
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/** A vendor's subject's stored id: its key template filled from its fields (its parent's among them), else its id
|
|
372
|
+
* field. Undefined when the subject names none. */
|
|
373
|
+
function storedIdOf(decl, fields) {
|
|
374
|
+
if (decl.key) {
|
|
375
|
+
let missing = false;
|
|
376
|
+
const id = decl.key.replace(/\{([^}]+)\}/g, (_, n) => { const v = fields[n]; if (typeof v !== 'string' && typeof v !== 'number')
|
|
377
|
+
missing = true; return String(v ?? ''); });
|
|
378
|
+
return missing ? undefined : id;
|
|
379
|
+
}
|
|
380
|
+
const id = fields[decl.idAs ?? 'id'];
|
|
381
|
+
return typeof id === 'string' || typeof id === 'number' ? String(id) : undefined;
|
|
382
|
+
}
|
|
383
|
+
/** Adoption ("Refresh"): an observation whose vendor id the World holds under no subject, matching a subject a deploy
|
|
384
|
+
* settled with no vendor id (the same type and parent, and equal on the resource's `alternateKeys`, else the only
|
|
385
|
+
* such subject under that parent), is that subject: it lands as its alias instead of a second subject beside it. */
|
|
386
|
+
function adopt(service, root, units, observed) {
|
|
387
|
+
const entries = parentEntries(service, root);
|
|
388
|
+
const aliases = aliasesFrom(entries);
|
|
389
|
+
const tree = readParentTreeMap(service, root);
|
|
390
|
+
const pending = new Map();
|
|
391
|
+
for (const e of entries) {
|
|
392
|
+
if (e.receipt?.status !== 'deployed' || e.receipt.externalId || e.landsId === undefined || e.aliasOf !== undefined)
|
|
393
|
+
continue;
|
|
394
|
+
const key = `${e.subject.type}:${e.subject.id}`;
|
|
395
|
+
const held = tree.get(key);
|
|
396
|
+
if (aliases.has(key) || !held || held.fields.deleted === true)
|
|
397
|
+
continue;
|
|
398
|
+
const list = pending.get(e.subject.type) ?? [];
|
|
399
|
+
if (!list.some((p) => p.id === e.subject.id))
|
|
400
|
+
list.push({ id: e.subject.id, fields: held.fields });
|
|
401
|
+
pending.set(e.subject.type, list);
|
|
402
|
+
}
|
|
403
|
+
if (!pending.size)
|
|
404
|
+
return;
|
|
405
|
+
const declOf = (type) => {
|
|
406
|
+
for (const u of units) {
|
|
407
|
+
const hit = Object.entries(u.manifest.resources).find(([r]) => storedType(u.manifest, r) === type);
|
|
408
|
+
if (hit)
|
|
409
|
+
return { m: u.manifest, decl: hit[1] };
|
|
410
|
+
}
|
|
411
|
+
return undefined;
|
|
412
|
+
};
|
|
413
|
+
for (const o of observed) {
|
|
414
|
+
const pool = pending.get(o.type);
|
|
415
|
+
if (!pool?.length || tree.has(`${o.type}:${o.id}`))
|
|
416
|
+
continue;
|
|
417
|
+
const { m, decl } = declOf(o.type) ?? {};
|
|
418
|
+
const parentField = decl?.parent?.field;
|
|
419
|
+
// a local child names its parent by the parent's local id, which the parent's adoption aliased to the vendor's
|
|
420
|
+
const parentType = decl?.parent && m ? storedType(m, decl.parent.resource) : undefined;
|
|
421
|
+
const resolved = (v) => aliases.get(`${parentType}:${String(v)}`) ?? String(v);
|
|
422
|
+
const siblings = pool.filter((p) => parentField === undefined || resolved(p.fields[parentField]) === String(o.fields[parentField]));
|
|
423
|
+
const keys = decl?.alternateKeys ?? [];
|
|
424
|
+
const match = keys.length ? siblings.filter((p) => keys.every((k) => p.fields[k] !== undefined && p.fields[k] === o.fields[k])) : siblings.length === 1 ? siblings : [];
|
|
425
|
+
if (match.length !== 1)
|
|
426
|
+
continue;
|
|
427
|
+
o.adopts = match[0].id;
|
|
428
|
+
pool.splice(pool.indexOf(match[0]), 1);
|
|
429
|
+
// its children, observed after it, name it by the local id this adoption aliases
|
|
430
|
+
aliases.set(`${o.type}:${o.adopts}`, o.id);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** The event's resource: the declared `types`, else the manifest's events read backwards, else `<resource>.created|updated|deleted`. */
|
|
434
|
+
function eventResource(m, decl, type) {
|
|
435
|
+
if (decl.types?.[type])
|
|
436
|
+
return decl.types[type];
|
|
437
|
+
for (const [operation, sent] of Object.entries(m.events?.types ?? {})) {
|
|
438
|
+
if ((Array.isArray(sent) ? sent : [sent]).includes(type)) {
|
|
439
|
+
const r = resourceOf(m, operation.split('.')[0]);
|
|
440
|
+
if (r)
|
|
441
|
+
return { resource: r, ...(/\.delete/.test(operation) ? { deleted: true } : {}) };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const pattern = /^(.+)\.(created|updated|deleted)$/.exec(type);
|
|
445
|
+
const r = pattern ? resourceOf(m, pattern[1]) : undefined;
|
|
446
|
+
return r ? { resource: r, ...(pattern[2] === 'deleted' ? { deleted: true } : {}) } : undefined;
|
|
447
|
+
}
|
|
448
|
+
/** A version older than the held one: numbers as numbers ("9" before "10"), timestamps as instants, else as text. */
|
|
449
|
+
function older(theirs, held) {
|
|
450
|
+
if (theirs === undefined || theirs === null || held === undefined || held === null)
|
|
451
|
+
return false;
|
|
452
|
+
const a = Number(theirs);
|
|
453
|
+
const b = Number(held);
|
|
454
|
+
if (String(theirs).trim() !== '' && String(held).trim() !== '' && Number.isFinite(a) && Number.isFinite(b))
|
|
455
|
+
return a < b;
|
|
456
|
+
const ta = Date.parse(String(theirs));
|
|
457
|
+
const tb = Date.parse(String(held));
|
|
458
|
+
if (Number.isFinite(ta) && Number.isFinite(tb))
|
|
459
|
+
return ta < tb;
|
|
460
|
+
return String(theirs) < String(held);
|
|
461
|
+
}
|
|
462
|
+
function ingestOf(units, decl, service) {
|
|
463
|
+
return async (request, ctx) => {
|
|
464
|
+
const body = await request.text();
|
|
465
|
+
const headers = Object.fromEntries(request.headers);
|
|
466
|
+
const now = Math.floor(Date.now() / 1000);
|
|
467
|
+
const schemes = (Array.isArray(decl.scheme) ? decl.scheme : [decl.scheme]);
|
|
468
|
+
if (!ctx.secret || !schemes.some((s) => verifyEvent(s, ctx.secret, body, headers, { now, tolerance: 300 })))
|
|
469
|
+
return Response.json({ error: 'invalid signature' }, { status: 401 });
|
|
470
|
+
let event;
|
|
471
|
+
try {
|
|
472
|
+
event = JSON.parse(body);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return Response.json({ error: 'the event is not JSON' }, { status: 400 });
|
|
476
|
+
}
|
|
477
|
+
if (decl.handshake && Object.entries(decl.handshake.when).every(([k, v]) => at(event, k) === v)) {
|
|
478
|
+
const answer = decl.handshake.answer.startsWith('$body.') ? at(event, decl.handshake.answer) : decl.handshake.answer;
|
|
479
|
+
return new Response(typeof answer === 'string' ? answer : JSON.stringify(answer), { status: 200 });
|
|
480
|
+
}
|
|
481
|
+
const named = [decl.type.header ? request.headers.get(decl.type.header) : decl.type.body ? at(event, decl.type.body) : undefined, decl.type.action ? at(event, decl.type.action) : undefined].filter((x) => typeof x === 'string' && x !== '');
|
|
482
|
+
const type = named.join('.');
|
|
483
|
+
for (const { manifest: m } of units) {
|
|
484
|
+
const found = eventResource(m, decl, type);
|
|
485
|
+
if (!found)
|
|
486
|
+
continue;
|
|
487
|
+
const item = object(at(event, decl.object));
|
|
488
|
+
const d = m.resources[found.resource];
|
|
489
|
+
const id = item ? storedIdOf(d, item) : undefined;
|
|
490
|
+
if (!item || id === undefined)
|
|
491
|
+
break;
|
|
492
|
+
const stored = storedType(m, found.resource);
|
|
493
|
+
// an event older than the root's copy folds nothing
|
|
494
|
+
const held = d.version ? readTree(service, ctx.root).find((r) => r.type === stored && r.id === String(id)) : undefined;
|
|
495
|
+
if (held && d.version && older(item[d.version], held[d.version]))
|
|
496
|
+
break;
|
|
497
|
+
observeResources(service, [{ type: stored, id: String(id), fields: item, ...(found.deleted ? { deleted: true } : {}) }], ctx.root !== undefined ? { root: ctx.root } : {});
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
return Response.json({ received: true });
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
function adaptersOf(units, service, ingest) {
|
|
504
|
+
return { perform: performOf(units), refresh: refreshOf(units, service), ...(ingest ? { ingest: ingestOf(units, ingest, service) } : {}) };
|
|
505
|
+
}
|
|
506
|
+
/** A derived pack's adapters, from its manifest and its generated surface. */
|
|
507
|
+
export function deriveStateSystem(manifest, surface, lanes = {}) {
|
|
508
|
+
// a pack with an API of its own and lanes beside it (Clerk's Backend API and its Frontend API, `fapi`): one state
|
|
509
|
+
// system over its surface and each lane's
|
|
510
|
+
const units = [{ manifest, surface }, ...Object.entries(lanes).map(([lane, u]) => ({ ...u, lane }))];
|
|
511
|
+
return adaptersOf(units, manifest.service, manifest.ingest ?? units.find((u) => u.manifest.ingest)?.manifest.ingest);
|
|
512
|
+
}
|
|
513
|
+
/** A vendor of lanes' adapters: one state system over every lane's surface (the lanes keep the vendor's one store). */
|
|
514
|
+
export function deriveVendorStateSystem(vendor, lanes) {
|
|
515
|
+
const units = Object.entries(lanes).map(([lane, u]) => ({ ...u, lane }));
|
|
516
|
+
const service = units[0]?.manifest.service ?? vendor.vendor;
|
|
517
|
+
return adaptersOf(units, service, vendor.ingest ?? units.find((u) => u.manifest.ingest)?.manifest.ingest);
|
|
518
|
+
}
|
package/dist/src/derived.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WebSocketUpgrade } from './serve-http.js';
|
|
1
2
|
/** The slice of a generated surface the dispatch reads. */
|
|
2
3
|
export type DerivedOperation = {
|
|
3
4
|
id: string;
|
|
@@ -11,6 +12,10 @@ export type DerivedOperation = {
|
|
|
11
12
|
headers?: Record<string, string | null>;
|
|
12
13
|
/** the status the vendor answers success with (201 for a create, 204 for an empty answer) */
|
|
13
14
|
successStatus?: number;
|
|
15
|
+
/** how the spec says the request body is encoded (derive-pack: json, form, multipart, none) */
|
|
16
|
+
bodyEncoding?: string;
|
|
17
|
+
/** the success is documented with no content (a setting's update answered 200 and nothing else) */
|
|
18
|
+
emptySuccess?: boolean;
|
|
14
19
|
/** a form-encoded operation's number and boolean fields, by bracket path (`items.[].quantity`, `metadata.*`) */
|
|
15
20
|
scalars?: Readonly<Record<string, string | undefined>>;
|
|
16
21
|
/** what the success answer is: a resource, a list of them, and the envelope key holding it */
|
|
@@ -21,6 +26,24 @@ export type DerivedOperation = {
|
|
|
21
26
|
};
|
|
22
27
|
/** the path its own server puts before it, where the operation names a server other than the surface's */
|
|
23
28
|
basePath?: string;
|
|
29
|
+
/** the query's declared parameters (a strict body checks what is not one of them) */
|
|
30
|
+
query?: ReadonlyArray<{
|
|
31
|
+
name: string;
|
|
32
|
+
type: string;
|
|
33
|
+
required?: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
/** the credential scopes any one of which lets a caller make the call, and the security schemes it is made with
|
|
36
|
+
* (`anonymous` where it may also go without; absent when it takes none), from the spec's security */
|
|
37
|
+
scopes?: ReadonlyArray<string>;
|
|
38
|
+
credentials?: ReadonlyArray<string>;
|
|
39
|
+
/** the body's declared fields: name, type, and the numeric bounds the spec states (a strict body, `ctx.fields`) */
|
|
40
|
+
body?: ReadonlyArray<{
|
|
41
|
+
name: string;
|
|
42
|
+
type: string;
|
|
43
|
+
required?: boolean;
|
|
44
|
+
minimum?: number;
|
|
45
|
+
maximum?: number;
|
|
46
|
+
}>;
|
|
24
47
|
};
|
|
25
48
|
/** `basePath` is what the vendor's server URL puts before every operation path; `spanning` names the
|
|
26
49
|
* path parameters whose values may hold slashes (a git ref, a file path), which no spec says. */
|
|
@@ -36,6 +59,13 @@ export type DerivedCall = {
|
|
|
36
59
|
operation: DerivedOperation;
|
|
37
60
|
params: Record<string, string>;
|
|
38
61
|
};
|
|
62
|
+
export declare function bodyTextOf(request: Request): Promise<string>;
|
|
63
|
+
/** The body's text, as JSON, or undefined when it is not JSON; the parse is kept for the first taker. */
|
|
64
|
+
export declare function checkJsonBody(request: Request): Promise<boolean>;
|
|
65
|
+
/** The guard's parse of this request's body, taken once (a clone of a tagged request shares its key). */
|
|
66
|
+
export declare function takeParsedBody(request: Request): {
|
|
67
|
+
value: unknown;
|
|
68
|
+
} | undefined;
|
|
39
69
|
export type DerivedHandler = (call: DerivedCall) => Promise<Response>;
|
|
40
70
|
/** What the derived core made of an operation: an answer, or why it cannot model it. */
|
|
41
71
|
export type DerivedCoreOutcome = {
|
|
@@ -58,7 +88,7 @@ export type DerivedFetchOptions = {
|
|
|
58
88
|
* checks, idempotent replay). */
|
|
59
89
|
around?: (call: DerivedCall, next: () => Promise<Response>) => Promise<Response>;
|
|
60
90
|
/** The vendor's answer for an operation nothing models: the pack's gap. */
|
|
61
|
-
gap: (request: Request, operation: DerivedOperation | undefined, reason: string) => Response
|
|
91
|
+
gap: (request: Request, operation: DerivedOperation | undefined, reason: string) => Response | Promise<Response>;
|
|
62
92
|
};
|
|
63
93
|
type Route = {
|
|
64
94
|
operation: DerivedOperation;
|
|
@@ -73,10 +103,14 @@ export declare function matchOperation(routes: Map<string, Route[]>, method: str
|
|
|
73
103
|
params: Record<string, string>;
|
|
74
104
|
} | undefined;
|
|
75
105
|
export type DerivedOwner = 'handler' | 'core' | 'gap';
|
|
106
|
+
/** The header the kernel carries a path prefix's named groups in, from routing to the call (never a client's). */
|
|
107
|
+
export declare const PREFIX_PARAMS_HEADER = "x-volter-path-prefix-params";
|
|
76
108
|
export type DerivedFetch = ((request: Request) => Promise<Response>) & {
|
|
77
109
|
/** Per operationId, who serves it: a semantics handler, the derived core, the pack's existing
|
|
78
110
|
* fetch while it moves, or nothing (the vendor's gap). */
|
|
79
111
|
owners(): Record<string, DerivedOwner>;
|
|
112
|
+
/** The vendor's sockets (sockets.ts), for the serve seam to upgrade to, when its manifest declares any. */
|
|
113
|
+
upgrade?: WebSocketUpgrade;
|
|
80
114
|
};
|
|
81
115
|
export declare function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch;
|
|
82
116
|
export {};
|