@volter/world-core 3.0.38 → 3.0.39
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/app-route.cjs +26 -25
- package/app-route.d.cts +1 -0
- package/dist/app-route.cjs +26 -25
- package/dist/app-route.d.cts +1 -0
- package/dist/generated/pack-facts.json +409 -352
- package/dist/inject.cjs +474 -95
- package/dist/network-policy.cjs +108 -13
- package/dist/network-policy.d.cts +8 -3
- package/dist/src/actions.js +2 -1
- package/dist/src/anthropic-wire.d.ts +2 -0
- package/dist/src/anthropic-wire.js +5 -0
- package/dist/src/attestation.d.ts +6 -0
- package/dist/src/attestation.js +27 -0
- package/dist/src/bytes.d.ts +3 -0
- package/dist/src/bytes.js +17 -0
- package/dist/src/changeset.js +2 -1
- package/dist/src/clickhouse/sql.js +73 -14
- package/dist/src/derived-core.d.ts +210 -32
- package/dist/src/derived-core.js +510 -244
- package/dist/src/derived-real.d.ts +1 -1
- package/dist/src/derived-real.js +704 -93
- package/dist/src/derived.d.ts +18 -2
- package/dist/src/derived.js +21 -2
- package/dist/src/events.d.ts +28 -45
- package/dist/src/events.js +40 -58
- package/dist/src/exact-json.d.ts +10 -0
- package/dist/src/exact-json.js +98 -0
- package/dist/src/executor.d.ts +17 -0
- package/dist/src/executor.js +91 -30
- package/dist/src/file-response.js +1 -1
- package/dist/src/form.d.ts +8 -0
- package/dist/src/form.js +76 -0
- package/dist/src/git/format.d.ts +16 -0
- package/dist/src/git/format.js +110 -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 +1 -1
- package/dist/src/git/lfs.js +4 -3
- package/dist/src/graphql-wire.d.ts +6 -0
- package/dist/src/graphql-wire.js +6 -3
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/head.d.ts +1 -0
- package/dist/src/head.js +15 -4
- package/dist/src/held-reads.d.ts +6 -0
- package/dist/src/held-reads.js +60 -0
- package/dist/src/history.js +5 -4
- package/dist/src/host-port.d.ts +5 -0
- package/dist/src/host-port.js +14 -0
- package/dist/src/index.d.ts +11 -3
- package/dist/src/index.js +9 -4
- package/dist/src/log.js +12 -7
- package/dist/src/machines.d.ts +81 -5
- package/dist/src/machines.js +125 -9
- package/dist/src/managed-database.d.ts +15 -11
- package/dist/src/managed-database.js +38 -37
- package/dist/src/multipart.d.ts +21 -3
- package/dist/src/multipart.js +112 -38
- package/dist/src/observe.d.ts +3 -0
- package/dist/src/observe.js +6 -2
- package/dist/src/openai-wire.d.ts +16 -0
- package/dist/src/openai-wire.js +43 -12
- package/dist/src/pack-fetch.d.ts +4 -6
- package/dist/src/pack-fetch.js +189 -57
- package/dist/src/packRegistry.d.ts +7 -1
- package/dist/src/packRegistry.js +20 -3
- package/dist/src/private-transport.d.ts +1 -0
- package/dist/src/private-transport.js +11 -0
- package/dist/src/protobuf.js +10 -4
- package/dist/src/rateBudget.d.ts +8 -1
- package/dist/src/rateBudget.js +1 -0
- package/dist/src/redis/engine.d.ts +14 -5
- package/dist/src/redis/engine.js +271 -72
- package/dist/src/redis/frames.d.ts +17 -0
- package/dist/src/redis/frames.js +123 -0
- package/dist/src/redis/index.d.ts +6 -0
- package/dist/src/redis/index.js +6 -0
- package/dist/src/redis/json.d.ts +2 -0
- package/dist/src/redis/json.js +14 -0
- package/dist/src/redis/lua-libs.d.ts +4 -0
- package/dist/src/redis/lua-libs.js +173 -0
- package/dist/src/redis/lua.d.ts +1 -0
- package/dist/src/redis/lua.js +36 -0
- package/dist/src/redis/member-storage.d.ts +5 -0
- package/dist/src/redis/member-storage.js +75 -0
- package/dist/src/redis/resp2.d.ts +4 -0
- package/dist/src/redis/resp2.js +29 -0
- package/dist/src/redis/stream.d.ts +14 -0
- package/dist/src/redis/stream.js +335 -0
- package/dist/src/remote-execute.d.ts +2 -2
- package/dist/src/request-body.d.ts +11 -0
- package/dist/src/request-body.js +11 -0
- package/dist/src/runtime.d.ts +6 -5
- package/dist/src/runtime.js +5 -5
- package/dist/src/s3/wire.d.ts +4 -4
- package/dist/src/s3/wire.js +5 -3
- package/dist/src/scenario.d.ts +22 -0
- package/dist/src/scenario.js +35 -31
- package/dist/src/serve-http.d.ts +2 -1
- package/dist/src/serve-http.js +10 -1
- package/dist/src/serve.js +7 -4
- package/dist/src/signing.d.ts +23 -5
- package/dist/src/signing.js +74 -11
- package/dist/src/sigv4.d.ts +3 -1
- package/dist/src/sigv4.js +4 -2
- package/dist/src/smtp.js +2 -1
- package/dist/src/sockets.js +8 -3
- package/dist/src/state-system.d.ts +3 -0
- package/dist/src/storage.js +5 -2
- package/dist/src/twin-fetch.d.ts +3 -1
- package/dist/src/twin-fetch.js +3 -2
- package/dist/src/vendor-call.d.ts +10 -3
- package/dist/src/vendor-call.js +58 -14
- package/dist/test-fixtures/attestation.SOURCE.md +1 -0
- package/dist/test-fixtures/attestation.json +50 -0
- package/generated/pack-facts.json +409 -352
- package/inject.cjs +474 -95
- package/network-policy.cjs +108 -13
- package/network-policy.d.cts +8 -3
- package/package.json +11 -4
- package/src/actions.ts +2 -1
- package/src/anthropic-wire.ts +6 -0
- package/src/attestation.ts +23 -0
- package/src/bytes.ts +21 -0
- package/src/changeset.ts +2 -1
- package/src/clickhouse/sql.ts +50 -16
- package/src/derived-core.ts +573 -249
- package/src/derived-real.ts +552 -42
- package/src/derived.ts +23 -4
- package/src/events.ts +54 -85
- package/src/exact-json.ts +71 -0
- package/src/executor.ts +86 -27
- package/src/file-response.ts +1 -1
- package/src/form.ts +66 -0
- package/src/git/format.ts +89 -0
- package/src/git/index.ts +1 -0
- package/src/git/lfs.ts +5 -4
- package/src/graphql-wire.ts +8 -3
- package/src/grpc-wire.ts +2 -1
- package/src/head.ts +15 -5
- package/src/held-reads.ts +54 -0
- package/src/history.ts +5 -4
- package/src/host-port.ts +9 -0
- package/src/index.ts +10 -4
- package/src/log.ts +11 -7
- package/src/machines.ts +135 -13
- package/src/managed-database.ts +48 -40
- package/src/multipart.ts +110 -35
- package/src/observe.ts +6 -3
- package/src/openai-wire.ts +45 -14
- package/src/pack-fetch.ts +173 -50
- package/src/packRegistry.ts +27 -4
- package/src/private-transport.ts +8 -0
- package/src/protobuf.ts +8 -4
- package/src/rateBudget.ts +6 -1
- package/src/redis/engine.ts +168 -63
- package/src/redis/frames.ts +53 -0
- package/src/redis/index.ts +6 -0
- package/src/redis/json.ts +13 -0
- package/src/redis/lua-libs.ts +62 -0
- package/src/redis/lua.ts +20 -0
- package/src/redis/member-storage.ts +58 -0
- package/src/redis/resp2.ts +25 -0
- package/src/redis/stream.ts +102 -0
- package/src/remote-execute.ts +2 -2
- package/src/request-body.ts +12 -0
- package/src/runtime.ts +6 -5
- package/src/s3/wire.ts +8 -5
- package/src/scenario.ts +34 -13
- package/src/serve-http.ts +11 -2
- package/src/serve.ts +4 -3
- package/src/signing.ts +71 -17
- package/src/sigv4.ts +5 -3
- package/src/smtp.ts +3 -2
- package/src/sockets.ts +8 -3
- package/src/state-system.ts +3 -0
- package/src/storage.ts +5 -2
- package/src/twin-fetch.ts +4 -3
- package/src/vendor-call.ts +59 -12
- package/test-fixtures/attestation.json +50 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { exactInteger } from "../exact-json.js";
|
|
2
|
+
// RESP frame parsing and RESP2 replies, shared by the Redis stream and journey tooling.
|
|
3
|
+
// https://redis.io/docs/latest/develop/reference/protocol-spec/
|
|
4
|
+
import { RedisInteger, RedisStatus } from "./engine.js";
|
|
5
|
+
export const binaryText = (b) => Array.from(b, (n) => String.fromCharCode(n)).join('');
|
|
6
|
+
export const binaryBytes = (s) => Uint8Array.from(s, (c) => c.charCodeAt(0));
|
|
7
|
+
export function readResp(s, start = 0) {
|
|
8
|
+
const e = s.indexOf('\r\n', start);
|
|
9
|
+
if (e < 0)
|
|
10
|
+
return undefined;
|
|
11
|
+
const tag = s[start], head = s.slice(start + 1, e);
|
|
12
|
+
let at = e + 2;
|
|
13
|
+
if (tag === '+' || tag === '-')
|
|
14
|
+
return { type: tag, value: head, end: at, ...(tag === '-' ? { error: true } : {}) };
|
|
15
|
+
if (tag === ':')
|
|
16
|
+
return { type: tag, value: exactInteger(head), end: at };
|
|
17
|
+
if (tag === '_')
|
|
18
|
+
return { type: tag, value: null, end: at };
|
|
19
|
+
if (tag === '#')
|
|
20
|
+
return { type: tag, value: head === 't', end: at };
|
|
21
|
+
if (tag === ',')
|
|
22
|
+
return { type: tag, value: Number(head), end: at };
|
|
23
|
+
const n = Number(head);
|
|
24
|
+
if (!Number.isSafeInteger(n) || n < -1)
|
|
25
|
+
throw new Error('Protocol error: invalid length');
|
|
26
|
+
if (tag === '$' || tag === '!') {
|
|
27
|
+
if (n === -1)
|
|
28
|
+
return { type: tag, value: null, end: at };
|
|
29
|
+
if (s.length < at + n + 2)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (s.slice(at + n, at + n + 2) !== '\r\n')
|
|
32
|
+
throw new Error('Protocol error: invalid bulk terminator');
|
|
33
|
+
return { type: tag, value: s.slice(at, at + n), end: at + n + 2, ...(tag === '!' ? { error: true } : {}) };
|
|
34
|
+
}
|
|
35
|
+
if (['*', '%', '~', '>'].includes(tag)) {
|
|
36
|
+
if (n === -1)
|
|
37
|
+
return { type: tag, value: null, end: at };
|
|
38
|
+
const out = [];
|
|
39
|
+
for (let i = 0; i < n * (tag === '%' ? 2 : 1); i++) {
|
|
40
|
+
const f = readResp(s, at);
|
|
41
|
+
if (!f)
|
|
42
|
+
return undefined;
|
|
43
|
+
out.push(f.error ? { error: f.value } : f.value);
|
|
44
|
+
at = f.end;
|
|
45
|
+
}
|
|
46
|
+
return { type: tag, value: tag === '%' ? Object.fromEntries(Array.from({ length: n }, (_, i) => [out[i * 2], out[i * 2 + 1]])) : out, end: at };
|
|
47
|
+
}
|
|
48
|
+
throw new Error('Protocol error: invalid type');
|
|
49
|
+
}
|
|
50
|
+
export function respReply(value) {
|
|
51
|
+
if (value instanceof RedisInteger || typeof value === 'object' && value !== null && 'integer' in value)
|
|
52
|
+
return `:${value.integer}\r\n`;
|
|
53
|
+
if (value === null)
|
|
54
|
+
return '$-1\r\n';
|
|
55
|
+
if (value instanceof RedisStatus || typeof value === 'object' && !Array.isArray(value) && 'value' in value)
|
|
56
|
+
return `+${String(value.value).replace(/[\r\n]/g, ' ')}\r\n`;
|
|
57
|
+
if (typeof value === 'object' && !Array.isArray(value) && 'error' in value)
|
|
58
|
+
return `-${String(value.error).trim().replace(/[\r\n]/g, ' ')}\r\n`;
|
|
59
|
+
if (Array.isArray(value))
|
|
60
|
+
return `*${value.length}\r\n${value.map((x) => respReply(x)).join('')}`;
|
|
61
|
+
if (typeof value === 'object')
|
|
62
|
+
return respReply(Object.entries(value).flat());
|
|
63
|
+
if (typeof value === 'number')
|
|
64
|
+
return Number.isInteger(value) ? `:${value}\r\n` : respReply(String(value));
|
|
65
|
+
const s = String(value);
|
|
66
|
+
return `$${s.length}\r\n${s}\r\n`;
|
|
67
|
+
}
|
|
68
|
+
export const commandFrame = (command) => {
|
|
69
|
+
const args = command.map((s) => binaryText(new TextEncoder().encode(s)));
|
|
70
|
+
return binaryBytes(`*${args.length}\r\n${args.map((s) => `$${s.length}\r\n${s}\r\n`).join('')}`);
|
|
71
|
+
};
|
|
72
|
+
export function inlineCommand(line) {
|
|
73
|
+
const words = [];
|
|
74
|
+
let word = '', quote = '', active = false;
|
|
75
|
+
for (let i = 0; i < line.length; i++) {
|
|
76
|
+
const c = line[i];
|
|
77
|
+
if (c === '\\' && quote) {
|
|
78
|
+
const next = line[++i];
|
|
79
|
+
if (next === 'x' && /^[0-9a-f]{2}$/i.test(line.slice(i + 1, i + 3))) {
|
|
80
|
+
word += String.fromCharCode(parseInt(line.slice(i + 1, i + 3), 16));
|
|
81
|
+
i += 2;
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
word += { n: '\n', r: '\r', t: '\t' }[next] ?? next;
|
|
85
|
+
}
|
|
86
|
+
else if (quote) {
|
|
87
|
+
if (c === quote)
|
|
88
|
+
quote = '';
|
|
89
|
+
else
|
|
90
|
+
word += c;
|
|
91
|
+
}
|
|
92
|
+
else if (c === '"' || c === "'") {
|
|
93
|
+
quote = c;
|
|
94
|
+
active = true;
|
|
95
|
+
}
|
|
96
|
+
else if (/\s/.test(c)) {
|
|
97
|
+
if (active) {
|
|
98
|
+
words.push(word);
|
|
99
|
+
word = '';
|
|
100
|
+
active = false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
word += c;
|
|
105
|
+
active = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (quote)
|
|
109
|
+
throw new Error('Protocol error: unbalanced quotes in request');
|
|
110
|
+
if (active)
|
|
111
|
+
words.push(word);
|
|
112
|
+
return words;
|
|
113
|
+
}
|
|
114
|
+
/** Carry RESP integer/status tags across an internal JSON request without losing int64 bytes. */
|
|
115
|
+
export function wireValue(value) {
|
|
116
|
+
if (value instanceof RedisInteger)
|
|
117
|
+
return { integer: value.integer };
|
|
118
|
+
if (Array.isArray(value))
|
|
119
|
+
return value.map(wireValue);
|
|
120
|
+
if (value && typeof value === 'object')
|
|
121
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, wireValue(item)]));
|
|
122
|
+
return value;
|
|
123
|
+
}
|
package/dist/src/redis/index.js
CHANGED
|
@@ -4,3 +4,9 @@
|
|
|
4
4
|
// docs/contributing/architecture.md, "Other wires: lanes, headers and line protocols".
|
|
5
5
|
export * from "./engine.js";
|
|
6
6
|
export * from "./lua.js";
|
|
7
|
+
export * from "./resp2.js";
|
|
8
|
+
export * from "./json.js";
|
|
9
|
+
export * from "./member-storage.js";
|
|
10
|
+
export * from "./lua-libs.js";
|
|
11
|
+
export * from "./frames.js";
|
|
12
|
+
export * from "./stream.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// JSON envelopes for Redis replies. Integer replies carry decimal digits until the wire writes them;
|
|
2
|
+
// converting them to a Number here would round values outside JavaScript's safe integer range.
|
|
3
|
+
import { RedisInteger } from "./engine.js";
|
|
4
|
+
/** Serialize a JSON reply envelope, including nested exact Redis integer numeric tokens. */
|
|
5
|
+
export function jsonReply(value) {
|
|
6
|
+
if (value instanceof RedisInteger)
|
|
7
|
+
return value.integer;
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return `[${value.map((item) => jsonReply(item)).join(',')}]`;
|
|
10
|
+
if (value !== null && typeof value === 'object') {
|
|
11
|
+
return `{${Object.entries(value).filter(([, item]) => item !== undefined).map(([key, item]) => `${JSON.stringify(key)}:${jsonReply(item)}`).join(',')}}`;
|
|
12
|
+
}
|
|
13
|
+
return JSON.stringify(value) ?? 'null';
|
|
14
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Redis's Lua JSON/MessagePack runtime libraries. Values only; no state or vendor names.
|
|
2
|
+
// https://redis.io/docs/latest/develop/programmability/lua-api/ (cjson, cmsgpack, bit)
|
|
3
|
+
import { LuaTable, LuaError, LUA_NULL, luaPattern, numberToLuaString, luaTypeName } from "./lua.js";
|
|
4
|
+
const bytes = (s) => Uint8Array.from(s, (c) => c.charCodeAt(0));
|
|
5
|
+
const text = (b) => Array.from(b, (v) => String.fromCharCode(v)).join('');
|
|
6
|
+
const objectTable = (v) => { const t = new LuaTable(); for (const [k, x] of Object.entries(v))
|
|
7
|
+
t.set(k, from(x)); return t; };
|
|
8
|
+
const from = (v) => v === null ? LUA_NULL : Array.isArray(v) ? LuaTable.fromArray(v.map(from)) : typeof v === 'object' ? objectTable(v) : v;
|
|
9
|
+
const to = (v) => v === LUA_NULL || v === null ? null : v instanceof LuaTable ? v.length > 0 ? v.toArray().map(to) : Object.fromEntries([...v.map.entries()].map(([k, x]) => [String(k), to(x)])) : v;
|
|
10
|
+
export function unpackMessage(s) {
|
|
11
|
+
const b = bytes(s), d = new DataView(b.buffer);
|
|
12
|
+
let i = 0;
|
|
13
|
+
const n = (width, signed = false) => { let v; if (i + width > b.length)
|
|
14
|
+
throw new LuaError('Bad data format in input.'); if (width === 1)
|
|
15
|
+
v = signed ? d.getInt8(i) : d.getUint8(i);
|
|
16
|
+
else if (width === 2)
|
|
17
|
+
v = signed ? d.getInt16(i) : d.getUint16(i);
|
|
18
|
+
else if (width === 4)
|
|
19
|
+
v = signed ? d.getInt32(i) : d.getUint32(i);
|
|
20
|
+
else
|
|
21
|
+
v = Number(signed ? d.getBigInt64(i) : d.getBigUint64(i)); i += width; return v; };
|
|
22
|
+
const string = (len) => { const out = text(b.subarray(i, i + len)); i += len; if (i > b.length)
|
|
23
|
+
throw new LuaError('Bad data format in input.'); return out; };
|
|
24
|
+
const array = (len) => LuaTable.fromArray(Array.from({ length: len }, read));
|
|
25
|
+
const map = (len) => { const t = new LuaTable(); for (let j = 0; j < len; j++)
|
|
26
|
+
t.set(read(), read()); return t; };
|
|
27
|
+
function read() {
|
|
28
|
+
const c = n(1);
|
|
29
|
+
if (c < 128)
|
|
30
|
+
return c;
|
|
31
|
+
if (c >= 224)
|
|
32
|
+
return c - 256;
|
|
33
|
+
if ((c & 0xe0) === 0xa0)
|
|
34
|
+
return string(c & 31);
|
|
35
|
+
if ((c & 0xf0) === 0x90)
|
|
36
|
+
return array(c & 15);
|
|
37
|
+
if ((c & 0xf0) === 0x80)
|
|
38
|
+
return map(c & 15);
|
|
39
|
+
switch (c) {
|
|
40
|
+
case 0xc0: return null;
|
|
41
|
+
case 0xc2: return false;
|
|
42
|
+
case 0xc3: return true;
|
|
43
|
+
case 0xc4:
|
|
44
|
+
case 0xd9: return string(n(1));
|
|
45
|
+
case 0xc5:
|
|
46
|
+
case 0xda: return string(n(2));
|
|
47
|
+
case 0xc6:
|
|
48
|
+
case 0xdb: return string(n(4));
|
|
49
|
+
case 0xcc: return n(1);
|
|
50
|
+
case 0xcd: return n(2);
|
|
51
|
+
case 0xce: return n(4);
|
|
52
|
+
case 0xcf: return n(8);
|
|
53
|
+
case 0xd0: return n(1, true);
|
|
54
|
+
case 0xd1: return n(2, true);
|
|
55
|
+
case 0xd2: return n(4, true);
|
|
56
|
+
case 0xd3: return n(8, true);
|
|
57
|
+
case 0xca: {
|
|
58
|
+
const v = d.getFloat32(i);
|
|
59
|
+
i += 4;
|
|
60
|
+
return v;
|
|
61
|
+
}
|
|
62
|
+
case 0xcb: {
|
|
63
|
+
const v = d.getFloat64(i);
|
|
64
|
+
i += 8;
|
|
65
|
+
return v;
|
|
66
|
+
}
|
|
67
|
+
case 0xdc: return array(n(2));
|
|
68
|
+
case 0xdd: return array(n(4));
|
|
69
|
+
case 0xde: return map(n(2));
|
|
70
|
+
case 0xdf: return map(n(4));
|
|
71
|
+
default: throw new LuaError('Bad data format in input.');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const value = read();
|
|
75
|
+
if (i !== b.length)
|
|
76
|
+
throw new LuaError('Extra bytes in input.');
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
export function packMessage(value) {
|
|
80
|
+
const out = [];
|
|
81
|
+
const len = (tag, size, n) => { out.push(tag); for (let j = size - 1; j >= 0; j--)
|
|
82
|
+
out.push(Math.floor(n / 2 ** (j * 8)) & 255); };
|
|
83
|
+
const put = (v) => {
|
|
84
|
+
if (v === null || v === LUA_NULL) {
|
|
85
|
+
out.push(0xc0);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (typeof v === 'boolean') {
|
|
89
|
+
out.push(v ? 0xc3 : 0xc2);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (typeof v === 'number') {
|
|
93
|
+
if (Number.isInteger(v) && v >= 0 && v < 128 || v >= -32 && v < 0 && Number.isInteger(v))
|
|
94
|
+
out.push(v & 255);
|
|
95
|
+
else {
|
|
96
|
+
const b = new Uint8Array(8);
|
|
97
|
+
new DataView(b.buffer).setFloat64(0, v);
|
|
98
|
+
out.push(0xcb, ...b);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (typeof v === 'string') {
|
|
103
|
+
if (v.length < 32)
|
|
104
|
+
out.push(0xa0 | v.length);
|
|
105
|
+
else
|
|
106
|
+
len(v.length < 256 ? 0xd9 : 0xda, v.length < 256 ? 1 : 2, v.length);
|
|
107
|
+
out.push(...bytes(v));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!(v instanceof LuaTable))
|
|
111
|
+
throw new LuaError('Unsupported type');
|
|
112
|
+
if (v.length > 0) {
|
|
113
|
+
const a = v.toArray();
|
|
114
|
+
if (a.length < 16)
|
|
115
|
+
out.push(0x90 | a.length);
|
|
116
|
+
else
|
|
117
|
+
len(0xdc, 2, a.length);
|
|
118
|
+
a.forEach(put);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
if (v.map.size < 16)
|
|
122
|
+
out.push(0x80 | v.map.size);
|
|
123
|
+
else
|
|
124
|
+
len(0xde, 2, v.map.size);
|
|
125
|
+
for (const [k, x] of v.map) {
|
|
126
|
+
put(k);
|
|
127
|
+
put(x);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
put(value);
|
|
132
|
+
return text(Uint8Array.from(out));
|
|
133
|
+
}
|
|
134
|
+
export const standardLuaLibraries = ({ get, set, commandApi }) => {
|
|
135
|
+
// Lua 5.1 string.match is a selected script runtime capability, not a base-library change.
|
|
136
|
+
// source: https://www.lua.org/manual/5.1/manual.html "Returns the captures from the pattern"
|
|
137
|
+
const string = get('string');
|
|
138
|
+
string.set('match', (a) => {
|
|
139
|
+
const str = (i) => { const v = a[i] ?? null; if (typeof v === 'string')
|
|
140
|
+
return v; if (typeof v === 'number')
|
|
141
|
+
return numberToLuaString(v); throw new LuaError(`bad argument #${i + 1} to 'string.match' (string expected, got ${luaTypeName(v)})`); };
|
|
142
|
+
const value = str(0), pattern = str(1);
|
|
143
|
+
const start = a[2] === undefined ? 0 : Math.max(0, Number(a[2]) - 1);
|
|
144
|
+
const match = new RegExp(luaPattern(pattern)).exec(value.slice(start));
|
|
145
|
+
return match ? match.length > 1 ? match.slice(1) : [match[0]] : [null];
|
|
146
|
+
});
|
|
147
|
+
set('redis', commandApi);
|
|
148
|
+
set('server', commandApi);
|
|
149
|
+
const json = new LuaTable();
|
|
150
|
+
json.set('null', LUA_NULL);
|
|
151
|
+
json.set('encode', (a) => [text(new TextEncoder().encode(JSON.stringify(to(a[0] ?? null))))]);
|
|
152
|
+
json.set('decode', (a) => { try {
|
|
153
|
+
return [from(JSON.parse(new TextDecoder().decode(bytes(String(a[0])))))];
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
throw new LuaError('Expected value but found invalid token');
|
|
157
|
+
} });
|
|
158
|
+
set('cjson', json);
|
|
159
|
+
const msg = new LuaTable();
|
|
160
|
+
msg.set('unpack', (a) => [unpackMessage(String(a[0]))]);
|
|
161
|
+
msg.set('pack', (a) => [packMessage(a[0] ?? null)]);
|
|
162
|
+
set('cmsgpack', msg);
|
|
163
|
+
const bit = new LuaTable();
|
|
164
|
+
const ns = (a) => a.map(Number);
|
|
165
|
+
bit.set('band', (a) => [ns(a).reduce((x, y) => x & y, -1)]);
|
|
166
|
+
bit.set('bor', (a) => [ns(a).reduce((x, y) => x | y, 0)]);
|
|
167
|
+
bit.set('bxor', (a) => [ns(a).reduce((x, y) => x ^ y, 0)]);
|
|
168
|
+
bit.set('bnot', (a) => [~Number(a[0])]);
|
|
169
|
+
bit.set('lshift', (a) => [Number(a[0]) << Number(a[1])]);
|
|
170
|
+
bit.set('rshift', (a) => [Number(a[0]) >>> Number(a[1])]);
|
|
171
|
+
bit.set('arshift', (a) => [Number(a[0]) >> Number(a[1])]);
|
|
172
|
+
set('bit', bit);
|
|
173
|
+
};
|
package/dist/src/redis/lua.d.ts
CHANGED
|
@@ -121,6 +121,7 @@ type Stat = {
|
|
|
121
121
|
};
|
|
122
122
|
/** Parse Lua source into a chunk. Exported so a test can prove a bad script fails at PARSE time. */
|
|
123
123
|
export declare function parseLua(src: string): Stat[];
|
|
124
|
+
export declare function luaPattern(pattern: string): string;
|
|
124
125
|
export declare function luaTruthy(v: LuaValue): boolean;
|
|
125
126
|
export declare function luaTypeName(v: LuaValue): string;
|
|
126
127
|
/** Lua 5.1's `%.14g` number formatting — `3` prints as "3", not "3.0". */
|
package/dist/src/redis/lua.js
CHANGED
|
@@ -604,6 +604,42 @@ class Scope {
|
|
|
604
604
|
}
|
|
605
605
|
}
|
|
606
606
|
const BREAK = Symbol('lua-break');
|
|
607
|
+
// Lua pattern subset used by command-language scripts; unsupported balanced/frontier patterns refuse explicitly.
|
|
608
|
+
export function luaPattern(pattern) {
|
|
609
|
+
const classes = { a: 'A-Za-z', d: '0-9', l: 'a-z', u: 'A-Z', w: 'A-Za-z0-9', s: '\\s', x: 'A-Fa-f0-9', p: '!-/:-@[-`{-~', c: '\\x00-\\x1f\\x7f', z: '\\x00' };
|
|
610
|
+
let out = '', inClass = false;
|
|
611
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
612
|
+
const c = pattern[i];
|
|
613
|
+
if (c === '%') {
|
|
614
|
+
const next = pattern[++i];
|
|
615
|
+
if (next === 'b' || next === 'f')
|
|
616
|
+
throw new LuaError('unsupported Lua balanced/frontier pattern');
|
|
617
|
+
if (classes[next.toLowerCase()]) {
|
|
618
|
+
const content = classes[next.toLowerCase()];
|
|
619
|
+
out += inClass ? content : `[${next === next.toUpperCase() ? '^' : ''}${content}]`;
|
|
620
|
+
}
|
|
621
|
+
else if (/\d/.test(next))
|
|
622
|
+
out += `\\${next}`;
|
|
623
|
+
else
|
|
624
|
+
out += next.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
|
|
625
|
+
}
|
|
626
|
+
else if (c === '[') {
|
|
627
|
+
inClass = true;
|
|
628
|
+
out += c;
|
|
629
|
+
}
|
|
630
|
+
else if (c === ']') {
|
|
631
|
+
inClass = false;
|
|
632
|
+
out += c;
|
|
633
|
+
}
|
|
634
|
+
else if (!inClass && c === '-')
|
|
635
|
+
out += '*?';
|
|
636
|
+
else if ('\\{}|'.includes(c))
|
|
637
|
+
out += `\\${c}`;
|
|
638
|
+
else
|
|
639
|
+
out += c;
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
}
|
|
607
643
|
export function luaTruthy(v) {
|
|
608
644
|
return v !== null && v !== false;
|
|
609
645
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Redis's collection layout over the World tree. First need: the managed Redis queue's growing events stream.
|
|
2
|
+
// Key metadata and each hash field/set member/sorted-set member/stream entry are separate subjects.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { applyTwinWrite } from "../serve.js";
|
|
5
|
+
import { projectResources } from "../actions.js";
|
|
6
|
+
import { treeStamp, treeChangesSince } from "../log.js";
|
|
7
|
+
import { getActiveWorldStore } from "../world-store.js";
|
|
8
|
+
const scope = (c) => c.database === undefined ? '' : `db:${c.database}:`;
|
|
9
|
+
const collections = new Set(['hash', 'set', 'zset', 'stream', 'list']);
|
|
10
|
+
const held = new WeakMap();
|
|
11
|
+
const members = (r) => !r || !collections.has(r.kind) ? new Map() : new Map(r.v.map((x, i) => r.kind === 'list' ? [String(i), x] : r.kind === 'set' ? [x, x] : [x[0], x[1]]));
|
|
12
|
+
const memberId = (c, name, field) => `${scope(c)}member:${createHash('sha256').update(JSON.stringify([name, field])).digest('hex')}`;
|
|
13
|
+
export const memberStorage = {
|
|
14
|
+
image(ctx) {
|
|
15
|
+
const cache = held.get(getActiveWorldStore()) ?? new Map();
|
|
16
|
+
held.set(getActiveWorldStore(), cache);
|
|
17
|
+
const key = `${ctx.service}\0${ctx.root ?? ''}\0${scope(ctx)}`;
|
|
18
|
+
let h = cache.get(key);
|
|
19
|
+
const stamp = treeStamp(ctx.service, ctx.root);
|
|
20
|
+
if (h?.stamp === stamp)
|
|
21
|
+
return h.image;
|
|
22
|
+
const delta = h && treeChangesSince(ctx.service, ctx.root, h.stamp);
|
|
23
|
+
if (!h || !delta || delta.removed.length)
|
|
24
|
+
h = { stamp, subjects: new Map(), image: { rows: new Map(), scripts: new Map(), revs: new Map() } };
|
|
25
|
+
for (const r of delta && !delta.removed.length ? delta.changed : projectResources(ctx.service, ctx.root))
|
|
26
|
+
h.subjects.set(r.id, r);
|
|
27
|
+
// Reconstruct only keys affected by the tree delta, not every member on every read.
|
|
28
|
+
const affected = new Set();
|
|
29
|
+
const changed = delta && !delta.removed.length ? delta.changed : [...h.subjects.values()];
|
|
30
|
+
for (const r of changed) {
|
|
31
|
+
if (!r.id.startsWith(scope(ctx)))
|
|
32
|
+
continue;
|
|
33
|
+
if (r.type === 'key')
|
|
34
|
+
affected.add(r.name);
|
|
35
|
+
if (r.type === 'member')
|
|
36
|
+
affected.add(r.key);
|
|
37
|
+
if (r.type === 'script') {
|
|
38
|
+
if (r.gone)
|
|
39
|
+
h.image.scripts.delete(r.sha);
|
|
40
|
+
else
|
|
41
|
+
h.image.scripts.set(r.sha, r.body);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const name of affected) {
|
|
45
|
+
const meta = h.subjects.get(`${scope(ctx)}key:${name}`);
|
|
46
|
+
if (!meta)
|
|
47
|
+
continue;
|
|
48
|
+
h.image.revs.set(name, meta._rev ?? 0);
|
|
49
|
+
if (meta.gone) {
|
|
50
|
+
h.image.rows.delete(name);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const v = collections.has(meta.kind) ? [...h.subjects.values()].filter((r) => r.type === 'member' && r.id.startsWith(scope(ctx)) && r.key === name && r.gone !== true).sort((a, b) => meta.kind === 'list' ? Number(a.field) - Number(b.field) : 0).map((r) => meta.kind === 'list' ? r.value : meta.kind === 'set' ? r.field : [r.field, r.value]) : meta.v;
|
|
54
|
+
h.image.rows.set(name, { name, kind: meta.kind, v, pexpireAt: meta.pexpire_at == null ? null : String(meta.pexpire_at), lastId: meta.last_id, _rev: meta._rev });
|
|
55
|
+
}
|
|
56
|
+
h.stamp = stamp;
|
|
57
|
+
cache.set(key, h);
|
|
58
|
+
return h.image;
|
|
59
|
+
},
|
|
60
|
+
async flush(space) {
|
|
61
|
+
const ctx = space.ctx;
|
|
62
|
+
const write = async (type, id, operation, fields) => applyTwinWrite(ctx.service, { operation, subjectType: type, subjectId: id, fields, occurredAt: ctx.occurredAt, actor: { kind: 'agent' } }, ctx.root);
|
|
63
|
+
for (const { name, row, operation } of space.pendingWrites()) {
|
|
64
|
+
const before = members(space.image.rows.get(name) ?? null), after = members(row);
|
|
65
|
+
for (const field of new Set([...before.keys(), ...after.keys()])) {
|
|
66
|
+
if (before.has(field) && after.has(field) && JSON.stringify(before.get(field)) === JSON.stringify(after.get(field)))
|
|
67
|
+
continue;
|
|
68
|
+
await write('member', memberId(ctx, name, field), operation, { key: name, field, value: after.get(field) ?? null, gone: !after.has(field) });
|
|
69
|
+
}
|
|
70
|
+
await write('key', `${scope(ctx)}key:${name}`, operation, { name, kind: row?.kind ?? 'string', v: row && !collections.has(row.kind) ? row.v : null, pexpire_at: row?.pexpireAt ?? null, last_id: row?.lastId ?? '', gone: row === null, _rev: space.nextRev(name) });
|
|
71
|
+
}
|
|
72
|
+
for (const { sha, body, operation } of space.pendingScriptWrites())
|
|
73
|
+
await write('script', `${scope(ctx)}script:${sha}`, operation, { sha, body: body ?? '', gone: body === null });
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// A reply as RESP2 writes it (https://redis.io/docs/latest/develop/reference/protocol-spec/): a status as a simple string,
|
|
2
|
+
// an error as a simple error, an integer as an integer, a string as a bulk string (its length in bytes), a null as the
|
|
3
|
+
// null bulk string, an array as an array of its elements' replies. RESP2 has no float or double: a number that is not an
|
|
4
|
+
// integer is its bulk string, as Redis sends INCRBYFLOAT's. "Simple strings never contain carriage return ( \r ) or line
|
|
5
|
+
// feed ( \n ) characters" (the page), and an error is a simple string's form: Redis writes a status's text with each CR
|
|
6
|
+
// and LF mapped to a space (script_lua.c, `sdsmapchars(ok,"\r\n"," ",2)` before `addReplyStatusLength`), and an
|
|
7
|
+
// error's with the CRs and LFs at its ends trimmed and each one left inside mapped to a space (networking.c,
|
|
8
|
+
// `addReplyErrorFormatInternal`: `sdstrim(s, "\r\n")`, then `sdsmapchars(s, "\r\n", " ", 2)`), so a message holding
|
|
9
|
+
// one never ends the reply early. Source: https://raw.githubusercontent.com/redis/redis/7.4.0/src/networking.c and
|
|
10
|
+
// https://raw.githubusercontent.com/redis/redis/7.4.0/src/script_lua.c.
|
|
11
|
+
// Shared by every wire that answers a Redis reply as RESP2 bytes.
|
|
12
|
+
import { RedisInteger, RedisStatus } from "./engine.js";
|
|
13
|
+
const spaced = (text) => text.replace(/[\r\n]/g, ' ');
|
|
14
|
+
const trimmed = (text) => text.replace(/^[\r\n]+|[\r\n]+$/g, '');
|
|
15
|
+
export function resp2(value) {
|
|
16
|
+
if (value instanceof RedisInteger)
|
|
17
|
+
return `:${value.integer}\r\n`;
|
|
18
|
+
if (value === null)
|
|
19
|
+
return '$-1\r\n';
|
|
20
|
+
if (value instanceof RedisStatus)
|
|
21
|
+
return `+${spaced(value.value)}\r\n`;
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return `*${value.length}\r\n${value.map((v) => resp2(v)).join('')}`;
|
|
24
|
+
if (typeof value === 'number')
|
|
25
|
+
return Number.isInteger(value) ? `:${value}\r\n` : resp2(String(value));
|
|
26
|
+
if (typeof value === 'string')
|
|
27
|
+
return `$${new TextEncoder().encode(value).length}\r\n${value}\r\n`;
|
|
28
|
+
return `-${spaced(trimmed(value.error))}\r\n`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TwinStream } from '../twin-fetch.js';
|
|
2
|
+
import { type RedisDialect, type RunItem } from './engine.js';
|
|
3
|
+
export type RedisStreamOptions = {
|
|
4
|
+
dialect: RedisDialect;
|
|
5
|
+
version: string;
|
|
6
|
+
password?: string;
|
|
7
|
+
databases: number;
|
|
8
|
+
run(commands: string[][], database: string): Promise<{
|
|
9
|
+
items: RunItem[];
|
|
10
|
+
wrote: boolean;
|
|
11
|
+
instant?: number;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export declare function createRedisStream(options: RedisStreamOptions): TwinStream;
|