@volter/world-core 3.0.145 → 3.0.147
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.
|
@@ -1084,10 +1084,12 @@ export type SemanticsContext = {
|
|
|
1084
1084
|
/** A vendor's Redis (the kernel's Redis library, redis/engine.ts): a request's commands run in order over this call's
|
|
1085
1085
|
* tree and clock, in the pack's own service, under its wire's dialect, in one of its databases (`database`: its keys
|
|
1086
1086
|
* and scripts apart from the others'); each answered `{result}` or `{error}`, and whether any wrote. A read-only
|
|
1087
|
-
* World raises ReadOnlyError at the first write.
|
|
1087
|
+
* World raises ReadOnlyError at the first write. `transaction` distinguishes a wire's queued transaction from
|
|
1088
|
+
* a pipeline for script restrictions. The wire around it (Upstash's REST envelope) is the pack's. */
|
|
1088
1089
|
redis(commands: string[][], options: {
|
|
1089
1090
|
dialect: RedisDialect;
|
|
1090
1091
|
database?: string;
|
|
1092
|
+
transaction?: boolean;
|
|
1091
1093
|
}): Promise<{
|
|
1092
1094
|
items: RunItem[];
|
|
1093
1095
|
wrote: boolean;
|
package/dist/src/derived-core.js
CHANGED
|
@@ -1536,6 +1536,7 @@ export async function contextFor(m, call, scope) {
|
|
|
1536
1536
|
redis: (commands, options) => execRedisRun(commands, {
|
|
1537
1537
|
root, occurredAt: at, service: m.service, readOnly: scope.readOnly === true || isReadOnlyRequest(call.request), dialect: options.dialect,
|
|
1538
1538
|
...(options.database === undefined ? {} : { database: options.database }),
|
|
1539
|
+
...(options.transaction === undefined ? {} : { transaction: options.transaction }),
|
|
1539
1540
|
}),
|
|
1540
1541
|
blobs: {
|
|
1541
1542
|
put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
|
|
@@ -85,6 +85,8 @@ export type RedisContext = {
|
|
|
85
85
|
occurredAt: string;
|
|
86
86
|
/** When true, any write command raises `ReadOnlyError` (the wire answers its read-only refusal). */
|
|
87
87
|
readOnly?: boolean;
|
|
88
|
+
/** Commands queued in a wire transaction, rather than an ordinary pipeline. */
|
|
89
|
+
transaction?: boolean;
|
|
88
90
|
/** A database other than the service's first (Redis's SELECT n, a vendor's database id), whose keys and scripts
|
|
89
91
|
* are its own: subjects `db:<id>:key:<name>` and `db:<id>:script:<sha1>`. Absent: `key:<name>`, `script:<sha1>`. */
|
|
90
92
|
database?: string;
|
|
@@ -116,6 +118,9 @@ export type RedisDialect = {
|
|
|
116
118
|
lua: Required<Pick<LuaHost, 'extend' | 'numberArg'>>;
|
|
117
119
|
/** Whether a script that fails part-way keeps what it wrote (Redis) or leaves nothing (Upstash). */
|
|
118
120
|
scriptErrorsKeepWrites: boolean;
|
|
121
|
+
/** Wire-specific Lua flags. A flag installs a restriction before each command; the core owns `no-writes`.
|
|
122
|
+
* Flags absent here are refused, never ignored. A transaction is passed explicitly by the wire. */
|
|
123
|
+
scriptFlags?: Record<string, (keys: readonly string[], transaction: boolean) => (argv: string[]) => void>;
|
|
119
124
|
/** How the dialect's keys are laid out in the tree, when not one subject per key (`keyspaceImageOf`, `flushKeySpace`). */
|
|
120
125
|
storage?: {
|
|
121
126
|
image: (ctx: RedisContext) => KeyspaceImage;
|
|
@@ -137,6 +142,11 @@ export declare function wrongArity(name: string): string;
|
|
|
137
142
|
* a connection's (Redis) or a URL's (Upstash).
|
|
138
143
|
*/
|
|
139
144
|
export declare const SERVED_COMMANDS: Record<string, [number, number]>;
|
|
145
|
+
/** Redis hash tag: the first nonempty {...} pair, otherwise the entire key (an empty first pair is invalid). */
|
|
146
|
+
export declare function redisHashTag(key: string): string;
|
|
147
|
+
/** Key positions of the shared core's commands. Undefined means database-wide access, a script, or an unknown
|
|
148
|
+
* command; callers must refuse an unsupported restriction rather than guess that the command has no keys. */
|
|
149
|
+
export declare function redisCommandKeys(argv: readonly string[]): string[] | undefined;
|
|
140
150
|
/**
|
|
141
151
|
* Does this command MUTATE? Takes the whole argv, not just the name, because two commands are
|
|
142
152
|
* only writes for some of their subcommands. A read-only twin and `EVAL_RO` both key off this,
|
package/dist/src/redis/engine.js
CHANGED
|
@@ -49,6 +49,7 @@ import { treeChangesSince, treeStamp } from "../log.js";
|
|
|
49
49
|
import { applyTwinWrite } from "../serve.js";
|
|
50
50
|
import { getActiveWorldStore } from "../world-store.js";
|
|
51
51
|
import { LUA_NULL, luaScriptSha1, LuaError, LuaTable, runLua } from "./lua.js";
|
|
52
|
+
import { inlineCommand } from "./frames.js";
|
|
52
53
|
/**
|
|
53
54
|
* A Redis SIMPLE STATUS reply, kept distinct from a bulk string.
|
|
54
55
|
*
|
|
@@ -138,6 +139,60 @@ export const SERVED_COMMANDS = {
|
|
|
138
139
|
XADD: [4, -1], XLEN: [1, 1], XRANGE: [3, -1], XREVRANGE: [3, -1], XDEL: [2, -1],
|
|
139
140
|
SCRIPT: [1, -1], EVAL: [2, -1], EVAL_RO: [2, -1], EVALSHA: [2, -1], EVALSHA_RO: [2, -1],
|
|
140
141
|
};
|
|
142
|
+
/** Redis hash tag: the first nonempty {...} pair, otherwise the entire key (an empty first pair is invalid). */
|
|
143
|
+
export function redisHashTag(key) {
|
|
144
|
+
const start = key.indexOf('{');
|
|
145
|
+
const end = start < 0 ? -1 : key.indexOf('}', start + 1);
|
|
146
|
+
return end > start + 1 && start >= 0 ? key.slice(start + 1, end) : key;
|
|
147
|
+
}
|
|
148
|
+
/** Key positions of the shared core's commands. Undefined means database-wide access, a script, or an unknown
|
|
149
|
+
* command; callers must refuse an unsupported restriction rather than guess that the command has no keys. */
|
|
150
|
+
export function redisCommandKeys(argv) {
|
|
151
|
+
const name = (argv[0] ?? '').toUpperCase();
|
|
152
|
+
if (['PING', 'ECHO', 'SCRIPT'].includes(name))
|
|
153
|
+
return [];
|
|
154
|
+
if (['EXISTS', 'TOUCH', 'DEL', 'UNLINK', 'MGET'].includes(name))
|
|
155
|
+
return argv.slice(1);
|
|
156
|
+
if (name === 'MSET' || name === 'MSETNX')
|
|
157
|
+
return argv.slice(1).filter((_, i) => i % 2 === 0);
|
|
158
|
+
if (['RENAME', 'RENAMENX', 'RPOPLPUSH'].includes(name))
|
|
159
|
+
return argv.slice(1, 3);
|
|
160
|
+
if (['DBSIZE', 'FLUSHDB', 'FLUSHALL', 'KEYS', 'RANDOMKEY', 'SCAN', 'EVAL', 'EVALSHA', 'EVAL_RO', 'EVALSHA_RO'].includes(name))
|
|
161
|
+
return undefined;
|
|
162
|
+
return SERVED_COMMANDS[name] && argv[1] !== undefined ? [argv[1]] : undefined;
|
|
163
|
+
}
|
|
164
|
+
/** EVAL shebang metadata: keep the newline for Lua line numbers and the original script for cache identity.
|
|
165
|
+
* source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/eval.c (evalExtractShebangFlags). */
|
|
166
|
+
function redisScript(script, dialect) {
|
|
167
|
+
const flags = new Set();
|
|
168
|
+
if (!script.startsWith('#!'))
|
|
169
|
+
return { body: script, flags };
|
|
170
|
+
const newline = script.indexOf('\n');
|
|
171
|
+
if (newline < 0)
|
|
172
|
+
throw new RedisCommandError('ERR Invalid script shebang');
|
|
173
|
+
let parts;
|
|
174
|
+
try {
|
|
175
|
+
parts = inlineCommand(script.slice(0, newline));
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
throw new RedisCommandError('ERR Invalid engine in script shebang');
|
|
179
|
+
}
|
|
180
|
+
if (parts[0] !== '#!lua')
|
|
181
|
+
throw new RedisCommandError(`ERR Unexpected engine in script shebang: ${parts[0] ?? ''}`);
|
|
182
|
+
for (const option of parts.slice(1)) {
|
|
183
|
+
if (!option.startsWith('flags='))
|
|
184
|
+
throw new RedisCommandError(`ERR Unknown lua shebang option: ${option}`);
|
|
185
|
+
for (const flag of option.slice(6).split(',')) {
|
|
186
|
+
if (!flag && option === 'flags=')
|
|
187
|
+
continue;
|
|
188
|
+
if (flag !== 'no-writes' && !Object.hasOwn(dialect.scriptFlags ?? {}, flag)) {
|
|
189
|
+
throw new RedisCommandError(dialect.unmodeled(`Lua script flag ${flag}`));
|
|
190
|
+
}
|
|
191
|
+
flags.add(flag);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { body: script.slice(newline), flags };
|
|
195
|
+
}
|
|
141
196
|
/** Commands that mutate state — refused by a read-only twin and inside `EVAL_RO`/`EVALSHA_RO`. */
|
|
142
197
|
const WRITE_COMMANDS = new Set([
|
|
143
198
|
'SET', 'SETEX', 'PSETEX', 'SETNX', 'MSET', 'MSETNX', 'GETSET', 'GETDEL', 'GETEX', 'APPEND',
|
|
@@ -1366,6 +1421,7 @@ export function execOne(space, argv) {
|
|
|
1366
1421
|
if (sub === 'LOAD') {
|
|
1367
1422
|
if (a.length !== 2)
|
|
1368
1423
|
throw new RedisCommandError(wrongArity('script'));
|
|
1424
|
+
redisScript(a[1], dialect);
|
|
1369
1425
|
return storeScript(space, a[1]);
|
|
1370
1426
|
}
|
|
1371
1427
|
if (sub === 'EXISTS')
|
|
@@ -1400,6 +1456,7 @@ export function execOne(space, argv) {
|
|
|
1400
1456
|
}
|
|
1401
1457
|
else {
|
|
1402
1458
|
script = a[0];
|
|
1459
|
+
redisScript(script, dialect); // an invalid/unsupported header must not enter the script cache
|
|
1403
1460
|
// Caching is a WRITE, so a read-only twin must not attempt it — otherwise `EVAL_RO`, which
|
|
1404
1461
|
// is a read by definition, would 405 on the cache rather than on anything it does (§9
|
|
1405
1462
|
// round 1). The cost is that a read-only twin cannot serve a later EVALSHA for this
|
|
@@ -1797,15 +1854,23 @@ function luaToRedis(v) {
|
|
|
1797
1854
|
function evalScript(space, script, keys, args, readOnly) {
|
|
1798
1855
|
const before = space.snapshot();
|
|
1799
1856
|
try {
|
|
1800
|
-
const
|
|
1857
|
+
const { body, flags } = redisScript(script, space.ctx.dialect);
|
|
1858
|
+
const guards = [...flags].filter(flag => flag !== 'no-writes')
|
|
1859
|
+
.map(flag => space.ctx.dialect.scriptFlags[flag](keys, space.ctx.transaction === true));
|
|
1860
|
+
const result = runLua(body, {
|
|
1801
1861
|
keys,
|
|
1802
1862
|
argv: args,
|
|
1803
1863
|
call: (callArgs, line) => {
|
|
1804
1864
|
const cmd = String(callArgs[0] ?? '').toUpperCase();
|
|
1805
1865
|
// Redis's refusal, as EVAL_RO's page prints it (redis.io/docs/latest/commands/eval_ro/): the script by its SHA1
|
|
1806
1866
|
// and the line of the call
|
|
1807
|
-
if (readOnly && isWriteCommand(cmd, callArgs.slice(1), space.ctx.dialect))
|
|
1867
|
+
if ((readOnly || flags.has('no-writes')) && isWriteCommand(cmd, callArgs.slice(1), space.ctx.dialect))
|
|
1808
1868
|
throw new RedisCommandError(`ERR Error running script (call to ${luaScriptSha1(script)}): @user_script:${line}: @user_script: ${line}: Write commands are not allowed from read-only scripts.`);
|
|
1869
|
+
const shape = commandShapeError(callArgs, space.ctx.dialect);
|
|
1870
|
+
if (shape !== null)
|
|
1871
|
+
throw new RedisCommandError(shape);
|
|
1872
|
+
for (const guard of guards)
|
|
1873
|
+
guard(callArgs);
|
|
1809
1874
|
return redisToLua(execOne(space, callArgs));
|
|
1810
1875
|
},
|
|
1811
1876
|
...space.ctx.dialect.lua,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-core",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.147",
|
|
4
4
|
"description": "The kernel of Volter World: one log per twin, branches as pointers, checkpoints, the fold that keeps a twin current, the head that performs a write against the vendor, references, and the git plane. A twin package builds on it; the runtime serves it.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
package/src/derived-core.ts
CHANGED
|
@@ -1873,8 +1873,9 @@ export type SemanticsContext = {
|
|
|
1873
1873
|
/** A vendor's Redis (the kernel's Redis library, redis/engine.ts): a request's commands run in order over this call's
|
|
1874
1874
|
* tree and clock, in the pack's own service, under its wire's dialect, in one of its databases (`database`: its keys
|
|
1875
1875
|
* and scripts apart from the others'); each answered `{result}` or `{error}`, and whether any wrote. A read-only
|
|
1876
|
-
* World raises ReadOnlyError at the first write.
|
|
1877
|
-
|
|
1876
|
+
* World raises ReadOnlyError at the first write. `transaction` distinguishes a wire's queued transaction from
|
|
1877
|
+
* a pipeline for script restrictions. The wire around it (Upstash's REST envelope) is the pack's. */
|
|
1878
|
+
redis(commands: string[][], options: { dialect: RedisDialect; database?: string; transaction?: boolean }): Promise<{ items: RunItem[]; wrote: boolean }>;
|
|
1878
1879
|
/** Another vendor's own URL answered by the World's twin of it (vendor-call.ts): a vendor's call to another as part of
|
|
1879
1880
|
* its behaviour (Clerk exchanging a Google sign-in's code). A vendor the World runs no twin of is refused
|
|
1880
1881
|
* (VendorUnreachableError); nothing leaves the World. */
|
|
@@ -2136,6 +2137,7 @@ export async function contextFor(m: DerivedManifest, call: DerivedCall, scope: C
|
|
|
2136
2137
|
redis: (commands, options) => execRedisRun(commands, {
|
|
2137
2138
|
root, occurredAt: at, service: m.service, readOnly: scope.readOnly === true || isReadOnlyRequest(call.request), dialect: options.dialect,
|
|
2138
2139
|
...(options.database === undefined ? {} : { database: options.database }),
|
|
2140
|
+
...(options.transaction === undefined ? {} : { transaction: options.transaction }),
|
|
2139
2141
|
}),
|
|
2140
2142
|
blobs: {
|
|
2141
2143
|
put: (key, bytes) => writeResourceBlob(m.service, key, bytes, root),
|
package/src/redis/engine.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { treeChangesSince, treeStamp } from '../log.ts';
|
|
|
49
49
|
import { applyTwinWrite, type TwinResource } from '../serve.ts';
|
|
50
50
|
import { getActiveWorldStore } from '../world-store.ts';
|
|
51
51
|
import { LUA_NULL, luaScriptSha1, LuaError, LuaTable, runLua, type LuaHost, type LuaValue } from './lua.ts';
|
|
52
|
+
import { inlineCommand } from './frames.ts';
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* A Redis SIMPLE STATUS reply, kept distinct from a bulk string.
|
|
@@ -147,6 +148,8 @@ export type RedisContext = {
|
|
|
147
148
|
occurredAt: string;
|
|
148
149
|
/** When true, any write command raises `ReadOnlyError` (the wire answers its read-only refusal). */
|
|
149
150
|
readOnly?: boolean;
|
|
151
|
+
/** Commands queued in a wire transaction, rather than an ordinary pipeline. */
|
|
152
|
+
transaction?: boolean;
|
|
150
153
|
/** A database other than the service's first (Redis's SELECT n, a vendor's database id), whose keys and scripts
|
|
151
154
|
* are its own: subjects `db:<id>:key:<name>` and `db:<id>:script:<sha1>`. Absent: `key:<name>`, `script:<sha1>`. */
|
|
152
155
|
database?: string;
|
|
@@ -179,6 +182,9 @@ export type RedisDialect = {
|
|
|
179
182
|
lua: Required<Pick<LuaHost, 'extend' | 'numberArg'>>;
|
|
180
183
|
/** Whether a script that fails part-way keeps what it wrote (Redis) or leaves nothing (Upstash). */
|
|
181
184
|
scriptErrorsKeepWrites: boolean;
|
|
185
|
+
/** Wire-specific Lua flags. A flag installs a restriction before each command; the core owns `no-writes`.
|
|
186
|
+
* Flags absent here are refused, never ignored. A transaction is passed explicitly by the wire. */
|
|
187
|
+
scriptFlags?: Record<string, (keys: readonly string[], transaction: boolean) => (argv: string[]) => void>;
|
|
182
188
|
/** How the dialect's keys are laid out in the tree, when not one subject per key (`keyspaceImageOf`, `flushKeySpace`). */
|
|
183
189
|
storage?: { image: (ctx: RedisContext) => KeyspaceImage; flush: (space: KeySpace) => Promise<void> };
|
|
184
190
|
};
|
|
@@ -233,6 +239,49 @@ export const SERVED_COMMANDS: Record<string, [number, number]> = {
|
|
|
233
239
|
SCRIPT: [1, -1], EVAL: [2, -1], EVAL_RO: [2, -1], EVALSHA: [2, -1], EVALSHA_RO: [2, -1],
|
|
234
240
|
};
|
|
235
241
|
|
|
242
|
+
/** Redis hash tag: the first nonempty {...} pair, otherwise the entire key (an empty first pair is invalid). */
|
|
243
|
+
export function redisHashTag(key: string): string {
|
|
244
|
+
const start = key.indexOf('{');
|
|
245
|
+
const end = start < 0 ? -1 : key.indexOf('}', start + 1);
|
|
246
|
+
return end > start + 1 && start >= 0 ? key.slice(start + 1, end) : key;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Key positions of the shared core's commands. Undefined means database-wide access, a script, or an unknown
|
|
250
|
+
* command; callers must refuse an unsupported restriction rather than guess that the command has no keys. */
|
|
251
|
+
export function redisCommandKeys(argv: readonly string[]): string[] | undefined {
|
|
252
|
+
const name = (argv[0] ?? '').toUpperCase();
|
|
253
|
+
if (['PING', 'ECHO', 'SCRIPT'].includes(name)) return [];
|
|
254
|
+
if (['EXISTS', 'TOUCH', 'DEL', 'UNLINK', 'MGET'].includes(name)) return argv.slice(1);
|
|
255
|
+
if (name === 'MSET' || name === 'MSETNX') return argv.slice(1).filter((_, i) => i % 2 === 0);
|
|
256
|
+
if (['RENAME', 'RENAMENX', 'RPOPLPUSH'].includes(name)) return argv.slice(1, 3);
|
|
257
|
+
if (['DBSIZE', 'FLUSHDB', 'FLUSHALL', 'KEYS', 'RANDOMKEY', 'SCAN', 'EVAL', 'EVALSHA', 'EVAL_RO', 'EVALSHA_RO'].includes(name)) return undefined;
|
|
258
|
+
return SERVED_COMMANDS[name] && argv[1] !== undefined ? [argv[1]] : undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** EVAL shebang metadata: keep the newline for Lua line numbers and the original script for cache identity.
|
|
262
|
+
* source: https://raw.githubusercontent.com/redis/redis/c9d29f6a918c335bc1778d9f68e521c1bbb36a0f/src/eval.c (evalExtractShebangFlags). */
|
|
263
|
+
function redisScript(script: string, dialect: RedisDialect): { body: string; flags: Set<string> } {
|
|
264
|
+
const flags = new Set<string>();
|
|
265
|
+
if (!script.startsWith('#!')) return { body: script, flags };
|
|
266
|
+
const newline = script.indexOf('\n');
|
|
267
|
+
if (newline < 0) throw new RedisCommandError('ERR Invalid script shebang');
|
|
268
|
+
let parts: string[];
|
|
269
|
+
try { parts = inlineCommand(script.slice(0, newline)); }
|
|
270
|
+
catch { throw new RedisCommandError('ERR Invalid engine in script shebang'); }
|
|
271
|
+
if (parts[0] !== '#!lua') throw new RedisCommandError(`ERR Unexpected engine in script shebang: ${parts[0] ?? ''}`);
|
|
272
|
+
for (const option of parts.slice(1)) {
|
|
273
|
+
if (!option.startsWith('flags=')) throw new RedisCommandError(`ERR Unknown lua shebang option: ${option}`);
|
|
274
|
+
for (const flag of option.slice(6).split(',')) {
|
|
275
|
+
if (!flag && option === 'flags=') continue;
|
|
276
|
+
if (flag !== 'no-writes' && !Object.hasOwn(dialect.scriptFlags ?? {}, flag)) {
|
|
277
|
+
throw new RedisCommandError(dialect.unmodeled(`Lua script flag ${flag}`));
|
|
278
|
+
}
|
|
279
|
+
flags.add(flag);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return { body: script.slice(newline), flags };
|
|
283
|
+
}
|
|
284
|
+
|
|
236
285
|
/** Commands that mutate state — refused by a read-only twin and inside `EVAL_RO`/`EVALSHA_RO`. */
|
|
237
286
|
const WRITE_COMMANDS = new Set([
|
|
238
287
|
'SET', 'SETEX', 'PSETEX', 'SETNX', 'MSET', 'MSETNX', 'GETSET', 'GETDEL', 'GETEX', 'APPEND',
|
|
@@ -1185,7 +1234,7 @@ export function execOne(space: KeySpace, argv: string[]): RedisValue {
|
|
|
1185
1234
|
// ── scripting ────────────────────────────────────────────────────────────────────────────
|
|
1186
1235
|
case 'SCRIPT': {
|
|
1187
1236
|
const sub = a[0]!.toUpperCase();
|
|
1188
|
-
if (sub === 'LOAD') { if (a.length !== 2) throw new RedisCommandError(wrongArity('script')); return storeScript(space, a[1]!); }
|
|
1237
|
+
if (sub === 'LOAD') { if (a.length !== 2) throw new RedisCommandError(wrongArity('script')); redisScript(a[1]!, dialect); return storeScript(space, a[1]!); }
|
|
1189
1238
|
if (sub === 'EXISTS') return a.slice(1).map((sha) => (space.hasScript(sha.toLowerCase()) ? 1 : 0));
|
|
1190
1239
|
if (sub === 'FLUSH') { space.flushScripts(); return OK; }
|
|
1191
1240
|
throw new RedisCommandError(`ERR Unknown SCRIPT subcommand or wrong number of arguments for '${a[0]}'`);
|
|
@@ -1207,6 +1256,7 @@ export function execOne(space: KeySpace, argv: string[]): RedisValue {
|
|
|
1207
1256
|
script = found;
|
|
1208
1257
|
} else {
|
|
1209
1258
|
script = a[0]!;
|
|
1259
|
+
redisScript(script, dialect); // an invalid/unsupported header must not enter the script cache
|
|
1210
1260
|
// Caching is a WRITE, so a read-only twin must not attempt it — otherwise `EVAL_RO`, which
|
|
1211
1261
|
// is a read by definition, would 405 on the cache rather than on anything it does (§9
|
|
1212
1262
|
// round 1). The cost is that a read-only twin cannot serve a later EVALSHA for this
|
|
@@ -1511,14 +1561,20 @@ function luaToRedis(v: LuaValue): RedisValue {
|
|
|
1511
1561
|
function evalScript(space: KeySpace, script: string, keys: string[], args: string[], readOnly: boolean): RedisValue {
|
|
1512
1562
|
const before = space.snapshot();
|
|
1513
1563
|
try {
|
|
1514
|
-
const
|
|
1564
|
+
const { body, flags } = redisScript(script, space.ctx.dialect);
|
|
1565
|
+
const guards = [...flags].filter(flag => flag !== 'no-writes')
|
|
1566
|
+
.map(flag => space.ctx.dialect.scriptFlags![flag]!(keys, space.ctx.transaction === true));
|
|
1567
|
+
const result = runLua(body, {
|
|
1515
1568
|
keys,
|
|
1516
1569
|
argv: args,
|
|
1517
1570
|
call: (callArgs, line) => {
|
|
1518
1571
|
const cmd = String(callArgs[0] ?? '').toUpperCase();
|
|
1519
1572
|
// Redis's refusal, as EVAL_RO's page prints it (redis.io/docs/latest/commands/eval_ro/): the script by its SHA1
|
|
1520
1573
|
// and the line of the call
|
|
1521
|
-
if (readOnly && isWriteCommand(cmd, callArgs.slice(1), space.ctx.dialect)) throw new RedisCommandError(`ERR Error running script (call to ${luaScriptSha1(script)}): @user_script:${line}: @user_script: ${line}: Write commands are not allowed from read-only scripts.`);
|
|
1574
|
+
if ((readOnly || flags.has('no-writes')) && isWriteCommand(cmd, callArgs.slice(1), space.ctx.dialect)) throw new RedisCommandError(`ERR Error running script (call to ${luaScriptSha1(script)}): @user_script:${line}: @user_script: ${line}: Write commands are not allowed from read-only scripts.`);
|
|
1575
|
+
const shape = commandShapeError(callArgs, space.ctx.dialect);
|
|
1576
|
+
if (shape !== null) throw new RedisCommandError(shape);
|
|
1577
|
+
for (const guard of guards) guard(callArgs);
|
|
1522
1578
|
return redisToLua(execOne(space, callArgs));
|
|
1523
1579
|
},
|
|
1524
1580
|
...space.ctx.dialect.lua,
|
|
@@ -1570,4 +1626,3 @@ export async function execRedisRun(commands: string[][], ctx: RedisContext): Pro
|
|
|
1570
1626
|
await (space.ctx.dialect.storage?.flush(space) ?? flushKeySpace(space));
|
|
1571
1627
|
return { items, wrote, instant: nowMs };
|
|
1572
1628
|
}
|
|
1573
|
-
|