@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
package/dist/src/redis/engine.js
CHANGED
|
@@ -66,6 +66,15 @@ export class RedisStatus {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
export const OK = new RedisStatus('OK');
|
|
69
|
+
/** The JSON projection of a RESP reply — what Upstash puts in `{"result": …}`. */
|
|
70
|
+
// Redis integers are signed 64-bit on RESP; JSON clients read them as JSON numbers.
|
|
71
|
+
export class RedisInteger {
|
|
72
|
+
integer;
|
|
73
|
+
constructor(integer) {
|
|
74
|
+
this.integer = integer;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const integerReply = (n) => n >= -9007199254740991n && n <= 9007199254740991n ? Number(n) : new RedisInteger(String(n));
|
|
69
78
|
/** A command-level failure. `message` is the vendor's literal error string. Maps to HTTP 400. */
|
|
70
79
|
export class RedisCommandError extends Error {
|
|
71
80
|
constructor(message) {
|
|
@@ -125,6 +134,7 @@ export const SERVED_COMMANDS = {
|
|
|
125
134
|
ZADD: [3, -1], ZINCRBY: [3, 3], ZSCORE: [2, 2], ZCARD: [1, 1], ZCOUNT: [3, 3],
|
|
126
135
|
ZRANK: [2, 3], ZREVRANK: [2, 3], ZREM: [2, -1], ZREMRANGEBYSCORE: [3, 3], ZREMRANGEBYRANK: [3, 3],
|
|
127
136
|
ZRANGE: [3, -1], ZREVRANGE: [3, -1], ZRANGEBYSCORE: [3, -1], ZREVRANGEBYSCORE: [3, -1],
|
|
137
|
+
LPOS: [2, -1], RPOPLPUSH: [2, 2], ZPOPMIN: [1, 2], XTRIM: [3, -1],
|
|
128
138
|
XADD: [4, -1], XLEN: [1, 1], XRANGE: [3, -1], XREVRANGE: [3, -1], XDEL: [2, -1],
|
|
129
139
|
SCRIPT: [1, -1], EVAL: [2, -1], EVAL_RO: [2, -1], EVALSHA: [2, -1], EVALSHA_RO: [2, -1],
|
|
130
140
|
};
|
|
@@ -135,7 +145,7 @@ const WRITE_COMMANDS = new Set([
|
|
|
135
145
|
'EXPIRE', 'PEXPIRE', 'EXPIREAT', 'PEXPIREAT', 'PERSIST', 'RENAME', 'RENAMENX',
|
|
136
146
|
'FLUSHDB', 'FLUSHALL', 'HSET', 'HSETNX', 'HMSET', 'HDEL', 'HINCRBY', 'HINCRBYFLOAT',
|
|
137
147
|
'SADD', 'SREM', 'SPOP', 'LPUSH', 'RPUSH', 'LPOP', 'RPOP', 'LREM', 'LSET', 'LTRIM',
|
|
138
|
-
'ZADD', 'ZINCRBY', 'ZREM', 'ZREMRANGEBYSCORE', 'ZREMRANGEBYRANK', 'XADD', 'XDEL',
|
|
148
|
+
'RPOPLPUSH', 'ZPOPMIN', 'XTRIM', 'ZADD', 'ZINCRBY', 'ZREM', 'ZREMRANGEBYSCORE', 'ZREMRANGEBYRANK', 'XADD', 'XDEL',
|
|
139
149
|
// NB: 'SCRIPT' is deliberately ABSENT — it is classified per SUBCOMMAND below, because
|
|
140
150
|
// `SCRIPT EXISTS` is a pure read and a read-only twin must serve every read (§9 round 1 found
|
|
141
151
|
// it answering 405). Likewise `EVAL_RO`/`EVALSHA_RO` are reads by definition.
|
|
@@ -238,7 +248,7 @@ function patchImage(image, changed, removed, scope) {
|
|
|
238
248
|
name,
|
|
239
249
|
kind: rec.kind,
|
|
240
250
|
v: rec.v,
|
|
241
|
-
pexpireAt: typeof rec.pexpire_at === 'number' ? rec.pexpire_at : null,
|
|
251
|
+
pexpireAt: typeof rec.pexpire_at === 'string' || typeof rec.pexpire_at === 'number' ? String(rec.pexpire_at) : null,
|
|
242
252
|
...(typeof rec.last_id === 'string' && rec.last_id !== '' ? { lastId: rec.last_id } : {}),
|
|
243
253
|
...(typeof rec._rev === 'number' ? { _rev: rec._rev } : {}),
|
|
244
254
|
...(rec.meta !== undefined && rec.meta !== null ? { meta: rec.meta } : {}),
|
|
@@ -285,7 +295,7 @@ export class KeySpace {
|
|
|
285
295
|
live(row) {
|
|
286
296
|
if (!row)
|
|
287
297
|
return undefined;
|
|
288
|
-
return row.pexpireAt !== null && row.pexpireAt < this.nowMs ? undefined : row;
|
|
298
|
+
return row.pexpireAt !== null && BigInt(row.pexpireAt) < BigInt(this.nowMs) ? undefined : row;
|
|
289
299
|
}
|
|
290
300
|
all() { return [...this.rows.values()].filter((r) => this.live(r) !== undefined); }
|
|
291
301
|
// ── the EVAL script cache ────────────────────────────────────────────────────────────────
|
|
@@ -440,14 +450,36 @@ export const asZSet = (row) => {
|
|
|
440
450
|
export const storeZSet = (pairs) => pairs.map(([m, sc]) => [m, fmtScore(sc)]);
|
|
441
451
|
export const asStream = (row) => { expectType(row, STREAM); expectShape(row, Array.isArray(row?.v)); return row ? row.v.map(([id, f]) => [id, f.map(([field, value]) => [field, value])]) : []; };
|
|
442
452
|
// ── numeric parsing (Redis's exact acceptance rules) ──────────────────────────────────────────
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
453
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/util.c "Convert a string into a long long."
|
|
454
|
+
export function integerArgument(raw, message = NOT_INT, unsigned = false) {
|
|
455
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/util.c "strtoull() is used instead."
|
|
456
|
+
if (unsigned) {
|
|
457
|
+
const canonical = /^(0|[1-9]\d*|-[1-9]\d*)$/.test(raw);
|
|
458
|
+
if (!canonical && !/^[ \t\r\n\v\f]*[+-]?\d+$/.test(raw))
|
|
459
|
+
throw new RedisCommandError(message);
|
|
460
|
+
const n = BigInt(raw.trim());
|
|
461
|
+
if (n > 18446744073709551615n || n < -18446744073709551615n || canonical && n < 0n)
|
|
462
|
+
throw new RedisCommandError(message);
|
|
463
|
+
return BigInt.asUintN(64, n);
|
|
464
|
+
}
|
|
465
|
+
if (!/^(0|[1-9]\d*|-[1-9]\d*)$/.test(raw))
|
|
466
|
+
throw new RedisCommandError(message);
|
|
467
|
+
const n = BigInt(raw);
|
|
468
|
+
if (n < -9223372036854775808n || n > 9223372036854775807n)
|
|
469
|
+
throw new RedisCommandError(message);
|
|
449
470
|
return n;
|
|
450
471
|
}
|
|
472
|
+
// Collection positions/counts are bounded by their actual arrays, not by Number.MAX_SAFE_INTEGER.
|
|
473
|
+
export const commandInteger = (raw, message = NOT_INT) => Number(integerArgument(raw, message));
|
|
474
|
+
export const toInt = commandInteger;
|
|
475
|
+
export const unsignedArgument = (raw, message) => integerArgument(raw, message, true);
|
|
476
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/object.c "value is out of range, must be positive"
|
|
477
|
+
export function positiveInteger(raw, message = 'ERR value is out of range, must be positive') {
|
|
478
|
+
const n = integerArgument(raw, message);
|
|
479
|
+
if (n < 0)
|
|
480
|
+
throw new RedisCommandError(message);
|
|
481
|
+
return Number(n);
|
|
482
|
+
}
|
|
451
483
|
export function toFloat(raw) {
|
|
452
484
|
if (raw !== raw.trim() || raw === '')
|
|
453
485
|
throw new RedisCommandError(NOT_FLOAT);
|
|
@@ -532,20 +564,13 @@ export function globMatch(pattern, subject) {
|
|
|
532
564
|
}
|
|
533
565
|
// ── TTL option parsing, shared by SET and GETEX ────────────────────────────────────────────────
|
|
534
566
|
function ttlFromToken(token, value, nowMs, cmd) {
|
|
535
|
-
const n =
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (n <= 0)
|
|
543
|
-
throw new RedisCommandError(`ERR invalid expire time in '${cmd}' command`);
|
|
544
|
-
return nowMs + n;
|
|
545
|
-
}
|
|
546
|
-
if (token === 'exat')
|
|
547
|
-
return n * 1000;
|
|
548
|
-
return n; // pxat
|
|
567
|
+
const n = integerArgument(value);
|
|
568
|
+
const millis = token === 'ex' || token === 'exat' ? n * 1000n : n;
|
|
569
|
+
const at = token === 'ex' || token === 'px' ? BigInt(Math.trunc(nowMs)) + millis : millis;
|
|
570
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/t_string.c "Overflow detected."
|
|
571
|
+
if (n <= 0n || millis > 9223372036854775807n || at > 9223372036854775807n)
|
|
572
|
+
throw new RedisCommandError(`ERR invalid expire time in '${cmd}' command`);
|
|
573
|
+
return String(at);
|
|
549
574
|
}
|
|
550
575
|
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
551
576
|
// COMMAND DISPATCH — fully synchronous over the snapshot (see the file header for why).
|
|
@@ -564,6 +589,107 @@ export function execOne(space, argv) {
|
|
|
564
589
|
return dialect.exec(space, argv);
|
|
565
590
|
switch (name) {
|
|
566
591
|
// ── connection / server ──────────────────────────────────────────────────────────────────
|
|
592
|
+
// https://redis.io/docs/latest/commands/rpoplpush/: atomically moves the last element to the destination's head.
|
|
593
|
+
case 'RPOPLPUSH': {
|
|
594
|
+
const sourceRow = space.get(a[0]);
|
|
595
|
+
const source = asList(sourceRow);
|
|
596
|
+
asList(space.get(a[1]));
|
|
597
|
+
const value = source.pop();
|
|
598
|
+
if (value === undefined)
|
|
599
|
+
return null;
|
|
600
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/t_list.c "listTypePush(dstobj,value,where);"
|
|
601
|
+
// A rotation rewrites the existing list, including a singleton, without deleting its key or expiry.
|
|
602
|
+
if (a[0] === a[1]) {
|
|
603
|
+
source.unshift(value);
|
|
604
|
+
space.put({ ...sourceRow, v: source }, 'list.move');
|
|
605
|
+
return value;
|
|
606
|
+
}
|
|
607
|
+
if (source.length)
|
|
608
|
+
space.put({ ...space.get(a[0]), v: source }, 'list.move');
|
|
609
|
+
else
|
|
610
|
+
space.remove(a[0], 'list.move');
|
|
611
|
+
const dest = space.get(a[1]);
|
|
612
|
+
const values = asList(dest);
|
|
613
|
+
values.unshift(value);
|
|
614
|
+
space.put({ name: a[1], kind: 'list', v: values, pexpireAt: dest?.pexpireAt ?? null }, 'list.move');
|
|
615
|
+
return value;
|
|
616
|
+
}
|
|
617
|
+
// https://redis.io/docs/latest/commands/lpos/: rank, count and maxlen apply to matching positions.
|
|
618
|
+
case 'LPOS': {
|
|
619
|
+
const values = asList(space.get(a[0]));
|
|
620
|
+
let rank = 1, count, max = values.length;
|
|
621
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/t_list.c "COUNT can't be negative"
|
|
622
|
+
for (let i = 2; i < a.length; i += 2) {
|
|
623
|
+
const option = a[i].toUpperCase();
|
|
624
|
+
if (!['RANK', 'COUNT', 'MAXLEN'].includes(option) || i + 1 >= a.length)
|
|
625
|
+
throw new RedisCommandError(SYNTAX);
|
|
626
|
+
let value;
|
|
627
|
+
try {
|
|
628
|
+
value = commandInteger(a[i + 1]);
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
if (option === 'RANK')
|
|
632
|
+
throw error;
|
|
633
|
+
throw new RedisCommandError(`ERR ${option} can't be negative`);
|
|
634
|
+
}
|
|
635
|
+
if (option === 'RANK') {
|
|
636
|
+
if (a[i + 1] === '-9223372036854775808')
|
|
637
|
+
throw new RedisCommandError('ERR value is out of range, value must between -9223372036854775807 and 9223372036854775807');
|
|
638
|
+
if (value === 0)
|
|
639
|
+
throw new RedisCommandError("ERR RANK can't be zero: use 1 to start from the first match, 2 from the second ... or use negative to start from the end of the list");
|
|
640
|
+
rank = value;
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
if (value < 0)
|
|
644
|
+
throw new RedisCommandError(`ERR ${option} can't be negative`);
|
|
645
|
+
if (option === 'COUNT')
|
|
646
|
+
count = value;
|
|
647
|
+
else
|
|
648
|
+
max = value || values.length;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const matches = Array.from({ length: values.length }, (_, i) => i);
|
|
652
|
+
if (rank < 0)
|
|
653
|
+
matches.reverse();
|
|
654
|
+
const hits = matches.slice(0, max).filter((i) => values[i] === a[1]).slice(Math.abs(rank) - 1);
|
|
655
|
+
return count === undefined ? hits[0] ?? null : hits.slice(0, count === 0 ? undefined : count);
|
|
656
|
+
}
|
|
657
|
+
// https://redis.io/docs/latest/commands/zpopmin/: lowest score first; ties by member.
|
|
658
|
+
case 'ZPOPMIN': {
|
|
659
|
+
const row = space.get(a[0]);
|
|
660
|
+
const pairs = sortZSet(asZSet(row));
|
|
661
|
+
let count = 1;
|
|
662
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/object.c "value is out of range, must be positive"
|
|
663
|
+
try {
|
|
664
|
+
if (a[1] !== undefined)
|
|
665
|
+
count = commandInteger(a[1]);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
throw new RedisCommandError('ERR value is out of range, must be positive');
|
|
669
|
+
}
|
|
670
|
+
if (count < 0)
|
|
671
|
+
throw new RedisCommandError('ERR value is out of range, must be positive');
|
|
672
|
+
const popped = pairs.splice(0, count);
|
|
673
|
+
if (popped.length) {
|
|
674
|
+
if (pairs.length)
|
|
675
|
+
space.put({ ...row, v: storeZSet(sortZSet(pairs)) }, 'zset.pop');
|
|
676
|
+
else
|
|
677
|
+
space.remove(a[0], 'zset.pop');
|
|
678
|
+
}
|
|
679
|
+
return popped.flatMap(([m, score]) => [m, fmtScore(score)]);
|
|
680
|
+
}
|
|
681
|
+
// https://redis.io/docs/latest/commands/xtrim/: exact trimming also satisfies the approximate upper bound.
|
|
682
|
+
case 'XTRIM': {
|
|
683
|
+
const row = space.get(a[0]);
|
|
684
|
+
const entries = asStream(row);
|
|
685
|
+
const spec = trimSpec(a, 1);
|
|
686
|
+
if (spec.next !== a.length)
|
|
687
|
+
throw new RedisCommandError(SYNTAX);
|
|
688
|
+
const kept = trimEntries(entries, spec);
|
|
689
|
+
if (row && kept.length !== entries.length)
|
|
690
|
+
space.put({ ...row, v: kept }, 'stream.trim');
|
|
691
|
+
return entries.length - kept.length;
|
|
692
|
+
}
|
|
567
693
|
case 'PING': return a.length === 0 ? new RedisStatus('PONG') : a[0];
|
|
568
694
|
case 'ECHO': return a[0];
|
|
569
695
|
case 'DBSIZE': return visibleKeys(space).length;
|
|
@@ -585,9 +711,8 @@ export function execOne(space, argv) {
|
|
|
585
711
|
return keys.length === 0 ? null : keys[0];
|
|
586
712
|
}
|
|
587
713
|
case 'SCAN': {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
throw new RedisCommandError('ERR invalid cursor');
|
|
714
|
+
// SCAN uses Redis's unsigned cursor parser, unlike signed COUNT.
|
|
715
|
+
const cursor = Number(unsignedArgument(a[0], 'ERR invalid cursor'));
|
|
591
716
|
let match = null;
|
|
592
717
|
let count = 10;
|
|
593
718
|
let typeFilter = null;
|
|
@@ -669,7 +794,7 @@ export function execOne(space, argv) {
|
|
|
669
794
|
case 'EXPIREAT':
|
|
670
795
|
case 'PEXPIREAT': {
|
|
671
796
|
const row = space.get(a[0]);
|
|
672
|
-
const n =
|
|
797
|
+
const n = integerArgument(a[1]);
|
|
673
798
|
// Redis 7.0's options (redis.io/docs/latest/commands/expire/, "Options"): NX sets only a key with no expiry, XX
|
|
674
799
|
// only one with an expiry, GT only a later deadline, LT only an earlier one, a key with no expiry counting as
|
|
675
800
|
// one that never expires; NX with any other, and GT with LT, are refused
|
|
@@ -679,26 +804,27 @@ export function execOne(space, argv) {
|
|
|
679
804
|
throw new RedisCommandError('ERR NX and XX, GT or LT options at the same time are not compatible');
|
|
680
805
|
if (opts.has('GT') && opts.has('LT'))
|
|
681
806
|
throw new RedisCommandError('ERR GT and LT options at the same time are not compatible');
|
|
807
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/expire.c "overflow by either unit conversion or basetime addition."
|
|
808
|
+
const millis = name === 'EXPIRE' || name === 'EXPIREAT' ? n * 1000n : n;
|
|
809
|
+
const deadline = name === 'EXPIRE' || name === 'PEXPIRE' ? BigInt(Math.trunc(nowMs)) + millis : millis;
|
|
810
|
+
if (millis < -9223372036854775808n || millis > 9223372036854775807n || deadline > 9223372036854775807n)
|
|
811
|
+
throw new RedisCommandError(`ERR invalid expire time in '${name.toLowerCase()}' command`);
|
|
682
812
|
if (!row)
|
|
683
813
|
return 0;
|
|
684
|
-
const at = name === 'EXPIRE' ? nowMs + n * 1000
|
|
685
|
-
: name === 'PEXPIRE' ? nowMs + n
|
|
686
|
-
: name === 'EXPIREAT' ? n * 1000
|
|
687
|
-
: n;
|
|
688
814
|
if (opts.has('NX') && row.pexpireAt !== null)
|
|
689
815
|
return 0;
|
|
690
816
|
if (opts.has('XX') && row.pexpireAt === null)
|
|
691
817
|
return 0;
|
|
692
|
-
if (opts.has('GT') && (row.pexpireAt === null ||
|
|
818
|
+
if (opts.has('GT') && (row.pexpireAt === null || deadline <= BigInt(row.pexpireAt)))
|
|
693
819
|
return 0;
|
|
694
|
-
if (opts.has('LT') && row.pexpireAt !== null &&
|
|
820
|
+
if (opts.has('LT') && row.pexpireAt !== null && deadline >= BigInt(row.pexpireAt))
|
|
695
821
|
return 0;
|
|
696
822
|
// A deadline already in the past DELETES the key, exactly as Redis does.
|
|
697
|
-
if (
|
|
823
|
+
if (deadline <= BigInt(nowMs)) {
|
|
698
824
|
space.remove(a[0], 'key.expire_now');
|
|
699
825
|
return 1;
|
|
700
826
|
}
|
|
701
|
-
space.put({ ...row, pexpireAt:
|
|
827
|
+
space.put({ ...row, pexpireAt: String(deadline) }, 'key.expire');
|
|
702
828
|
return 1;
|
|
703
829
|
}
|
|
704
830
|
case 'TTL':
|
|
@@ -708,11 +834,11 @@ export function execOne(space, argv) {
|
|
|
708
834
|
return -2;
|
|
709
835
|
if (row.pexpireAt === null)
|
|
710
836
|
return -1;
|
|
711
|
-
const remainingMs = row.pexpireAt - nowMs;
|
|
837
|
+
const remainingMs = BigInt(row.pexpireAt) - BigInt(nowMs);
|
|
712
838
|
// Redis computes `(ttl+500)/1000` in integer arithmetic — round HALF-UP, not ceil. With ceil,
|
|
713
839
|
// a key set `EX 10` and read 600ms later still reported 10 where the vendor says 9 (§9 round
|
|
714
840
|
// 1); every earlier verify happened to land on an exact second boundary, where they agree.
|
|
715
|
-
return name === 'TTL' ?
|
|
841
|
+
return integerReply(name === 'TTL' ? (remainingMs + 500n) / 1000n : remainingMs);
|
|
716
842
|
}
|
|
717
843
|
case 'PERSIST': {
|
|
718
844
|
const row = space.get(a[0]);
|
|
@@ -721,6 +847,7 @@ export function execOne(space, argv) {
|
|
|
721
847
|
space.put({ ...row, pexpireAt: null }, 'key.persist');
|
|
722
848
|
return 1;
|
|
723
849
|
}
|
|
850
|
+
// source: https://redis.io/docs/latest/commands/expire/ "the associated time to live is transferred to the new key name"
|
|
724
851
|
case 'RENAME':
|
|
725
852
|
case 'RENAMENX': {
|
|
726
853
|
const row = space.get(a[0]);
|
|
@@ -746,6 +873,7 @@ export function execOne(space, argv) {
|
|
|
746
873
|
space.remove(a[0], 'string.getdel');
|
|
747
874
|
return v === undefined ? null : v;
|
|
748
875
|
}
|
|
876
|
+
// source: https://redis.io/docs/latest/commands/getset/ "Any previous time to live associated with the key is discarded"
|
|
749
877
|
case 'GETSET': {
|
|
750
878
|
const old = asString(space.get(a[0]));
|
|
751
879
|
space.put({ name: a[0], kind: 'string', v: a[1], pexpireAt: null }, 'string.getset');
|
|
@@ -824,6 +952,7 @@ export function execOne(space, argv) {
|
|
|
824
952
|
return get ? (old ?? null) : null;
|
|
825
953
|
if (xx && !existing)
|
|
826
954
|
return get ? (old ?? null) : null;
|
|
955
|
+
// source: https://redis.io/docs/latest/commands/set/ "Retain the time to live associated with the key"
|
|
827
956
|
space.put({ name: key, kind: 'string', v: value, pexpireAt: keepTtl ? (existing?.pexpireAt ?? null) : pexpireAt }, 'string.set');
|
|
828
957
|
return get ? (old ?? null) : OK;
|
|
829
958
|
}
|
|
@@ -835,10 +964,8 @@ export function execOne(space, argv) {
|
|
|
835
964
|
}
|
|
836
965
|
case 'SETEX':
|
|
837
966
|
case 'PSETEX': {
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
throw new RedisCommandError(`ERR invalid expire time in '${name.toLowerCase()}' command`);
|
|
841
|
-
space.put({ name: a[0], kind: 'string', v: a[2], pexpireAt: nowMs + (name === 'SETEX' ? n * 1000 : n) }, 'string.setex');
|
|
967
|
+
const pexpireAt = ttlFromToken(name === 'SETEX' ? 'ex' : 'px', a[1], nowMs, name.toLowerCase());
|
|
968
|
+
space.put({ name: a[0], kind: 'string', v: a[2], pexpireAt }, 'string.setex');
|
|
842
969
|
return OK;
|
|
843
970
|
}
|
|
844
971
|
case 'MGET': return a.map((k) => { const r = space.get(k); return r && r.kind === 'string' ? r.v : null; });
|
|
@@ -866,17 +993,17 @@ export function execOne(space, argv) {
|
|
|
866
993
|
case 'DECR':
|
|
867
994
|
case 'INCRBY':
|
|
868
995
|
case 'DECRBY': {
|
|
869
|
-
const by = name === 'INCR' ?
|
|
996
|
+
const by = name === 'INCR' ? 1n : name === 'DECR' ? -1n : integerArgument(a[1]) * (name === 'DECRBY' ? -1n : 1n);
|
|
870
997
|
const row = space.get(a[0]);
|
|
871
998
|
const current = asString(row);
|
|
872
|
-
const next = (current === undefined ?
|
|
873
|
-
if (
|
|
999
|
+
const next = (current === undefined ? 0n : integerArgument(current)) + by;
|
|
1000
|
+
if (next < -9223372036854775808n || next > 9223372036854775807n)
|
|
874
1001
|
throw new RedisCommandError('ERR increment or decrement would overflow');
|
|
875
1002
|
// INCR PRESERVES the key's TTL (real Redis semantics). This is exactly what makes
|
|
876
1003
|
// @upstash/ratelimit's fixed-window script correct: the PEXPIRE is armed on the first
|
|
877
1004
|
// increment and every later INCRBY must leave it alone, or the window would never close.
|
|
878
1005
|
space.put({ name: a[0], kind: 'string', v: String(next), pexpireAt: row?.pexpireAt ?? null }, 'string.incr');
|
|
879
|
-
return next;
|
|
1006
|
+
return integerReply(next);
|
|
880
1007
|
}
|
|
881
1008
|
case 'INCRBYFLOAT': {
|
|
882
1009
|
const row = space.get(a[0]);
|
|
@@ -947,9 +1074,10 @@ export function execOne(space, argv) {
|
|
|
947
1074
|
const pairs = asHash(row);
|
|
948
1075
|
const idx = pairs.findIndex(([f]) => f === a[1]);
|
|
949
1076
|
const current = idx < 0 ? undefined : pairs[idx][1];
|
|
950
|
-
const
|
|
951
|
-
const
|
|
952
|
-
|
|
1077
|
+
const by = isFloat ? toFloat(a[2]) : integerArgument(a[2]);
|
|
1078
|
+
const base = current === undefined ? (isFloat ? 0 : 0n) : isFloat ? toFloat(current) : integerArgument(current, 'ERR hash value is not an integer');
|
|
1079
|
+
const next = isFloat ? guardFloatResult(base + by) : base + by;
|
|
1080
|
+
if (!isFloat && (next < -9223372036854775808n || next > 9223372036854775807n))
|
|
953
1081
|
throw new RedisCommandError('ERR increment or decrement would overflow');
|
|
954
1082
|
const text = isFloat ? fmtScore(next) : String(next);
|
|
955
1083
|
if (idx < 0)
|
|
@@ -957,7 +1085,7 @@ export function execOne(space, argv) {
|
|
|
957
1085
|
else
|
|
958
1086
|
pairs[idx] = [a[1], text];
|
|
959
1087
|
space.put({ name: a[0], kind: 'hash', v: pairs, pexpireAt: row?.pexpireAt ?? null }, 'hash.incrby');
|
|
960
|
-
return isFloat ? text : next;
|
|
1088
|
+
return isFloat ? text : integerReply(next);
|
|
961
1089
|
}
|
|
962
1090
|
// ── sets ─────────────────────────────────────────────────────────────────────────────────
|
|
963
1091
|
case 'SADD': {
|
|
@@ -1031,11 +1159,9 @@ export function execOne(space, argv) {
|
|
|
1031
1159
|
}
|
|
1032
1160
|
case 'LPOP':
|
|
1033
1161
|
case 'RPOP': {
|
|
1162
|
+
const count = a.length >= 2 ? positiveInteger(a[1]) : null;
|
|
1034
1163
|
const row = space.get(a[0]);
|
|
1035
1164
|
const items = asList(row);
|
|
1036
|
-
const count = a.length >= 2 ? toInt(a[1]) : null;
|
|
1037
|
-
if (count !== null && count < 0)
|
|
1038
|
-
throw new RedisCommandError('ERR value is out of range, must be positive');
|
|
1039
1165
|
if (items.length === 0)
|
|
1040
1166
|
return null;
|
|
1041
1167
|
const n = count === null ? 1 : count;
|
|
@@ -1073,9 +1199,9 @@ export function execOne(space, argv) {
|
|
|
1073
1199
|
case 'LTRIM': {
|
|
1074
1200
|
const row = space.get(a[0]);
|
|
1075
1201
|
const items = asList(row);
|
|
1202
|
+
const [start, stop] = normalizeRange(toInt(a[1]), toInt(a[2]), items.length);
|
|
1076
1203
|
if (!row)
|
|
1077
1204
|
return OK;
|
|
1078
|
-
const [start, stop] = normalizeRange(toInt(a[1]), toInt(a[2]), items.length);
|
|
1079
1205
|
const kept = start > stop ? [] : items.slice(start, stop + 1);
|
|
1080
1206
|
if (kept.length === 0)
|
|
1081
1207
|
space.remove(a[0], 'list.trim_empty');
|
|
@@ -1254,6 +1380,12 @@ export function execOne(space, argv) {
|
|
|
1254
1380
|
case 'EVAL_RO':
|
|
1255
1381
|
case 'EVALSHA':
|
|
1256
1382
|
case 'EVALSHA_RO': {
|
|
1383
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/eval.c "Get the number of arguments that are keys"
|
|
1384
|
+
const numKeys = toInt(a[1]);
|
|
1385
|
+
if (numKeys < 0)
|
|
1386
|
+
throw new RedisCommandError("ERR Number of keys can't be negative");
|
|
1387
|
+
if (numKeys > a.length - 2)
|
|
1388
|
+
throw new RedisCommandError("ERR Number of keys can't be greater than number of args");
|
|
1257
1389
|
const bySha = name.startsWith('EVALSHA');
|
|
1258
1390
|
let script;
|
|
1259
1391
|
if (bySha) {
|
|
@@ -1275,11 +1407,6 @@ export function execOne(space, argv) {
|
|
|
1275
1407
|
if (!space.ctx.readOnly)
|
|
1276
1408
|
storeScript(space, script);
|
|
1277
1409
|
}
|
|
1278
|
-
const numKeys = toInt(a[1]);
|
|
1279
|
-
if (numKeys < 0)
|
|
1280
|
-
throw new RedisCommandError("ERR Number of keys can't be negative");
|
|
1281
|
-
if (numKeys > a.length - 2)
|
|
1282
|
-
throw new RedisCommandError("ERR Number of keys can't be greater than number of args");
|
|
1283
1410
|
return evalScript(space, script, a.slice(2, 2 + numKeys), a.slice(2 + numKeys), name.endsWith('_RO'));
|
|
1284
1411
|
}
|
|
1285
1412
|
default:
|
|
@@ -1452,7 +1579,7 @@ export function zrange(space, name, a) {
|
|
|
1452
1579
|
if (_rev)
|
|
1453
1580
|
selected = selected.reverse();
|
|
1454
1581
|
if (limit)
|
|
1455
|
-
selected = limit.count < 0 ? selected.slice(limit.offset) : selected.slice(limit.offset, limit.offset + limit.count);
|
|
1582
|
+
selected = limit.offset < 0 ? [] : limit.count < 0 ? selected.slice(limit.offset) : selected.slice(limit.offset, limit.offset + limit.count);
|
|
1456
1583
|
}
|
|
1457
1584
|
else {
|
|
1458
1585
|
const ordered = _rev ? [...sorted].reverse() : sorted;
|
|
@@ -1463,8 +1590,8 @@ export function zrange(space, name, a) {
|
|
|
1463
1590
|
}
|
|
1464
1591
|
// ── streams ────────────────────────────────────────────────────────────────────────────────────
|
|
1465
1592
|
export function compareStreamIds(left, right) {
|
|
1466
|
-
const [lms =
|
|
1467
|
-
const [rms =
|
|
1593
|
+
const [lms = 0n, lseq = 0n] = left.split('-').map(BigInt);
|
|
1594
|
+
const [rms = 0n, rseq = 0n] = right.split('-').map(BigInt);
|
|
1468
1595
|
if (lms !== rms)
|
|
1469
1596
|
return lms < rms ? -1 : 1;
|
|
1470
1597
|
return lseq === rseq ? 0 : lseq < rseq ? -1 : 1;
|
|
@@ -1473,59 +1600,129 @@ export function parseStreamBound(raw, side) {
|
|
|
1473
1600
|
if (raw === '-')
|
|
1474
1601
|
return '0-0';
|
|
1475
1602
|
if (raw === '+')
|
|
1476
|
-
return
|
|
1603
|
+
return '18446744073709551615-18446744073709551615';
|
|
1477
1604
|
const bare = raw.startsWith('(') ? raw.slice(1) : raw;
|
|
1478
1605
|
if (!/^\d+(-\d+)?$/.test(bare))
|
|
1479
1606
|
throw new RedisCommandError('ERR Invalid stream ID specified as stream command argument');
|
|
1480
|
-
return bare.includes('-') ? bare : `${bare}-${side === 'min' ? 0 :
|
|
1607
|
+
return bare.includes('-') ? bare : `${bare}-${side === 'min' ? '0' : '18446744073709551615'}`;
|
|
1608
|
+
}
|
|
1609
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/util.c "strtoull() is used instead."
|
|
1610
|
+
// Stream components are uint64; Redis's fallback also accepts a leading plus or zeroes.
|
|
1611
|
+
function streamArgument(raw, wildcard = false) {
|
|
1612
|
+
const at = raw.indexOf('-');
|
|
1613
|
+
const ms = at < 0 ? raw : raw.slice(0, at), seq = at < 0 ? '0' : raw.slice(at + 1);
|
|
1614
|
+
const part = (v) => String(unsignedArgument(v, 'ERR Invalid stream ID specified as stream command argument'));
|
|
1615
|
+
return `${part(ms)}-${wildcard && seq === '*' ? '*' : part(seq)}`;
|
|
1616
|
+
}
|
|
1617
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/t_stream.c "The LIMIT argument must be >= 0."
|
|
1618
|
+
// MAXLEN/MINID use a strict unsigned threshold; LIMIT is allowed only with approximate trimming.
|
|
1619
|
+
function trimSpec(a, start) {
|
|
1620
|
+
let i = start;
|
|
1621
|
+
const strategy = a[i++].toUpperCase();
|
|
1622
|
+
if (!['MAXLEN', 'MINID'].includes(strategy))
|
|
1623
|
+
throw new RedisCommandError(SYNTAX);
|
|
1624
|
+
const approximate = a[i] === '~';
|
|
1625
|
+
if (['~', '='].includes(a[i] ?? ''))
|
|
1626
|
+
i++;
|
|
1627
|
+
if (i >= a.length)
|
|
1628
|
+
throw new RedisCommandError(SYNTAX);
|
|
1629
|
+
let threshold = a[i++];
|
|
1630
|
+
if (strategy === 'MAXLEN') {
|
|
1631
|
+
if (commandInteger(threshold) < 0)
|
|
1632
|
+
throw new RedisCommandError('ERR The MAXLEN argument must be >= 0.');
|
|
1633
|
+
}
|
|
1634
|
+
else
|
|
1635
|
+
threshold = streamArgument(threshold);
|
|
1636
|
+
let limit = Infinity;
|
|
1637
|
+
if (a[i]?.toUpperCase() === 'LIMIT') {
|
|
1638
|
+
if (i + 1 >= a.length)
|
|
1639
|
+
throw new RedisCommandError(SYNTAX);
|
|
1640
|
+
i++;
|
|
1641
|
+
limit = commandInteger(a[i++]);
|
|
1642
|
+
if (limit < 0)
|
|
1643
|
+
throw new RedisCommandError('ERR The LIMIT argument must be >= 0.');
|
|
1644
|
+
// source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/t_stream.c "syntax error, LIMIT cannot be used without the special ~ option"
|
|
1645
|
+
if (!approximate)
|
|
1646
|
+
throw new RedisCommandError('ERR syntax error, LIMIT cannot be used without the special ~ option');
|
|
1647
|
+
if (limit === 0)
|
|
1648
|
+
limit = Infinity;
|
|
1649
|
+
}
|
|
1650
|
+
return { strategy, threshold, limit, next: i };
|
|
1651
|
+
}
|
|
1652
|
+
function trimEntries(entries, spec) {
|
|
1653
|
+
const remove = spec.strategy === 'MAXLEN' ? Math.max(0, entries.length - Number(spec.threshold)) : entries.filter(([id]) => compareStreamIds(id, parseStreamBound(spec.threshold, 'min')) < 0).length;
|
|
1654
|
+
return entries.slice(Math.min(remove, spec.limit));
|
|
1481
1655
|
}
|
|
1482
1656
|
function xadd(space, a, nowMs) {
|
|
1483
1657
|
let i = 1;
|
|
1484
1658
|
let noMkStream = false;
|
|
1659
|
+
let trim;
|
|
1485
1660
|
for (; i < a.length; i++) {
|
|
1486
1661
|
const tok = a[i].toLowerCase();
|
|
1487
1662
|
if (tok === 'nomkstream') {
|
|
1488
1663
|
noMkStream = true;
|
|
1489
1664
|
continue;
|
|
1490
1665
|
}
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1666
|
+
if (tok === 'maxlen' || tok === 'minid') {
|
|
1667
|
+
if (trim)
|
|
1668
|
+
throw new RedisCommandError('ERR syntax error, MAXLEN and MINID options at the same time are not compatible');
|
|
1669
|
+
trim = trimSpec(a, i);
|
|
1670
|
+
i = trim.next - 1;
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1494
1673
|
break;
|
|
1495
1674
|
}
|
|
1496
1675
|
const row = space.get(a[0]);
|
|
1497
1676
|
if (!row && noMkStream)
|
|
1498
1677
|
return null;
|
|
1499
1678
|
const entries = asStream(row);
|
|
1500
|
-
|
|
1679
|
+
if (row?.lastId === '18446744073709551615-18446744073709551615')
|
|
1680
|
+
throw new RedisCommandError('ERR The stream has exhausted the last possible ID, unable to add more items');
|
|
1681
|
+
const rawId = a[i];
|
|
1682
|
+
if (rawId === undefined)
|
|
1683
|
+
throw new RedisCommandError(wrongArity('xadd'));
|
|
1501
1684
|
const fields = a.slice(i + 1);
|
|
1502
1685
|
if (fields.length === 0 || fields.length % 2 !== 0)
|
|
1503
1686
|
throw new RedisCommandError(wrongArity('xadd'));
|
|
1687
|
+
const idSpec = rawId === '*' ? rawId : streamArgument(rawId, true);
|
|
1504
1688
|
let id;
|
|
1505
1689
|
if (idSpec === '*') {
|
|
1506
1690
|
// The id's ms part is the REQUEST CLOCK (`occurredAt`), never `Date.now()`. Two entries added
|
|
1507
1691
|
// in the same pinned millisecond get sequence 0, 1, 2 … exactly as Redis does — which is what
|
|
1508
1692
|
// makes a stream verify reproducible.
|
|
1509
|
-
const [lastMs =
|
|
1510
|
-
id = nowMs > lastMs ? `${nowMs}-0` : `${lastMs}-${lastSeq +
|
|
1693
|
+
const [lastMs = 0n, lastSeq = 0n] = (row?.lastId ?? '0-0').split('-').map(BigInt);
|
|
1694
|
+
id = BigInt(nowMs) > lastMs ? `${nowMs}-0` : `${lastMs}-${lastSeq + 1n}`;
|
|
1695
|
+
}
|
|
1696
|
+
else if (/^(0|[1-9]\d*)-\*$/.test(idSpec) && BigInt(idSpec.split('-')[0]) <= 18446744073709551615n) {
|
|
1697
|
+
const ms = BigInt(idSpec.split('-')[0]);
|
|
1698
|
+
const [lastMs = 0n, lastSeq = 0n] = (row?.lastId ?? '0-0').split('-').map(BigInt);
|
|
1699
|
+
if (ms < lastMs)
|
|
1700
|
+
throw new RedisCommandError('ERR The ID specified in XADD is equal or smaller than the target stream top item');
|
|
1701
|
+
id = `${ms}-${ms === lastMs ? lastSeq + 1n : 0n}`;
|
|
1511
1702
|
}
|
|
1512
1703
|
else {
|
|
1513
1704
|
id = idSpec.includes('-') ? idSpec : `${idSpec}-0`;
|
|
1514
|
-
if (
|
|
1705
|
+
if (!/^(0|[1-9]\d*)-(0|[1-9]\d*)$/.test(id) || id.split('-').some((v) => BigInt(v) > 18446744073709551615n))
|
|
1515
1706
|
throw new RedisCommandError('ERR Invalid stream ID specified as stream command argument');
|
|
1516
1707
|
// Compare against the RETAINED last-id, not merely the last surviving entry: Redis keeps
|
|
1517
1708
|
// `last_id` past an XDEL, so an id that was used and then deleted stays refused forever. §9
|
|
1518
1709
|
// round 1 found the entries-only comparison re-accepting `5-5` after `XDEL st 5-5`.
|
|
1710
|
+
if (id === '0-0')
|
|
1711
|
+
throw new RedisCommandError('ERR The ID specified in XADD must be greater than 0-0');
|
|
1519
1712
|
const top = row?.lastId ?? (entries.length > 0 ? entries[entries.length - 1][0] : '0-0');
|
|
1520
1713
|
if (compareStreamIds(id, top) <= 0) {
|
|
1521
1714
|
throw new RedisCommandError('ERR The ID specified in XADD is equal or smaller than the target stream top item');
|
|
1522
1715
|
}
|
|
1523
1716
|
}
|
|
1717
|
+
if (id === '0-0')
|
|
1718
|
+
throw new RedisCommandError('ERR The ID specified in XADD must be greater than 0-0');
|
|
1719
|
+
if (id.split('-').some((v) => BigInt(v) > 18446744073709551615n))
|
|
1720
|
+
throw new RedisCommandError('ERR The stream has exhausted the last possible ID, unable to add more items');
|
|
1524
1721
|
const pairs = [];
|
|
1525
1722
|
for (let j = 0; j < fields.length; j += 2)
|
|
1526
1723
|
pairs.push([fields[j], fields[j + 1]]);
|
|
1527
1724
|
entries.push([id, pairs]);
|
|
1528
|
-
space.put({ name: a[0], kind: STREAM, v: entries, pexpireAt: row?.pexpireAt ?? null, lastId: id }, 'stream.add');
|
|
1725
|
+
space.put({ name: a[0], kind: STREAM, v: trim ? trimEntries(entries, trim) : entries, pexpireAt: row?.pexpireAt ?? null, lastId: id }, 'stream.add');
|
|
1529
1726
|
return id;
|
|
1530
1727
|
}
|
|
1531
1728
|
// ── the script cache ───────────────────────────────────────────────────────────────────────────
|
|
@@ -1552,6 +1749,8 @@ function redisToLua(v) {
|
|
|
1552
1749
|
return false; // nil bulk reply → Lua `false`
|
|
1553
1750
|
if (typeof v === 'number' || typeof v === 'string')
|
|
1554
1751
|
return v;
|
|
1752
|
+
if (v instanceof RedisInteger)
|
|
1753
|
+
return Number(v.integer);
|
|
1555
1754
|
if (v instanceof RedisStatus) {
|
|
1556
1755
|
const t = new LuaTable();
|
|
1557
1756
|
t.set('ok', v.value); // status reply → Lua table {ok=…}
|
|
@@ -1659,5 +1858,5 @@ export async function execRedisRun(commands, ctx) {
|
|
|
1659
1858
|
}
|
|
1660
1859
|
const wrote = space.dirty;
|
|
1661
1860
|
await (space.ctx.dialect.storage?.flush(space) ?? flushKeySpace(space));
|
|
1662
|
-
return { items, wrote };
|
|
1861
|
+
return { items, wrote, instant: nowMs };
|
|
1663
1862
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RedisValue } from './engine.js';
|
|
2
|
+
export declare const binaryText: (b: Uint8Array) => string;
|
|
3
|
+
export declare const binaryBytes: (s: string) => Uint8Array;
|
|
4
|
+
export type Frame = {
|
|
5
|
+
value: any;
|
|
6
|
+
end: number;
|
|
7
|
+
error?: boolean;
|
|
8
|
+
type: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function readResp(s: string, start?: number): Frame | undefined;
|
|
11
|
+
export declare function respReply(value: RedisValue | {
|
|
12
|
+
error: string;
|
|
13
|
+
} | Record<string, any>): string;
|
|
14
|
+
export declare const commandFrame: (command: string[]) => Uint8Array;
|
|
15
|
+
export declare function inlineCommand(line: string): string[];
|
|
16
|
+
/** Carry RESP integer/status tags across an internal JSON request without losing int64 bytes. */
|
|
17
|
+
export declare function wireValue(value: any): any;
|