@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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// CityHashv1.0.2 arithmetic, ported from the vendor-carried Google source.
|
|
2
|
+
// Source: ClickHouse/ClickHouse contrib/cityhash102/src/city.cc, SHA256
|
|
3
|
+
// 4bcc8f0c09e3ef38210a0c568efc0550e4cb426719d8ad74a466f152296d0df7.
|
|
4
|
+
// Copyright (c) 2011 Google, Inc.
|
|
5
|
+
//
|
|
6
|
+
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
// of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
// in the Software without restriction, including without limitation the rights
|
|
9
|
+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
// copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
// furnished to do so, subject to the following conditions:
|
|
12
|
+
//
|
|
13
|
+
// The above copyright notice and this permission notice shall be included in
|
|
14
|
+
// all copies or substantial portions of the Software.
|
|
15
|
+
//
|
|
16
|
+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
// THE SOFTWARE.
|
|
23
|
+
const k0 = 0xc3a5c85c97cb3127n, k1 = 0xb492b66fbe98f273n, k2 = 0x9ae16a3b2f90404fn, k3 = 0xc949d7c7509e6557n;
|
|
24
|
+
const u = (value) => BigInt.asUintN(64, value);
|
|
25
|
+
const rotate = (value, shift) => shift === 0 ? u(value) : u((u(value) >> BigInt(shift)) | (u(value) << BigInt(64 - shift)));
|
|
26
|
+
const mix = (value) => u(value) ^ (u(value) >> 47n);
|
|
27
|
+
function hash16(first, second) {
|
|
28
|
+
const mul = 0x9ddfea08eb382d69n;
|
|
29
|
+
let a = u((u(first) ^ u(second)) * mul);
|
|
30
|
+
a ^= a >> 47n;
|
|
31
|
+
let b = u((u(second) ^ a) * mul);
|
|
32
|
+
b ^= b >> 47n;
|
|
33
|
+
return u(b * mul);
|
|
34
|
+
}
|
|
35
|
+
/** The CityHash 1.0.2 checksum used by ClickHouse native compression frames. */
|
|
36
|
+
export function cityHash128(bytes) {
|
|
37
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
38
|
+
const fetch = (at) => view.getBigUint64(at, true);
|
|
39
|
+
const short = (at, length) => {
|
|
40
|
+
if (length > 8) {
|
|
41
|
+
const a = fetch(at), b = fetch(at + length - 8);
|
|
42
|
+
return hash16(a, rotate(u(b + BigInt(length)), length)) ^ b;
|
|
43
|
+
}
|
|
44
|
+
if (length >= 4)
|
|
45
|
+
return hash16(BigInt(length) + (BigInt(view.getUint32(at, true)) << 3n), BigInt(view.getUint32(at + length - 4, true)));
|
|
46
|
+
if (length) {
|
|
47
|
+
const a = bytes[at], b = bytes[at + (length >> 1)], c = bytes[at + length - 1];
|
|
48
|
+
return u(mix(u(BigInt(a + (b << 8)) * k2) ^ u(BigInt(length + (c << 2)) * k3)) * k2);
|
|
49
|
+
}
|
|
50
|
+
return k2;
|
|
51
|
+
};
|
|
52
|
+
const weak = (at, first, second) => {
|
|
53
|
+
const w = fetch(at), x = fetch(at + 8), y = fetch(at + 16), z = fetch(at + 24);
|
|
54
|
+
let a = u(first + w), b = rotate(u(second + a + z), 21);
|
|
55
|
+
const c = a;
|
|
56
|
+
a = u(a + x + y);
|
|
57
|
+
b = u(b + rotate(a, 44));
|
|
58
|
+
return [u(a + z), u(b + c)];
|
|
59
|
+
};
|
|
60
|
+
const murmur = (at, length, seed) => {
|
|
61
|
+
let [a, b] = seed, c = 0n, d = 0n, left = length - 16;
|
|
62
|
+
if (left <= 0) {
|
|
63
|
+
a = u(mix(u(a * k1)) * k1);
|
|
64
|
+
c = u(b * k1 + short(at, length));
|
|
65
|
+
d = mix(u(a + (length >= 8 ? fetch(at) : c)));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
c = hash16(u(fetch(at + length - 8) + k1), a);
|
|
69
|
+
d = hash16(u(b + BigInt(length)), u(c + fetch(at + length - 16)));
|
|
70
|
+
a = u(a + d);
|
|
71
|
+
do {
|
|
72
|
+
a ^= u(mix(u(fetch(at) * k1)) * k1);
|
|
73
|
+
a = u(a * k1);
|
|
74
|
+
b ^= a;
|
|
75
|
+
c ^= u(mix(u(fetch(at + 8) * k1)) * k1);
|
|
76
|
+
c = u(c * k1);
|
|
77
|
+
d ^= c;
|
|
78
|
+
at += 16;
|
|
79
|
+
left -= 16;
|
|
80
|
+
} while (left > 0);
|
|
81
|
+
}
|
|
82
|
+
a = hash16(a, c);
|
|
83
|
+
b = hash16(d, b);
|
|
84
|
+
return [a ^ b, hash16(b, a)];
|
|
85
|
+
};
|
|
86
|
+
const seeded = (at, length, seed) => {
|
|
87
|
+
if (length < 128)
|
|
88
|
+
return murmur(at, length, seed);
|
|
89
|
+
let [x, y] = seed, z = u(BigInt(length) * k1);
|
|
90
|
+
let v = [u(rotate(y ^ k1, 49) * k1 + fetch(at)), 0n];
|
|
91
|
+
v[1] = u(rotate(v[0], 42) * k1 + fetch(at + 8));
|
|
92
|
+
let w = [u(rotate(u(y + z), 35) * k1 + x), u(rotate(u(x + fetch(at + 88)), 53) * k1)];
|
|
93
|
+
const round = () => {
|
|
94
|
+
x = u(rotate(u(x + y + v[0] + fetch(at + 16)), 37) * k1);
|
|
95
|
+
y = u(rotate(u(y + v[1] + fetch(at + 48)), 42) * k1);
|
|
96
|
+
x ^= w[1];
|
|
97
|
+
y ^= v[0];
|
|
98
|
+
z = rotate(z ^ w[0], 33);
|
|
99
|
+
v = weak(at, u(v[1] * k1), u(x + w[0]));
|
|
100
|
+
w = weak(at + 32, u(z + w[1]), y);
|
|
101
|
+
[z, x] = [x, z];
|
|
102
|
+
at += 64;
|
|
103
|
+
};
|
|
104
|
+
do {
|
|
105
|
+
round();
|
|
106
|
+
round();
|
|
107
|
+
length -= 128;
|
|
108
|
+
} while (length >= 128);
|
|
109
|
+
y = u(y + rotate(w[0], 37) * k0 + z);
|
|
110
|
+
x = u(x + rotate(u(v[0] + z), 49) * k0);
|
|
111
|
+
for (let done = 0; done < length;) {
|
|
112
|
+
done += 32;
|
|
113
|
+
y = u(rotate(u(y - x), 42) * k0 + v[1]);
|
|
114
|
+
w[0] = u(w[0] + fetch(at + length - done + 16));
|
|
115
|
+
x = u(rotate(x, 49) * k0 + w[0]);
|
|
116
|
+
w[0] = u(w[0] + v[0]);
|
|
117
|
+
v = weak(at + length - done, v[0], v[1]);
|
|
118
|
+
}
|
|
119
|
+
x = hash16(x, v[0]);
|
|
120
|
+
y = hash16(y, w[0]);
|
|
121
|
+
return [u(hash16(u(x + v[1]), w[1]) + y), hash16(u(x + w[1]), u(y + v[1]))];
|
|
122
|
+
};
|
|
123
|
+
const length = bytes.length;
|
|
124
|
+
if (length >= 16)
|
|
125
|
+
return seeded(16, length - 16, [fetch(0) ^ k3, fetch(8)]);
|
|
126
|
+
if (length >= 8)
|
|
127
|
+
return seeded(0, 0, [fetch(0) ^ u(BigInt(length) * k0), fetch(length - 8) ^ k1]);
|
|
128
|
+
return seeded(0, length, [k0, k1]);
|
|
129
|
+
}
|
|
130
|
+
// ClickHouse src/Compression/CompressionInfo.h:7,14 (SHA25614b39ae2…): both limits are1GiB.
|
|
131
|
+
export const MAX_COMPRESSED_BLOCK_SIZE = 0x40000000;
|
|
132
|
+
export const MAX_DECOMPRESSED_BLOCK_SIZE = 0x40000000;
|
|
133
|
+
/** LZ4 block sequences. Validate all lengths before allocating the output buffer. */
|
|
134
|
+
export function decodeLz4Block(input, size) {
|
|
135
|
+
if (!Number.isSafeInteger(size) || size < 0 || size > MAX_DECOMPRESSED_BLOCK_SIZE || input.length > MAX_COMPRESSED_BLOCK_SIZE)
|
|
136
|
+
throw new RangeError('Invalid LZ4 block size');
|
|
137
|
+
// lz4_Block_format.md:114–126 requires a final literal sequence, five final literals
|
|
138
|
+
// (except a short all-literal block), and the last match at least12 bytes before its end.
|
|
139
|
+
// This is the format specification's strictness; ClickHouse's decoder is not retained
|
|
140
|
+
// here and may accept malformed end sequences that this decoder rejects.
|
|
141
|
+
const walk = (output) => {
|
|
142
|
+
let source = 0, target = 0, sequences = 0, finalLiterals = -1, lastMatchStart = -1;
|
|
143
|
+
const length = (initial) => {
|
|
144
|
+
let result = initial;
|
|
145
|
+
if (initial === 15) {
|
|
146
|
+
let value;
|
|
147
|
+
do {
|
|
148
|
+
if (source >= input.length)
|
|
149
|
+
throw new RangeError('Truncated LZ4 length');
|
|
150
|
+
value = input[source++];
|
|
151
|
+
result += value;
|
|
152
|
+
} while (value === 255);
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
};
|
|
156
|
+
while (source < input.length) {
|
|
157
|
+
sequences++;
|
|
158
|
+
const token = input[source++], literals = length(token >> 4);
|
|
159
|
+
if (literals > input.length - source || literals > size - target)
|
|
160
|
+
throw new RangeError('Invalid LZ4 literal range');
|
|
161
|
+
output?.set(input.subarray(source, source + literals), target);
|
|
162
|
+
source += literals;
|
|
163
|
+
target += literals;
|
|
164
|
+
if (source === input.length) {
|
|
165
|
+
finalLiterals = literals;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
if (source + 2 > input.length)
|
|
169
|
+
throw new RangeError('Truncated LZ4 offset');
|
|
170
|
+
const offset = input[source++] | (input[source++] << 8);
|
|
171
|
+
if (!offset || offset > target)
|
|
172
|
+
throw new RangeError('Invalid LZ4 offset');
|
|
173
|
+
const match = length(token & 15) + 4;
|
|
174
|
+
if (match > size - target)
|
|
175
|
+
throw new RangeError('Invalid LZ4 match range');
|
|
176
|
+
lastMatchStart = target;
|
|
177
|
+
if (output)
|
|
178
|
+
for (let i = 0; i < match; i++) {
|
|
179
|
+
output[target] = output[target - offset];
|
|
180
|
+
target++;
|
|
181
|
+
}
|
|
182
|
+
else
|
|
183
|
+
target += match;
|
|
184
|
+
}
|
|
185
|
+
if (finalLiterals < 0 || (size >= 5 && finalLiterals < 5) || (size < 5 && sequences !== 1) || (lastMatchStart >= 0 && size - lastMatchStart < 12))
|
|
186
|
+
throw new RangeError('Invalid LZ4 end sequence');
|
|
187
|
+
if (target !== size)
|
|
188
|
+
throw new RangeError('LZ4 decoded size differs');
|
|
189
|
+
};
|
|
190
|
+
walk();
|
|
191
|
+
const output = new Uint8Array(size);
|
|
192
|
+
walk(output);
|
|
193
|
+
return output;
|
|
194
|
+
}
|
|
195
|
+
/** A valid literal-only LZ4 block. */
|
|
196
|
+
export function encodeLz4Literals(input) {
|
|
197
|
+
if (input.length > MAX_DECOMPRESSED_BLOCK_SIZE)
|
|
198
|
+
throw new RangeError('LZ4 block is too large');
|
|
199
|
+
const extension = input.length >= 15 ? Math.floor((input.length - 15) / 255) + 1 : 0;
|
|
200
|
+
const output = new Uint8Array(1 + extension + input.length);
|
|
201
|
+
let at = 0;
|
|
202
|
+
output[at++] = Math.min(input.length, 15) << 4;
|
|
203
|
+
if (extension) {
|
|
204
|
+
let left = input.length - 15;
|
|
205
|
+
while (left >= 255) {
|
|
206
|
+
output[at++] = 255;
|
|
207
|
+
left -= 255;
|
|
208
|
+
}
|
|
209
|
+
output[at++] = left;
|
|
210
|
+
}
|
|
211
|
+
output.set(input, at);
|
|
212
|
+
return output;
|
|
213
|
+
}
|
|
214
|
+
export class ClickHouseCompressionError extends Error {
|
|
215
|
+
code;
|
|
216
|
+
constructor(code, message) {
|
|
217
|
+
super(message);
|
|
218
|
+
this.code = code;
|
|
219
|
+
this.name = 'ClickHouseCompressionError';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** ClickHouse compressed frame, including its sixteen-byte checksum.
|
|
223
|
+
* ICompressionCodec.cpp:62–69,102–116 (SHA2562cfbdbd7…) writes the9-byte header.
|
|
224
|
+
* CompressedReadBufferBase.cpp:193–194,255–256 (SHA2566f5af3a8…) reads low/high
|
|
225
|
+
* little-endian checksum words and verifies the complete header+payload before decompressing.
|
|
226
|
+
*/
|
|
227
|
+
export function encodeFrame(input, method = 0x82) {
|
|
228
|
+
if (!input.length || input.length > MAX_DECOMPRESSED_BLOCK_SIZE)
|
|
229
|
+
throw new ClickHouseCompressionError(39, 'Invalid decompressed block size');
|
|
230
|
+
if (method !== 0x82 && method !== 0x02)
|
|
231
|
+
throw new ClickHouseCompressionError(89, 'Unknown compression method');
|
|
232
|
+
const bodyLength = method === 0x82 ? 1 + (input.length >= 15 ? Math.floor((input.length - 15) / 255) + 1 : 0) + input.length : input.length;
|
|
233
|
+
if (bodyLength + 9 > MAX_COMPRESSED_BLOCK_SIZE)
|
|
234
|
+
throw new ClickHouseCompressionError(39, 'Compressed block is too large');
|
|
235
|
+
const body = method === 0x82 ? encodeLz4Literals(input) : input;
|
|
236
|
+
const output = new Uint8Array(25 + body.length), view = new DataView(output.buffer);
|
|
237
|
+
output[16] = method;
|
|
238
|
+
view.setUint32(17, 9 + body.length, true);
|
|
239
|
+
view.setUint32(21, input.length, true);
|
|
240
|
+
output.set(body, 25);
|
|
241
|
+
const [low, high] = cityHash128(output.subarray(16));
|
|
242
|
+
view.setBigUint64(0, low, true);
|
|
243
|
+
view.setBigUint64(8, high, true);
|
|
244
|
+
return output;
|
|
245
|
+
}
|
|
246
|
+
/** Decode one frame; undefined means the input has not yet supplied the whole frame.
|
|
247
|
+
* Sizes are validated from the header before waiting for its body, and the checksum
|
|
248
|
+
* precedes decompression. bytesRead allows the caller to retain subsequent frames.
|
|
249
|
+
*/
|
|
250
|
+
export function decodeFrame(input) {
|
|
251
|
+
if (input.length < 25)
|
|
252
|
+
return undefined;
|
|
253
|
+
const view = new DataView(input.buffer, input.byteOffset, input.byteLength), method = input[16];
|
|
254
|
+
// CompressedReadBufferBase.cpp:134–138 selects/refuses the codec before header sizes.
|
|
255
|
+
if (method !== 0x82 && method !== 0x02)
|
|
256
|
+
throw new ClickHouseCompressionError(89, 'Unknown compression method');
|
|
257
|
+
const compressed = view.getUint32(17, true), size = view.getUint32(21, true);
|
|
258
|
+
if (compressed > MAX_COMPRESSED_BLOCK_SIZE || size > MAX_DECOMPRESSED_BLOCK_SIZE)
|
|
259
|
+
throw new ClickHouseCompressionError(39, 'Compressed block size exceeds the vendor limit');
|
|
260
|
+
if (compressed < 9 || !size)
|
|
261
|
+
throw new ClickHouseCompressionError(246, 'Corrupt compressed block header');
|
|
262
|
+
const bytesRead = 16 + compressed;
|
|
263
|
+
if (input.length < bytesRead)
|
|
264
|
+
return undefined;
|
|
265
|
+
const frame = input.subarray(16, bytesRead), [low, high] = cityHash128(frame);
|
|
266
|
+
if (low !== view.getBigUint64(0, true) || high !== view.getBigUint64(8, true))
|
|
267
|
+
throw new ClickHouseCompressionError(40, 'Checksum does not match');
|
|
268
|
+
const body = frame.subarray(9);
|
|
269
|
+
if (method === 0x02) {
|
|
270
|
+
if (body.length !== size)
|
|
271
|
+
throw new ClickHouseCompressionError(246, 'Uncompressed block size differs');
|
|
272
|
+
// Own the returned bytes, as the LZ4 branch does.
|
|
273
|
+
return { data: body.slice(), bytesRead };
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
return { data: decodeLz4Block(body, size), bytesRead };
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
throw new ClickHouseCompressionError(246, error instanceof Error ? error.message : 'Invalid LZ4 block');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -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
|
}
|
|
@@ -992,6 +992,17 @@ export type SemanticsContext = {
|
|
|
992
992
|
/** Decide an update from the current live stored row under the action lock; undefined leaves it unchanged. */
|
|
993
993
|
change(resource: string, id: string, decide: (row: Record<string, unknown>) => Record<string, unknown> | undefined, operation: string): Promise<Record<string, unknown> | undefined>;
|
|
994
994
|
write(resource: string, id: string, fields: Record<string, unknown>, operation: string): Promise<Record<string, unknown>>;
|
|
995
|
+
/**
|
|
996
|
+
* One command's writes as one group (S2 R17; docs/contributing/architecture.md, "A command's writes as one group").
|
|
997
|
+
* Inside `fn`, `write` stages its write and answers the row as it will stand; `row` and `rowsRaw` read the tree with
|
|
998
|
+
* the staged writes folded over it, as it will be once they land. When `fn` returns or throws, every staged write
|
|
999
|
+
* lands, in order, as one append; then its value is answered or its error thrown. Every other way to write, or to
|
|
1000
|
+
* wait on a write, lands the staged writes first; any other read of the tree (`get`, `rows`, `tree`, `history`, ...)
|
|
1001
|
+
* while writes are staged is refused, never answered without them. Each write keeps its own action, id, ordinal,
|
|
1002
|
+
* moment and recorded request. Where the pack has write hooks, events, sockets or counts, a state observer is on,
|
|
1003
|
+
* a head performs, or ids are aliased, `fn` simply runs, each write landing as it is made.
|
|
1004
|
+
*/
|
|
1005
|
+
batch<T>(fn: () => Promise<T>): Promise<T>;
|
|
995
1006
|
/** Add numeric deltas to an existing bookkeeping row under the kernel's atomic write seam. */
|
|
996
1007
|
accumulate(resource: string, id: string, deltas: Record<string, number>, operation: string): Promise<void>;
|
|
997
1008
|
/** `write`, with what a live vendor answered when the head performed it */
|
|
@@ -1141,7 +1152,7 @@ export type HandlerContext = Omit<SemanticsContext, 'now' | 'core' | 'atomically
|
|
|
1141
1152
|
};
|
|
1142
1153
|
/** What a write hook reads: the writing call's context without its ways to write, so a hook that renders an event
|
|
1143
1154
|
* cannot write again (and run itself again). */
|
|
1144
|
-
export type WriteHookContext = Omit<HandlerContext, 'write' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
|
|
1155
|
+
export type WriteHookContext = Omit<HandlerContext, 'write' | 'batch' | 'create' | 'createUnique' | 'change' | 'issue' | 'record' | 'accumulate' | 'at' | 'over'>;
|
|
1145
1156
|
/** A derived pack's handler: one operation, by its operationId, over the contract's context. */
|
|
1146
1157
|
export type Handler = (ctx: HandlerContext) => Promise<Response>;
|
|
1147
1158
|
/** The context the kernel opens for a call (a handler's, a door's, a screen's). Kernel-internal: packs are given it. */
|
package/dist/src/derived-core.js
CHANGED
|
@@ -5,8 +5,9 @@ import { parseExactJson, compareExactNumbers, exactInteger, isExactIntegerValue
|
|
|
5
5
|
// how lists page, how errors read, which fields are state and how they move). From those two this
|
|
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
|
-
import { applyTwinWrite, applyTwinWriteAtomic, twinResources } from "./serve.js";
|
|
9
|
-
import { copyResource, ownFields, parentStamp, subjectHistory, treeChangesSince, treeStamp } from "./log.js";
|
|
8
|
+
import { applyTwinWrite, applyTwinWriteAtomic, applyTwinWrites, twinResources } from "./serve.js";
|
|
9
|
+
import { copyResource, foldLocalWrite, heldSubject, ownFields, parentStamp, subjectHistory, treeChangesSince, treeResources, treeStamp } from "./log.js";
|
|
10
|
+
import { performsAtHeadNow } from "./head.js";
|
|
10
11
|
import { getActiveWorldStore } from "./world-store.js";
|
|
11
12
|
import { getActiveBlobStore } from "./blob-store.js";
|
|
12
13
|
import { worldPaths } from "./storage.js";
|
|
@@ -23,6 +24,7 @@ import { handlerCrypto, hmac, ksuidFrom, lettersFrom, sha256, uuidFrom } from ".
|
|
|
23
24
|
import { hashFieldValue } from "./hash.js";
|
|
24
25
|
import { readResourceBlob, removeResourceBlob, writeResourceBlob } from "./resource-blob.js";
|
|
25
26
|
import { managedDatabase, unboundDatabase } from "./managed-database.js";
|
|
27
|
+
import { redisServiceTimes } from "./redis/service-times.js";
|
|
26
28
|
import { execRedisRun } from "./redis/engine.js";
|
|
27
29
|
import { sendMail } from "./smtp.js";
|
|
28
30
|
import { ownField, parseBracketForm } from "./form.js";
|
|
@@ -263,14 +265,16 @@ export function resourcesOfType(service, type, root) {
|
|
|
263
265
|
}
|
|
264
266
|
held.stamp = delta.stamp;
|
|
265
267
|
}
|
|
266
|
-
else
|
|
267
|
-
// the stamp is read before the fold, so a write between them can only leave a newer fold under an older stamp
|
|
268
|
+
else {
|
|
269
|
+
// the stamp is read once, before the fold, so a write between them can only leave a newer fold under an older stamp
|
|
268
270
|
const stamp = treeStamp(service, root);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
(
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
if (!held || held.stamp !== stamp) {
|
|
272
|
+
const byType = new Map();
|
|
273
|
+
for (const r of twinResources(service, root))
|
|
274
|
+
(byType.get(r.type) ?? byType.set(r.type, new Map()).get(r.type)).set(`${r.type}:${r.id}`, r);
|
|
275
|
+
held = { stamp, byType };
|
|
276
|
+
indexes.set(key, held);
|
|
277
|
+
}
|
|
274
278
|
}
|
|
275
279
|
return [...(held.byType.get(type)?.values() ?? [])].map(copyResource);
|
|
276
280
|
}
|
|
@@ -1336,7 +1340,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1336
1340
|
rawResources.set(row, name ?? resource);
|
|
1337
1341
|
return row;
|
|
1338
1342
|
};
|
|
1339
|
-
|
|
1343
|
+
const context = {
|
|
1340
1344
|
call,
|
|
1341
1345
|
params,
|
|
1342
1346
|
id: Object.values(call.params).at(-1),
|
|
@@ -1563,12 +1567,142 @@ export async function contextFor(m, call, scope) {
|
|
|
1563
1567
|
over: (other) => contextFor(other, { ...asAsked, request: asAsked.request.clone() }, { ...scope, clock: () => at }),
|
|
1564
1568
|
core: async () => { const out = await serveCore(m, { ...call, request: pristine.clone() }, { ...scope, clock: () => at }); return 'served' in out ? out.served : undefined; },
|
|
1565
1569
|
own: (row) => ownFields(row),
|
|
1570
|
+
batch: (fn) => fn(),
|
|
1571
|
+
};
|
|
1572
|
+
if (batchable(m, root))
|
|
1573
|
+
bindBatch(m, call, context, params, root, at, rawRow);
|
|
1574
|
+
return context;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Whether a call's writes may be grouped (`ctx.batch`) with nothing about them changing: every effect a write has
|
|
1578
|
+
* besides its action and the tree (write hooks, events, sockets, a parent's counts, a state observer, a head that
|
|
1579
|
+
* performs it, an alias a read resolves through) is one the grouped path would move, so where any is present the
|
|
1580
|
+
* writes land one at a time, as they always have.
|
|
1581
|
+
*/
|
|
1582
|
+
function batchable(m, root) {
|
|
1583
|
+
return !m.onWrite && !m.events && !m.sockets?.length && !transitionObserver
|
|
1584
|
+
&& !Object.values(m.resources).some((decl) => decl.counts && Object.keys(decl.counts).length > 0)
|
|
1585
|
+
&& !performsAtHeadNow(m.service, root) && subjectAliases(m.service, root).size === 0;
|
|
1586
|
+
}
|
|
1587
|
+
/** The members of a context that read the tree other than `row`/`rowsRaw`: refused while writes are staged. */
|
|
1588
|
+
const BATCH_REFUSED_READS = ['get', 'rows', 'tree', 'history', 'find', 'list', 'expand', 'mint', 'conflict', 'ownerRow', 'render'];
|
|
1589
|
+
/** The members that write, or wait on writes, or answer as another context: each lands the staged writes first. */
|
|
1590
|
+
const BATCH_LANDING_MEMBERS = ['create', 'createUnique', 'change', 'accumulate', 'writeDetailed', 'record', 'atomically', 'remove', 'issue', 'at', 'over', 'core', 'awaitWrite', 'awaitCondition',
|
|
1591
|
+
// a World's seed and signing keys are written when first asked (worldSeed, worldSigningKey, as a vendor's move)
|
|
1592
|
+
'secret', 'signingKey'];
|
|
1593
|
+
/** `ctx.batch` over a context whose writes may be grouped (see `batchable`). */
|
|
1594
|
+
function bindBatch(m, call, context, params, root, at, rawRow) {
|
|
1595
|
+
let staged;
|
|
1596
|
+
// each staged subject as it will stand, by `type:id`, in the order first written
|
|
1597
|
+
const subjects = new Map();
|
|
1598
|
+
const land = async () => {
|
|
1599
|
+
if (!staged?.length)
|
|
1600
|
+
return;
|
|
1601
|
+
const writes = staged;
|
|
1602
|
+
staged = [];
|
|
1603
|
+
subjects.clear();
|
|
1604
|
+
await writeGroup(m, call, writes, params, root, at);
|
|
1605
|
+
};
|
|
1606
|
+
const ctx = context;
|
|
1607
|
+
for (const name of BATCH_LANDING_MEMBERS) {
|
|
1608
|
+
const plain = ctx[name];
|
|
1609
|
+
if (typeof plain !== 'function')
|
|
1610
|
+
continue;
|
|
1611
|
+
ctx[name] = async (...args) => { await land(); return plain(...args); };
|
|
1612
|
+
}
|
|
1613
|
+
for (const name of BATCH_REFUSED_READS) {
|
|
1614
|
+
const plain = ctx[name];
|
|
1615
|
+
if (typeof plain !== 'function')
|
|
1616
|
+
continue;
|
|
1617
|
+
ctx[name] = (...args) => {
|
|
1618
|
+
if (staged?.length)
|
|
1619
|
+
throw new Error(`ctx.${name} reads the tree while ctx.batch holds staged writes: only row and rowsRaw see them`);
|
|
1620
|
+
return plain(...args);
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
const plainWrite = context.write, plainRow = context.row, plainRowsRaw = context.rowsRaw;
|
|
1624
|
+
context.write = async (resource, id, fields, operation) => {
|
|
1625
|
+
// a vendor's own move (ctx.asVendor) is the clock's write, recorded as the system's: it lands alone, in its place
|
|
1626
|
+
if (!staged || inVendorMove()) {
|
|
1627
|
+
await land();
|
|
1628
|
+
return plainWrite(resource, id, fields, operation);
|
|
1629
|
+
}
|
|
1630
|
+
const type = storedType(m, resource), key = `${type}:${id}`;
|
|
1631
|
+
// the one subject, as the memoized tree holds it (readTreeMap would copy the whole tree on every staged write)
|
|
1632
|
+
const subject = foldLocalWrite(subjects.get(key) ?? heldSubject(m.service, type, id, root), type, id, fields, at);
|
|
1633
|
+
subjects.set(key, subject);
|
|
1634
|
+
staged.push({ resource, id, fields, operation });
|
|
1635
|
+
return view(m, resource, treeResources(new Map([[key, subject]]))[0]);
|
|
1636
|
+
};
|
|
1637
|
+
// the stored rows of a resource's type with the staged subjects folded over them, filtered as `stored` filters
|
|
1638
|
+
const overlaid = (resource, opts) => {
|
|
1639
|
+
if (!staged?.length)
|
|
1640
|
+
return undefined;
|
|
1641
|
+
const type = storedType(m, resource), prefix = `${type}:`;
|
|
1642
|
+
const mine = [...subjects].filter(([key]) => key.startsWith(prefix));
|
|
1643
|
+
if (!mine.length)
|
|
1644
|
+
return undefined;
|
|
1645
|
+
const replaced = new Map(mine.map(([key, subject]) => [key, treeResources(new Map([[key, subject]]))[0]]));
|
|
1646
|
+
const base = resourcesOfType(m.service, type, root);
|
|
1647
|
+
// a subject the tree holds stays where it stands; one new to it goes to the end, in the order first written
|
|
1648
|
+
const rows = base.map((row) => replaced.get(`${prefix}${row.id}`) ?? row);
|
|
1649
|
+
const held = new Set(base.map((row) => `${prefix}${row.id}`));
|
|
1650
|
+
for (const [key, row] of replaced)
|
|
1651
|
+
if (!held.has(key))
|
|
1652
|
+
rows.push(row);
|
|
1653
|
+
const when = Object.entries(m.resources[resource]?.readableWhen ?? {});
|
|
1654
|
+
return rows.filter((r) => (opts?.withDeleted || r.deleted !== true) && when.every(([k, v]) => r[k] === v));
|
|
1655
|
+
};
|
|
1656
|
+
context.row = (resource, id, opts) => {
|
|
1657
|
+
const rows = overlaid(resource, opts);
|
|
1658
|
+
if (!rows)
|
|
1659
|
+
return plainRow(resource, id, opts);
|
|
1660
|
+
const row = rows.find((r) => r.id === id);
|
|
1661
|
+
return row ? rawRow(resource, row) : undefined;
|
|
1566
1662
|
};
|
|
1663
|
+
context.rowsRaw = (resource, opts) => {
|
|
1664
|
+
const rows = overlaid(resource, opts);
|
|
1665
|
+
return rows ? rows.map((row) => rawRow(resource, row)) : plainRowsRaw(resource, opts);
|
|
1666
|
+
};
|
|
1667
|
+
context.batch = async (fn) => {
|
|
1668
|
+
// a batch inside a batch is the outer one's
|
|
1669
|
+
if (staged !== undefined)
|
|
1670
|
+
return fn();
|
|
1671
|
+
staged = [];
|
|
1672
|
+
try {
|
|
1673
|
+
return await fn();
|
|
1674
|
+
}
|
|
1675
|
+
finally {
|
|
1676
|
+
// every write `write` answered lands, whether fn returned or threw: an ordered insert's prefix stays written
|
|
1677
|
+
try {
|
|
1678
|
+
await land();
|
|
1679
|
+
}
|
|
1680
|
+
finally {
|
|
1681
|
+
staged = undefined;
|
|
1682
|
+
subjects.clear();
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* A command's staged writes landed as one group: each recorded with the request as `writeDetailed` records it (one
|
|
1689
|
+
* recordedInput, the same for every write of the call), the call's moment and actor, in order, under one lock and one
|
|
1690
|
+
* append (serve.ts applyTwinWrites); then a held read on each written type is told, as each write tells it.
|
|
1691
|
+
*/
|
|
1692
|
+
async function writeGroup(m, call, writes, params, root, occurredAt) {
|
|
1693
|
+
const input = recordedInput(m, call, params);
|
|
1694
|
+
const files = input.operationId ? await keptFiles(m, call, root) : undefined;
|
|
1695
|
+
const recorded = files ? { ...input, files } : input;
|
|
1696
|
+
const actor = { kind: 'agent', ...(m.identity ? { id: m.identity(call.request.headers.get('authorization'), root) } : {}) };
|
|
1697
|
+
await applyTwinWrites(m.service, writes.map((write) => ({ operation: write.operation, subjectType: storedType(m, write.resource),
|
|
1698
|
+
subjectId: write.id, fields: write.fields, input: recorded, occurredAt, actor })), root);
|
|
1699
|
+
for (const type of new Set(writes.map((write) => storedType(m, write.resource))))
|
|
1700
|
+
heldReadsWrite(m.service, root, type);
|
|
1567
1701
|
}
|
|
1568
1702
|
/** A pack's semantics handlers as the dispatch's handlers. */
|
|
1569
1703
|
export function bindSemantics(m, handlers, scope = {}) {
|
|
1570
1704
|
prepareViewOmissions(m);
|
|
1571
|
-
return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) =>
|
|
1705
|
+
return Object.fromEntries(Object.entries(handlers).map(([id, h]) => [id, async (call) => { const ctx = await contextFor(m, call, scope); redisServiceTimes.lap('6c the handler\'s own context opened'); return h(ctx); }]));
|
|
1572
1706
|
}
|
|
1573
1707
|
/** The derived core as the dispatch's core: it owns every operation on a resource the manifest declares. */
|
|
1574
1708
|
export function coreFor(m, scope = {}) {
|
package/dist/src/derived.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseExactJson } from "./exact-json.js";
|
|
2
2
|
import { runWithRequestTrace } from "./trace-context.js";
|
|
3
|
+
import { redisServiceTimes } from "./redis/service-times.js";
|
|
3
4
|
/**
|
|
4
5
|
* A request's body as text, read once per request: the dispatch reads it and tags the request with it, and every clone
|
|
5
6
|
* made from a tagged request carries the tag, so the malformed-JSON guard, the idempotency signature, the handler's
|
|
@@ -169,8 +170,10 @@ export function createDerivedFetch(options) {
|
|
|
169
170
|
for (const observer of dispatchObservers ?? [])
|
|
170
171
|
observer({ surface: options.surface, operation: matched.operation, owner: handlers[matched.operation.id] ? 'handler' : options.core?.owns(matched.operation) ? 'core' : 'gap' });
|
|
171
172
|
// the body read once, as text, for everything the dispatch does with it (bodyTextOf)
|
|
173
|
+
redisServiceTimes.lap('6a around entered, guards and scenario passed, operation matched again');
|
|
172
174
|
if (request.method !== 'GET' && request.method !== 'HEAD' && request.body && !(request.headers.get('content-type') ?? '').includes('multipart'))
|
|
173
175
|
tagBody(request, await request.clone().text());
|
|
176
|
+
redisServiceTimes.lap('6b the dispatch read the body as text');
|
|
174
177
|
// the path prefix's named groups (pack-fetch.ts, `pathPrefix`) are the call's parameters too
|
|
175
178
|
const prefixed = request.headers.get(PREFIX_PARAMS_HEADER);
|
|
176
179
|
const call = { request, operation: matched.operation, params: prefixed ? { ...parseExactJson(prefixed), ...matched.params } : matched.params };
|
package/dist/src/fork.js
CHANGED
|
@@ -56,7 +56,7 @@ export function forkTwin(opts) {
|
|
|
56
56
|
};
|
|
57
57
|
getActiveWorldStore().write(forkMetaPath(service, toRoot), `${JSON.stringify(meta, null, 2)}\n`);
|
|
58
58
|
return meta;
|
|
59
|
-
});
|
|
59
|
+
}, { operation: 'pointer publication' });
|
|
60
60
|
}
|
|
61
61
|
export function readForkMeta(service, root) {
|
|
62
62
|
const path = forkMetaPath(service, root);
|
|
@@ -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;
|