@volter/twin-temporal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +104 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +36 -0
- package/dist/src/generated/hpack.json +1 -0
- package/dist/src/generated/temporal-api.json +1 -0
- package/dist/src/index.d.ts +18 -0
- package/dist/src/index.js +70 -0
- package/dist/src/temporal-budget.d.ts +31 -0
- package/dist/src/temporal-budget.js +50 -0
- package/dist/src/temporal-capabilities.d.ts +7 -0
- package/dist/src/temporal-capabilities.js +650 -0
- package/dist/src/temporal-conformance.d.ts +8 -0
- package/dist/src/temporal-conformance.js +54 -0
- package/dist/src/temporal-connector.d.ts +32 -0
- package/dist/src/temporal-connector.js +121 -0
- package/dist/src/temporal-engine.d.ts +274 -0
- package/dist/src/temporal-engine.js +2132 -0
- package/dist/src/temporal-grpc.d.ts +51 -0
- package/dist/src/temporal-grpc.js +124 -0
- package/dist/src/temporal-h2.d.ts +40 -0
- package/dist/src/temporal-h2.js +644 -0
- package/dist/src/temporal-history.d.ts +27 -0
- package/dist/src/temporal-history.js +85 -0
- package/dist/src/temporal-http.d.ts +17 -0
- package/dist/src/temporal-http.js +160 -0
- package/dist/src/temporal-json.d.ts +7 -0
- package/dist/src/temporal-json.js +68 -0
- package/dist/src/temporal-proto.d.ts +17 -0
- package/dist/src/temporal-proto.js +46 -0
- package/dist/src/temporal-server.d.ts +25 -0
- package/dist/src/temporal-server.js +56 -0
- package/dist/src/temporal-twin.d.ts +25 -0
- package/dist/src/temporal-twin.js +100 -0
- package/dist/src/temporal-visibility.d.ts +49 -0
- package/dist/src/temporal-visibility.js +293 -0
- package/package.json +59 -0
- package/src/cli.ts +36 -0
- package/src/generated/hpack.json +1 -0
- package/src/generated/temporal-api.json +1 -0
- package/src/index.ts +81 -0
- package/src/temporal-budget.ts +75 -0
- package/src/temporal-capabilities.ts +679 -0
- package/src/temporal-conformance.ts +52 -0
- package/src/temporal-connector.ts +118 -0
- package/src/temporal-engine.ts +2176 -0
- package/src/temporal-grpc.ts +139 -0
- package/src/temporal-h2.ts +614 -0
- package/src/temporal-history.ts +99 -0
- package/src/temporal-http.ts +146 -0
- package/src/temporal-json.ts +77 -0
- package/src/temporal-proto.ts +53 -0
- package/src/temporal-server.ts +67 -0
- package/src/temporal-twin.ts +102 -0
- package/src/temporal-visibility.ts +245 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import hpack from './generated/hpack.json' with { type: 'json' };
|
|
2
|
+
const PREFACE = Buffer.from('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n', 'latin1');
|
|
3
|
+
const FRAME = { DATA: 0, HEADERS: 1, PRIORITY: 2, RST_STREAM: 3, SETTINGS: 4, PUSH_PROMISE: 5, PING: 6, GOAWAY: 7, WINDOW_UPDATE: 8, CONTINUATION: 9 };
|
|
4
|
+
const FLAG = { END_STREAM: 0x1, ACK: 0x1, END_HEADERS: 0x4, PADDED: 0x8, PRIORITY: 0x20 };
|
|
5
|
+
const ERROR = { NO_ERROR: 0, PROTOCOL_ERROR: 1, INTERNAL_ERROR: 2, FLOW_CONTROL_ERROR: 3, STREAM_CLOSED: 5, FRAME_SIZE_ERROR: 6, REFUSED_STREAM: 7, CANCEL: 8, COMPRESSION_ERROR: 9 };
|
|
6
|
+
const DEFAULT_WINDOW = 65_535;
|
|
7
|
+
/** what this server grants each stream and the connection to receive */
|
|
8
|
+
const RECEIVE_WINDOW = 16 * 1024 * 1024;
|
|
9
|
+
const MAX_CONCURRENT_STREAMS = 1000;
|
|
10
|
+
/** a request body larger than this resets its stream: a memory bound, not Temporal's message limit (the
|
|
11
|
+
* SDKs' gRPC default is 4 MiB; this server does not enforce that one) */
|
|
12
|
+
const MAX_REQUEST_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
/** a header block (HEADERS + CONTINUATION) larger than this closes the connection */
|
|
14
|
+
const MAX_HEADER_BLOCK = 256 * 1024;
|
|
15
|
+
// ---- HPACK ------------------------------------------------------------------------------------
|
|
16
|
+
const STATIC_TABLE = hpack.staticTable;
|
|
17
|
+
const HUFFMAN_ROOT = (() => {
|
|
18
|
+
const root = {};
|
|
19
|
+
hpack.huffman.forEach(([code, length], symbol) => {
|
|
20
|
+
let node = root;
|
|
21
|
+
for (let bit = length - 1; bit >= 0; bit -= 1) {
|
|
22
|
+
const key = (code >>> bit) & 1 ? 'one' : 'zero';
|
|
23
|
+
node = node[key] ??= {};
|
|
24
|
+
}
|
|
25
|
+
node.symbol = symbol;
|
|
26
|
+
});
|
|
27
|
+
return root;
|
|
28
|
+
})();
|
|
29
|
+
export class HpackError extends Error {
|
|
30
|
+
}
|
|
31
|
+
export function huffmanDecode(bytes) {
|
|
32
|
+
const out = [];
|
|
33
|
+
let node = HUFFMAN_ROOT;
|
|
34
|
+
let pending = 0; // bits consumed since the last symbol
|
|
35
|
+
let allOnes = true;
|
|
36
|
+
for (const byte of bytes) {
|
|
37
|
+
for (let bit = 7; bit >= 0; bit -= 1) {
|
|
38
|
+
const one = (byte >> bit) & 1;
|
|
39
|
+
const next = one ? node.one : node.zero;
|
|
40
|
+
if (!next)
|
|
41
|
+
throw new HpackError('invalid huffman code');
|
|
42
|
+
pending += 1;
|
|
43
|
+
allOnes &&= one === 1;
|
|
44
|
+
if (next.symbol !== undefined) {
|
|
45
|
+
if (next.symbol === 256)
|
|
46
|
+
throw new HpackError('EOS symbol inside a huffman string'); // §5.2
|
|
47
|
+
out.push(next.symbol);
|
|
48
|
+
node = HUFFMAN_ROOT;
|
|
49
|
+
pending = 0;
|
|
50
|
+
allOnes = true;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
node = next;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// §5.2: padding is at most 7 bits and is the most significant bits of EOS (all ones)
|
|
58
|
+
if (pending > 7 || !allOnes)
|
|
59
|
+
throw new HpackError('invalid huffman padding');
|
|
60
|
+
return Buffer.from(out).toString('latin1');
|
|
61
|
+
}
|
|
62
|
+
export class HpackDecoder {
|
|
63
|
+
table = [];
|
|
64
|
+
size = 0;
|
|
65
|
+
maxSize;
|
|
66
|
+
/** the SETTINGS_HEADER_TABLE_SIZE this endpoint advertised (the decoder's ceiling; 4096 is the
|
|
67
|
+
* protocol default, which this server never changes) */
|
|
68
|
+
limit;
|
|
69
|
+
constructor(headerTableSize = 4096) {
|
|
70
|
+
this.maxSize = headerTableSize;
|
|
71
|
+
this.limit = headerTableSize;
|
|
72
|
+
}
|
|
73
|
+
decode(block) {
|
|
74
|
+
const headers = [];
|
|
75
|
+
let offset = 0;
|
|
76
|
+
const integer = (prefixBits) => {
|
|
77
|
+
const mask = (1 << prefixBits) - 1;
|
|
78
|
+
let value = block[offset] & mask;
|
|
79
|
+
offset += 1;
|
|
80
|
+
if (value < mask)
|
|
81
|
+
return value;
|
|
82
|
+
let shift = 0;
|
|
83
|
+
for (;;) {
|
|
84
|
+
if (offset >= block.length)
|
|
85
|
+
throw new HpackError('truncated integer');
|
|
86
|
+
const byte = block[offset];
|
|
87
|
+
offset += 1;
|
|
88
|
+
value += (byte & 0x7f) * 2 ** shift;
|
|
89
|
+
shift += 7;
|
|
90
|
+
if ((byte & 0x80) === 0)
|
|
91
|
+
return value;
|
|
92
|
+
if (shift > 28)
|
|
93
|
+
throw new HpackError('integer overflow');
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const string = () => {
|
|
97
|
+
if (offset >= block.length)
|
|
98
|
+
throw new HpackError('truncated string');
|
|
99
|
+
const huffman = (block[offset] & 0x80) !== 0;
|
|
100
|
+
const length = integer(7);
|
|
101
|
+
if (offset + length > block.length)
|
|
102
|
+
throw new HpackError('truncated string');
|
|
103
|
+
const bytes = block.subarray(offset, offset + length);
|
|
104
|
+
offset += length;
|
|
105
|
+
return huffman ? huffmanDecode(bytes) : Buffer.from(bytes).toString('latin1');
|
|
106
|
+
};
|
|
107
|
+
const entry = (index) => {
|
|
108
|
+
if (index <= 0)
|
|
109
|
+
throw new HpackError('index 0');
|
|
110
|
+
if (index <= STATIC_TABLE.length)
|
|
111
|
+
return STATIC_TABLE[index - 1];
|
|
112
|
+
const dynamic = this.table[index - STATIC_TABLE.length - 1];
|
|
113
|
+
if (!dynamic)
|
|
114
|
+
throw new HpackError(`index ${index} is beyond the dynamic table`);
|
|
115
|
+
return dynamic;
|
|
116
|
+
};
|
|
117
|
+
while (offset < block.length) {
|
|
118
|
+
const byte = block[offset];
|
|
119
|
+
if (byte & 0x80) { // §6.1 indexed
|
|
120
|
+
headers.push(entry(integer(7)));
|
|
121
|
+
}
|
|
122
|
+
else if (byte & 0x40) { // §6.2.1 literal with incremental indexing
|
|
123
|
+
const index = integer(6);
|
|
124
|
+
const name = index === 0 ? string() : entry(index)[0];
|
|
125
|
+
const value = string();
|
|
126
|
+
headers.push([name, value]);
|
|
127
|
+
this.insert(name, value);
|
|
128
|
+
}
|
|
129
|
+
else if (byte & 0x20) { // §6.3 dynamic table size update
|
|
130
|
+
const size = integer(5);
|
|
131
|
+
if (size > this.limit)
|
|
132
|
+
throw new HpackError('table size update above the advertised limit');
|
|
133
|
+
this.maxSize = size;
|
|
134
|
+
this.evict();
|
|
135
|
+
}
|
|
136
|
+
else { // §6.2.2 without indexing / §6.2.3 never indexed
|
|
137
|
+
const index = integer(4);
|
|
138
|
+
const name = index === 0 ? string() : entry(index)[0];
|
|
139
|
+
headers.push([name, string()]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return headers;
|
|
143
|
+
}
|
|
144
|
+
insert(name, value) {
|
|
145
|
+
this.table.unshift([name, value]);
|
|
146
|
+
this.size += name.length + value.length + 32;
|
|
147
|
+
this.evict();
|
|
148
|
+
}
|
|
149
|
+
evict() {
|
|
150
|
+
while (this.size > this.maxSize && this.table.length > 0) {
|
|
151
|
+
const [name, value] = this.table.pop();
|
|
152
|
+
this.size -= name.length + value.length + 32;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function hpackInteger(value, prefixBits, firstByte) {
|
|
157
|
+
const mask = (1 << prefixBits) - 1;
|
|
158
|
+
if (value < mask)
|
|
159
|
+
return [firstByte | value];
|
|
160
|
+
const out = [firstByte | mask];
|
|
161
|
+
let rest = value - mask;
|
|
162
|
+
while (rest >= 128) {
|
|
163
|
+
out.push((rest % 128) + 128);
|
|
164
|
+
rest = Math.floor(rest / 128);
|
|
165
|
+
}
|
|
166
|
+
out.push(rest);
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
/** Every header as a literal without indexing, new name, no huffman (RFC 7541 §6.2.2). */
|
|
170
|
+
export function hpackEncode(headers) {
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const [name, value] of headers) {
|
|
173
|
+
const nameBytes = Buffer.from(name.toLowerCase(), 'latin1');
|
|
174
|
+
const valueBytes = Buffer.from(value, 'latin1');
|
|
175
|
+
out.push(0x00, ...hpackInteger(nameBytes.length, 7, 0), ...nameBytes, ...hpackInteger(valueBytes.length, 7, 0), ...valueBytes);
|
|
176
|
+
}
|
|
177
|
+
return Uint8Array.from(out);
|
|
178
|
+
}
|
|
179
|
+
function frameHeader(length, type, flags, streamId) {
|
|
180
|
+
const header = Buffer.alloc(9);
|
|
181
|
+
header.writeUIntBE(length, 0, 3);
|
|
182
|
+
header[3] = type;
|
|
183
|
+
header[4] = flags;
|
|
184
|
+
header.writeUInt32BE(streamId & 0x7fffffff, 5);
|
|
185
|
+
return header;
|
|
186
|
+
}
|
|
187
|
+
class H2Connection {
|
|
188
|
+
buffer = Buffer.alloc(0);
|
|
189
|
+
prefaceSeen = false;
|
|
190
|
+
decoder = new HpackDecoder();
|
|
191
|
+
streams = new Map();
|
|
192
|
+
continuing = null;
|
|
193
|
+
lastStreamId = 0;
|
|
194
|
+
peerInitialWindow = DEFAULT_WINDOW;
|
|
195
|
+
peerMaxFrame = 16_384;
|
|
196
|
+
connectionSendWindow = DEFAULT_WINDOW;
|
|
197
|
+
dead = false;
|
|
198
|
+
socket;
|
|
199
|
+
handler;
|
|
200
|
+
http1;
|
|
201
|
+
http1Mode = false;
|
|
202
|
+
constructor(socket, handler, http1) {
|
|
203
|
+
this.socket = socket;
|
|
204
|
+
this.handler = handler;
|
|
205
|
+
this.http1 = http1;
|
|
206
|
+
socket.on('data', (chunk) => this.receive(chunk));
|
|
207
|
+
socket.on('close', () => this.teardown());
|
|
208
|
+
socket.on('error', () => this.teardown());
|
|
209
|
+
}
|
|
210
|
+
/** The server's connection preface (§3.4), sent once the client's has arrived: no push, a stream
|
|
211
|
+
* ceiling and a large receive window. Waiting for the client's is what lets an HTTP/1 request on
|
|
212
|
+
* the same port be answered as one instead of receiving binary frames first. */
|
|
213
|
+
sendPreface() {
|
|
214
|
+
const settings = Buffer.alloc(18);
|
|
215
|
+
settings.writeUInt16BE(0x2, 0);
|
|
216
|
+
settings.writeUInt32BE(0, 2); // ENABLE_PUSH
|
|
217
|
+
settings.writeUInt16BE(0x3, 6);
|
|
218
|
+
settings.writeUInt32BE(MAX_CONCURRENT_STREAMS, 8);
|
|
219
|
+
settings.writeUInt16BE(0x4, 12);
|
|
220
|
+
settings.writeUInt32BE(RECEIVE_WINDOW, 14);
|
|
221
|
+
this.write(FRAME.SETTINGS, 0, 0, settings);
|
|
222
|
+
this.windowUpdate(0, RECEIVE_WINDOW - DEFAULT_WINDOW);
|
|
223
|
+
}
|
|
224
|
+
/** An HTTP/1.1 request on this port (a World's door probe, a curl of the HTTP API): read one
|
|
225
|
+
* request, answer it through the fetch handler, close. */
|
|
226
|
+
receiveHttp1() {
|
|
227
|
+
const end = this.buffer.indexOf('\r\n\r\n');
|
|
228
|
+
if (end < 0) {
|
|
229
|
+
if (this.buffer.length > 64 * 1024)
|
|
230
|
+
this.teardown();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const head = this.buffer.subarray(0, end).toString('latin1').split('\r\n');
|
|
234
|
+
const [method, target] = head[0].split(' ');
|
|
235
|
+
const headers = new Headers();
|
|
236
|
+
try {
|
|
237
|
+
for (const line of head.slice(1)) {
|
|
238
|
+
const colon = line.indexOf(':');
|
|
239
|
+
if (colon > 0)
|
|
240
|
+
headers.append(line.slice(0, colon).trim(), line.slice(colon + 1).trim());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
this.respondHttp1(new Response('bad header', { status: 400 }));
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (headers.get('transfer-encoding')) {
|
|
248
|
+
this.respondHttp1(new Response('chunked request bodies are not accepted', { status: 411 }));
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const length = Number(headers.get('content-length') ?? 0);
|
|
252
|
+
if (!Number.isFinite(length) || length < 0 || length > MAX_REQUEST_BYTES) {
|
|
253
|
+
this.respondHttp1(new Response('bad content-length', { status: 400 }));
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (this.buffer.length < end + 4 + length)
|
|
257
|
+
return;
|
|
258
|
+
const body = this.buffer.subarray(end + 4, end + 4 + length);
|
|
259
|
+
this.buffer = Buffer.alloc(0);
|
|
260
|
+
this.socket.removeAllListeners('data');
|
|
261
|
+
let request;
|
|
262
|
+
try {
|
|
263
|
+
// the URL is built on loopback, never on the client's Host header
|
|
264
|
+
if (!target?.startsWith('/'))
|
|
265
|
+
throw new Error('request target must be origin-form');
|
|
266
|
+
request = new Request(`http://127.0.0.1${target}`, {
|
|
267
|
+
method: method, headers, ...(method === 'GET' || method === 'HEAD' ? {} : { body: new Uint8Array(body) }),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
this.respondHttp1(new Response('bad request', { status: 400 }));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
void Promise.resolve().then(() => this.http1(request)).then((response) => this.respondHttp1(response), () => this.respondHttp1(new Response('internal error', { status: 500 })));
|
|
275
|
+
}
|
|
276
|
+
respondHttp1(response) {
|
|
277
|
+
void response.arrayBuffer().then((bytes) => {
|
|
278
|
+
if (this.dead)
|
|
279
|
+
return;
|
|
280
|
+
const lines = [`HTTP/1.1 ${response.status} ${response.statusText || 'OK'}`];
|
|
281
|
+
response.headers.forEach((value, key) => { if (key !== 'content-length' && key !== 'connection')
|
|
282
|
+
lines.push(`${key}: ${value}`); });
|
|
283
|
+
lines.push(`content-length: ${bytes.byteLength}`, 'connection: close', '', '');
|
|
284
|
+
this.socket.end(Buffer.concat([Buffer.from(lines.join('\r\n'), 'latin1'), Buffer.from(bytes)]));
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
write(type, flags, streamId, payload = new Uint8Array(0)) {
|
|
288
|
+
if (this.dead)
|
|
289
|
+
return;
|
|
290
|
+
this.socket.write(Buffer.concat([frameHeader(payload.length, type, flags, streamId), payload]));
|
|
291
|
+
}
|
|
292
|
+
windowUpdate(streamId, increment) {
|
|
293
|
+
if (increment <= 0)
|
|
294
|
+
return;
|
|
295
|
+
const payload = Buffer.alloc(4);
|
|
296
|
+
payload.writeUInt32BE(increment & 0x7fffffff, 0);
|
|
297
|
+
this.write(FRAME.WINDOW_UPDATE, 0, streamId, payload);
|
|
298
|
+
}
|
|
299
|
+
goaway(code, message) {
|
|
300
|
+
const payload = Buffer.concat([Buffer.alloc(8), Buffer.from(message, 'utf8')]);
|
|
301
|
+
payload.writeUInt32BE(this.lastStreamId & 0x7fffffff, 0);
|
|
302
|
+
payload.writeUInt32BE(code, 4);
|
|
303
|
+
this.write(FRAME.GOAWAY, 0, 0, payload);
|
|
304
|
+
// end, not destroy: the GOAWAY frame is flushed before the socket closes
|
|
305
|
+
this.dead = true;
|
|
306
|
+
for (const stream of this.streams.values())
|
|
307
|
+
stream.abort.abort();
|
|
308
|
+
this.streams.clear();
|
|
309
|
+
this.socket.end();
|
|
310
|
+
// a peer that never closes its half is cut off after a grace
|
|
311
|
+
setTimeout(() => this.socket.destroy(), 1000).unref?.();
|
|
312
|
+
}
|
|
313
|
+
resetStream(stream, code) {
|
|
314
|
+
const payload = Buffer.alloc(4);
|
|
315
|
+
payload.writeUInt32BE(code, 0);
|
|
316
|
+
this.write(FRAME.RST_STREAM, 0, stream.id, payload);
|
|
317
|
+
this.closeStream(stream);
|
|
318
|
+
}
|
|
319
|
+
closeStream(stream) {
|
|
320
|
+
stream.localClosed = true;
|
|
321
|
+
stream.remoteClosed = true;
|
|
322
|
+
stream.outbound = [];
|
|
323
|
+
stream.abort.abort();
|
|
324
|
+
this.streams.delete(stream.id);
|
|
325
|
+
}
|
|
326
|
+
teardown() {
|
|
327
|
+
if (this.dead) {
|
|
328
|
+
this.socket.destroy();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
this.dead = true;
|
|
332
|
+
for (const stream of this.streams.values())
|
|
333
|
+
stream.abort.abort();
|
|
334
|
+
this.streams.clear();
|
|
335
|
+
this.socket.destroy();
|
|
336
|
+
}
|
|
337
|
+
receive(chunk) {
|
|
338
|
+
if (this.dead)
|
|
339
|
+
return;
|
|
340
|
+
this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;
|
|
341
|
+
if (this.http1Mode) {
|
|
342
|
+
this.receiveHttp1();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!this.prefaceSeen) {
|
|
346
|
+
const seen = this.buffer.subarray(0, Math.min(this.buffer.length, PREFACE.length));
|
|
347
|
+
if (!PREFACE.subarray(0, seen.length).equals(seen)) {
|
|
348
|
+
// not HTTP/2: an HTTP/1 request line is answered by the fetch handler, anything else refused
|
|
349
|
+
const start = this.buffer.subarray(0, 8).toString('latin1');
|
|
350
|
+
if (this.http1 && this.buffer.length < 8 && /^[A-Z]+$/.test(start))
|
|
351
|
+
return; // a method still arriving
|
|
352
|
+
if (this.http1 && /^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS) /.test(start)) {
|
|
353
|
+
this.http1Mode = true;
|
|
354
|
+
this.receiveHttp1();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.socket.end();
|
|
358
|
+
this.teardown();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (this.buffer.length < PREFACE.length)
|
|
362
|
+
return;
|
|
363
|
+
this.prefaceSeen = true;
|
|
364
|
+
this.buffer = this.buffer.subarray(PREFACE.length);
|
|
365
|
+
this.sendPreface();
|
|
366
|
+
}
|
|
367
|
+
while (!this.dead && this.buffer.length >= 9) {
|
|
368
|
+
const length = this.buffer.readUIntBE(0, 3);
|
|
369
|
+
if (length > 16_384) {
|
|
370
|
+
this.goaway(ERROR.FRAME_SIZE_ERROR, 'frame larger than SETTINGS_MAX_FRAME_SIZE');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (this.buffer.length < 9 + length)
|
|
374
|
+
return;
|
|
375
|
+
const type = this.buffer[3];
|
|
376
|
+
const flags = this.buffer[4];
|
|
377
|
+
const streamId = this.buffer.readUInt32BE(5) & 0x7fffffff;
|
|
378
|
+
const payload = this.buffer.subarray(9, 9 + length);
|
|
379
|
+
this.buffer = this.buffer.subarray(9 + length);
|
|
380
|
+
try {
|
|
381
|
+
this.frame(type, flags, streamId, payload);
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
this.goaway(error instanceof HpackError ? ERROR.COMPRESSION_ERROR : ERROR.PROTOCOL_ERROR, error.message);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
frame(type, flags, streamId, payload) {
|
|
390
|
+
if (this.continuing && type !== FRAME.CONTINUATION)
|
|
391
|
+
throw new Error('expected CONTINUATION'); // §6.10
|
|
392
|
+
switch (type) {
|
|
393
|
+
case FRAME.SETTINGS: {
|
|
394
|
+
if (streamId !== 0)
|
|
395
|
+
throw new Error('SETTINGS on a stream');
|
|
396
|
+
if (flags & FLAG.ACK)
|
|
397
|
+
return;
|
|
398
|
+
if (payload.length % 6 !== 0)
|
|
399
|
+
throw new Error('SETTINGS length');
|
|
400
|
+
for (let i = 0; i < payload.length; i += 6) {
|
|
401
|
+
const id = payload.readUInt16BE(i);
|
|
402
|
+
const value = payload.readUInt32BE(i + 2);
|
|
403
|
+
if (id === 0x4) {
|
|
404
|
+
if (value > 0x7fffffff)
|
|
405
|
+
throw new Error('SETTINGS_INITIAL_WINDOW_SIZE too large');
|
|
406
|
+
const delta = value - this.peerInitialWindow;
|
|
407
|
+
this.peerInitialWindow = value;
|
|
408
|
+
for (const stream of this.streams.values())
|
|
409
|
+
stream.sendWindow += delta;
|
|
410
|
+
}
|
|
411
|
+
else if (id === 0x5) {
|
|
412
|
+
if (value < 16_384 || value > 16_777_215)
|
|
413
|
+
throw new Error('SETTINGS_MAX_FRAME_SIZE out of range');
|
|
414
|
+
this.peerMaxFrame = value;
|
|
415
|
+
}
|
|
416
|
+
// HEADER_TABLE_SIZE governs the peer's decoder of OUR blocks, which never index; the
|
|
417
|
+
// rest (push, concurrency, header list size) bound what this server already never does
|
|
418
|
+
}
|
|
419
|
+
this.write(FRAME.SETTINGS, FLAG.ACK, 0);
|
|
420
|
+
this.flush();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
case FRAME.PING: {
|
|
424
|
+
if (payload.length !== 8)
|
|
425
|
+
throw new Error('PING length');
|
|
426
|
+
if (!(flags & FLAG.ACK))
|
|
427
|
+
this.write(FRAME.PING, FLAG.ACK, 0, payload);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
case FRAME.GOAWAY: {
|
|
431
|
+
this.socket.end();
|
|
432
|
+
this.teardown();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
case FRAME.WINDOW_UPDATE: {
|
|
436
|
+
if (payload.length !== 4)
|
|
437
|
+
throw new Error('WINDOW_UPDATE length');
|
|
438
|
+
const increment = payload.readUInt32BE(0) & 0x7fffffff;
|
|
439
|
+
if (streamId === 0) {
|
|
440
|
+
if (increment === 0)
|
|
441
|
+
throw new Error('WINDOW_UPDATE of 0');
|
|
442
|
+
this.connectionSendWindow += increment;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
const stream = this.streams.get(streamId);
|
|
446
|
+
if (stream)
|
|
447
|
+
stream.sendWindow += increment;
|
|
448
|
+
}
|
|
449
|
+
this.flush();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
case FRAME.RST_STREAM: {
|
|
453
|
+
const stream = this.streams.get(streamId);
|
|
454
|
+
if (stream)
|
|
455
|
+
this.closeStream(stream);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
case FRAME.PRIORITY:
|
|
459
|
+
return;
|
|
460
|
+
case FRAME.PUSH_PROMISE:
|
|
461
|
+
throw new Error('a client never sends PUSH_PROMISE');
|
|
462
|
+
case FRAME.HEADERS: {
|
|
463
|
+
if (streamId === 0 || streamId % 2 === 0)
|
|
464
|
+
throw new Error('HEADERS on an invalid stream id');
|
|
465
|
+
let body = payload;
|
|
466
|
+
let pad = 0;
|
|
467
|
+
if (flags & FLAG.PADDED) {
|
|
468
|
+
pad = body[0];
|
|
469
|
+
body = body.subarray(1);
|
|
470
|
+
}
|
|
471
|
+
if (flags & FLAG.PRIORITY)
|
|
472
|
+
body = body.subarray(5);
|
|
473
|
+
if (pad > body.length)
|
|
474
|
+
throw new Error('padding exceeds HEADERS payload');
|
|
475
|
+
body = body.subarray(0, body.length - pad);
|
|
476
|
+
let stream = this.streams.get(streamId);
|
|
477
|
+
if (!stream) {
|
|
478
|
+
if (streamId <= this.lastStreamId)
|
|
479
|
+
throw new Error('stream id reused'); // §5.1.1
|
|
480
|
+
this.lastStreamId = streamId;
|
|
481
|
+
stream = {
|
|
482
|
+
id: streamId, headerBlock: [], headers: {}, body: [], bodyBytes: 0, remoteClosed: false, localClosed: false,
|
|
483
|
+
sendWindow: this.peerInitialWindow, outbound: [], abort: new AbortController(),
|
|
484
|
+
};
|
|
485
|
+
this.streams.set(streamId, stream);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
// trailers from the client: this server's methods carry none, but the block must still
|
|
489
|
+
// be decoded to keep the HPACK table in step
|
|
490
|
+
stream.headerBlock = [];
|
|
491
|
+
}
|
|
492
|
+
stream.headerBlock.push(body);
|
|
493
|
+
if (stream.dispatched)
|
|
494
|
+
stream.late = true; // decoded for the HPACK table, then refused
|
|
495
|
+
if (flags & FLAG.END_STREAM)
|
|
496
|
+
stream.remoteClosed = true;
|
|
497
|
+
if (flags & FLAG.END_HEADERS)
|
|
498
|
+
this.headersDone(stream);
|
|
499
|
+
else
|
|
500
|
+
this.continuing = stream;
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
case FRAME.CONTINUATION: {
|
|
504
|
+
const stream = this.continuing;
|
|
505
|
+
if (!stream || stream.id !== streamId)
|
|
506
|
+
throw new Error('CONTINUATION without HEADERS');
|
|
507
|
+
stream.headerBlock.push(payload);
|
|
508
|
+
if (stream.headerBlock.reduce((n, part) => n + part.length, 0) > MAX_HEADER_BLOCK)
|
|
509
|
+
throw new Error('header block too large');
|
|
510
|
+
if (flags & FLAG.END_HEADERS) {
|
|
511
|
+
this.continuing = null;
|
|
512
|
+
this.headersDone(stream);
|
|
513
|
+
}
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
case FRAME.DATA: {
|
|
517
|
+
let body = payload;
|
|
518
|
+
if (flags & FLAG.PADDED) {
|
|
519
|
+
const pad = body[0];
|
|
520
|
+
if (pad >= body.length)
|
|
521
|
+
throw new Error('padding exceeds DATA payload');
|
|
522
|
+
body = body.subarray(1, body.length - pad);
|
|
523
|
+
}
|
|
524
|
+
// replenish what the frame consumed (padding included), connection and stream
|
|
525
|
+
this.windowUpdate(0, payload.length);
|
|
526
|
+
const stream = this.streams.get(streamId);
|
|
527
|
+
if (!stream || stream.remoteClosed) {
|
|
528
|
+
if (streamId > this.lastStreamId)
|
|
529
|
+
throw new Error('DATA on an idle stream');
|
|
530
|
+
return; // a stream this server already closed or reset
|
|
531
|
+
}
|
|
532
|
+
if (!(flags & FLAG.END_STREAM))
|
|
533
|
+
this.windowUpdate(streamId, payload.length);
|
|
534
|
+
stream.body.push(Buffer.from(body));
|
|
535
|
+
stream.bodyBytes += body.length;
|
|
536
|
+
if (stream.bodyBytes > MAX_REQUEST_BYTES) {
|
|
537
|
+
this.resetStream(stream, ERROR.REFUSED_STREAM);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (flags & FLAG.END_STREAM) {
|
|
541
|
+
stream.remoteClosed = true;
|
|
542
|
+
this.dispatch(stream);
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
default:
|
|
547
|
+
return; // §4.1: unknown frame types are ignored
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
headersDone(stream) {
|
|
551
|
+
const block = Buffer.concat(stream.headerBlock);
|
|
552
|
+
stream.headerBlock = null;
|
|
553
|
+
const decoded = this.decoder.decode(block);
|
|
554
|
+
// the decoded list is bounded too: indexed references re-emit dynamic entries, so a small block
|
|
555
|
+
// can decode to far more than it carries (SETTINGS_MAX_HEADER_LIST_SIZE's concern, RFC 9113 §6.5.2)
|
|
556
|
+
if (decoded.reduce((n, [name, value]) => n + name.length + value.length + 32, 0) > MAX_HEADER_BLOCK)
|
|
557
|
+
throw new HpackError('decoded header list too large');
|
|
558
|
+
// a header block on a stream this server already answers is a stream error (RFC 9113 §5.1,
|
|
559
|
+
// half-closed (remote)): it is never dispatched a second time
|
|
560
|
+
if (stream.late) {
|
|
561
|
+
this.resetStream(stream, ERROR.STREAM_CLOSED);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const first = Object.keys(stream.headers).length === 0;
|
|
565
|
+
if (first)
|
|
566
|
+
for (const [name, value] of decoded)
|
|
567
|
+
stream.headers[name] = stream.headers[name] === undefined ? value : `${stream.headers[name]}, ${value}`;
|
|
568
|
+
if (this.streams.size > MAX_CONCURRENT_STREAMS) {
|
|
569
|
+
this.resetStream(stream, ERROR.REFUSED_STREAM);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (stream.remoteClosed)
|
|
573
|
+
this.dispatch(stream);
|
|
574
|
+
}
|
|
575
|
+
dispatch(stream) {
|
|
576
|
+
if (stream.dispatched)
|
|
577
|
+
return;
|
|
578
|
+
stream.dispatched = true;
|
|
579
|
+
const response = {
|
|
580
|
+
headers: (headers, end = false) => this.enqueue(stream, { kind: 'headers', block: hpackEncode(headers), end }),
|
|
581
|
+
data: (bytes) => this.enqueue(stream, { kind: 'data', bytes, offset: 0 }),
|
|
582
|
+
trailers: (headers) => this.enqueue(stream, { kind: 'headers', block: hpackEncode(headers), end: true }),
|
|
583
|
+
get closed() { return stream.localClosed; },
|
|
584
|
+
};
|
|
585
|
+
try {
|
|
586
|
+
this.handler({ headers: stream.headers, body: Buffer.concat(stream.body), signal: stream.abort.signal }, response);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
this.resetStream(stream, ERROR.INTERNAL_ERROR);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
enqueue(stream, item) {
|
|
593
|
+
if (stream.localClosed || this.dead)
|
|
594
|
+
return;
|
|
595
|
+
stream.outbound.push(item);
|
|
596
|
+
if (item.kind === 'headers' && item.end)
|
|
597
|
+
stream.localClosed = true;
|
|
598
|
+
this.flush();
|
|
599
|
+
}
|
|
600
|
+
flush() {
|
|
601
|
+
for (const stream of [...this.streams.values()]) {
|
|
602
|
+
while (stream.outbound.length > 0 && !this.dead) {
|
|
603
|
+
const item = stream.outbound[0];
|
|
604
|
+
if (item.kind === 'headers') {
|
|
605
|
+
// one HEADERS frame, CONTINUATION for a block larger than the peer's frame size
|
|
606
|
+
let offset = 0;
|
|
607
|
+
const block = item.block;
|
|
608
|
+
const firstLength = Math.min(block.length, this.peerMaxFrame);
|
|
609
|
+
const last = firstLength === block.length;
|
|
610
|
+
this.write(FRAME.HEADERS, (item.end ? FLAG.END_STREAM : 0) | (last ? FLAG.END_HEADERS : 0), stream.id, block.subarray(0, firstLength));
|
|
611
|
+
offset = firstLength;
|
|
612
|
+
while (offset < block.length) {
|
|
613
|
+
const length = Math.min(block.length - offset, this.peerMaxFrame);
|
|
614
|
+
this.write(FRAME.CONTINUATION, offset + length === block.length ? FLAG.END_HEADERS : 0, stream.id, block.subarray(offset, offset + length));
|
|
615
|
+
offset += length;
|
|
616
|
+
}
|
|
617
|
+
stream.outbound.shift();
|
|
618
|
+
if (item.end) {
|
|
619
|
+
this.streams.delete(stream.id);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
const remaining = item.bytes.length - item.offset;
|
|
625
|
+
const allowance = Math.min(remaining, this.peerMaxFrame, this.connectionSendWindow, stream.sendWindow);
|
|
626
|
+
if (remaining > 0 && allowance <= 0)
|
|
627
|
+
break; // wait for WINDOW_UPDATE
|
|
628
|
+
const chunk = item.bytes.subarray(item.offset, item.offset + allowance);
|
|
629
|
+
this.write(FRAME.DATA, 0, stream.id, chunk);
|
|
630
|
+
item.offset += allowance;
|
|
631
|
+
this.connectionSendWindow -= allowance;
|
|
632
|
+
stream.sendWindow -= allowance;
|
|
633
|
+
if (item.offset >= item.bytes.length)
|
|
634
|
+
stream.outbound.shift();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
/** Speak HTTP/2 (h2c with prior knowledge) on an accepted socket; an HTTP/1 request line is handed to
|
|
640
|
+
* `http1` when given. The listener that accepts sockets is the server module's (temporal-server.ts). */
|
|
641
|
+
export function acceptH2Connection(socket, handler, http1) {
|
|
642
|
+
socket.setNoDelay(true);
|
|
643
|
+
return new H2Connection(socket, handler, http1);
|
|
644
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const TEMPORAL_SERVICE = "temporal";
|
|
2
|
+
/** A history event in the engine's plain form (temporal-proto.ts `OBJECT`): `eventId`, `eventTime`,
|
|
3
|
+
* `eventType` and one `<kind>EventAttributes`. */
|
|
4
|
+
export type HistoryEvent = Record<string, unknown> & {
|
|
5
|
+
eventId: number;
|
|
6
|
+
eventType: string;
|
|
7
|
+
};
|
|
8
|
+
/** A stored subject: its fields as written (the kernel's `id`/`type`/`updatedAt` are its own). */
|
|
9
|
+
export type Stored = Record<string, unknown> & {
|
|
10
|
+
id: string;
|
|
11
|
+
};
|
|
12
|
+
export declare class TemporalStore {
|
|
13
|
+
readonly root: string | undefined;
|
|
14
|
+
constructor(root?: string);
|
|
15
|
+
/** Every subject of one type, as its own fields. READ-ONLY: the kernel's memoized tree and parsed
|
|
16
|
+
* log rows share nested objects with what this hands out, so a caller that changes a subject
|
|
17
|
+
* copies it first (the engine's RunTx does, deeply) — editing one in place corrupts the log the
|
|
18
|
+
* kernel digests. */
|
|
19
|
+
list(type: string): Stored[];
|
|
20
|
+
get(type: string, id: string): Stored | undefined;
|
|
21
|
+
/** Write one subject (all of its fields) under `operation`, the RPC or the server act that moved it. */
|
|
22
|
+
put(type: string, id: string, fields: Record<string, unknown>, operation: string, input?: Record<string, unknown>): Promise<void>;
|
|
23
|
+
/** Store a run's new events as one chunk; answers the chunk's key for the subject to name. */
|
|
24
|
+
appendChunk(events: HistoryEvent[]): Promise<string>;
|
|
25
|
+
/** A run's events, in order, from the chunks its subject names. */
|
|
26
|
+
readChunks(keys: string[]): Promise<HistoryEvent[]>;
|
|
27
|
+
}
|