upstash-lua 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ import type { RedisLike } from "./redis-like.ts";
2
+ /**
3
+ * Checks if an error is a Redis NOSCRIPT error.
4
+ *
5
+ * NOSCRIPT errors occur when attempting to execute a script via EVALSHA
6
+ * but the script has not been loaded into the Redis script cache.
7
+ *
8
+ * @param error - The error to check
9
+ * @returns True if the error indicates the script is not cached
10
+ *
11
+ * @since 0.1.0
12
+ */
13
+ export declare function isNoScriptError(error: unknown): boolean;
14
+ /**
15
+ * Ensures a script is loaded on the Redis server.
16
+ *
17
+ * This function handles concurrent requests - if multiple calls arrive
18
+ * for the same script before it's loaded, they all share the same
19
+ * SCRIPT LOAD operation.
20
+ *
21
+ * @param redis - The Redis client
22
+ * @param sha - The expected SHA1 hash of the script
23
+ * @param script - The Lua script source code
24
+ * @returns Promise that resolves when the script is loaded
25
+ *
26
+ * @since 0.1.0
27
+ */
28
+ export declare function ensureLoaded(redis: RedisLike, sha: string, script: string): Promise<void>;
29
+ /**
30
+ * Options for executing a script with caching.
31
+ *
32
+ * @since 0.1.0
33
+ */
34
+ export interface EvalWithCacheOptions {
35
+ /** The Lua script source code */
36
+ readonly script: string;
37
+ /** The SHA1 hash of the script */
38
+ readonly sha: string;
39
+ /** Array of Redis keys (KEYS[1], KEYS[2], etc.) */
40
+ readonly keys: string[];
41
+ /** Array of arguments (ARGV[1], ARGV[2], etc.) */
42
+ readonly args: string[];
43
+ }
44
+ /**
45
+ * Executes a Lua script using EVALSHA with automatic NOSCRIPT fallback.
46
+ *
47
+ * This function implements the optimal execution strategy:
48
+ * 1. Try EVALSHA first (most efficient, assumes script is cached)
49
+ * 2. On NOSCRIPT error, load the script via SCRIPT LOAD
50
+ * 3. Retry EVALSHA
51
+ *
52
+ * The script loading is cached per-client, so subsequent calls for the
53
+ * same script won't trigger additional SCRIPT LOAD operations. Concurrent
54
+ * calls for the same script share a single SCRIPT LOAD.
55
+ *
56
+ * @param redis - The Redis client to execute on
57
+ * @param options - Script and parameters
58
+ * @returns Promise resolving to the script's return value
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * const result = await evalWithCache(redis, {
63
+ * script: 'return redis.call("GET", KEYS[1])',
64
+ * sha: "abc123...",
65
+ * keys: ["mykey"],
66
+ * args: [],
67
+ * })
68
+ * ```
69
+ *
70
+ * @since 0.1.0
71
+ */
72
+ export declare function evalWithCache(redis: RedisLike, options: EvalWithCacheOptions): Promise<unknown>;
73
+ //# sourceMappingURL=eval-with-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eval-with-cache.d.ts","sourceRoot":"","sources":["../src/eval-with-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAahD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQvD;AAiBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB/F;AAED;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,iCAAiC;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,kCAAkC;IAClC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;IACvB,kDAAkD;IAClD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CACxB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,OAAO,CAAC,CAalB"}
@@ -0,0 +1,48 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import type { AnyStandardSchema, StdOutput } from "./types.ts";
3
+ /**
4
+ * Wraps a StandardSchemaV1 object schema to accept HGETALL-style array input.
5
+ *
6
+ * Redis commands like `HGETALL` return flat arrays of alternating key-value pairs:
7
+ * `["field1", "value1", "field2", "value2"]`
8
+ *
9
+ * This helper converts that format to an object before validating with your schema,
10
+ * enabling a much cleaner DX:
11
+ *
12
+ * @typeParam S - The inner schema type (must accept object input)
13
+ * @param schema - A StandardSchemaV1 schema that validates objects
14
+ * @returns A new schema that accepts `unknown[]` and outputs the validated type
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { z } from "zod"
19
+ * import { defineScript, hashResult } from "upstash-lua"
20
+ *
21
+ * const getUser = defineScript({
22
+ * name: "getUser",
23
+ * lua: `return redis.call("HGETALL", KEYS[1])`,
24
+ * keys: { key: z.string() },
25
+ * returns: hashResult(z.object({
26
+ * name: z.string(),
27
+ * email: z.string(),
28
+ * age: z.coerce.number(),
29
+ * })),
30
+ * })
31
+ *
32
+ * // Result is typed as { name: string, email: string, age: number }
33
+ * const user = await getUser.run(redis, { keys: { key: "user:123" } })
34
+ * ```
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * // Works with partial/optional fields too
39
+ * returns: hashResult(z.object({
40
+ * name: z.string(),
41
+ * email: z.string().optional(),
42
+ * }).partial())
43
+ * ```
44
+ *
45
+ * @since 0.3.0
46
+ */
47
+ export declare function hashResult<S extends AnyStandardSchema>(schema: S): StandardSchemaV1<unknown[], StdOutput<S>>;
48
+ //# sourceMappingURL=hash-result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hash-result.d.ts","sourceRoot":"","sources":["../src/hash-result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,KAAK,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AA6C9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,iBAAiB,EACpD,MAAM,EAAE,CAAC,GACR,gBAAgB,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAiC3C"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * upstash-lua - Type-safe Lua scripts for Upstash Redis
3
+ *
4
+ * This library provides a `defineScript()` function for creating type-safe
5
+ * Lua scripts with StandardSchemaV1 validation (Zod, Effect Schema, ArkType, etc.).
6
+ *
7
+ * Features:
8
+ * - Full TypeScript inference for keys, args, and return values
9
+ * - Input validation and transformation using StandardSchemaV1 schemas
10
+ * - Efficient EVALSHA execution with automatic NOSCRIPT fallback
11
+ * - Universal runtime support (Node.js 18+, Bun, Edge runtimes)
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { z } from "zod"
16
+ * import { defineScript } from "upstash-lua"
17
+ * import { Redis } from "@upstash/redis"
18
+ *
19
+ * const redis = new Redis({ url: "...", token: "..." })
20
+ *
21
+ * const rateLimit = defineScript({
22
+ * name: "rateLimit",
23
+ * lua: `
24
+ * local current = redis.call("INCR", KEYS[1])
25
+ * if current == 1 then
26
+ * redis.call("EXPIRE", KEYS[1], ARGV[2])
27
+ * end
28
+ * return { current <= tonumber(ARGV[1]) and 1 or 0, tonumber(ARGV[1]) - current }
29
+ * `,
30
+ * keys: { key: z.string() },
31
+ * args: {
32
+ * limit: z.number().int().positive().transform(String),
33
+ * windowSeconds: z.number().int().positive().transform(String),
34
+ * },
35
+ * returns: z.tuple([z.number(), z.number()]).transform(([allowed, rem]) => ({
36
+ * allowed: allowed === 1,
37
+ * remaining: rem,
38
+ * })),
39
+ * })
40
+ *
41
+ * const result = await rateLimit.run(redis, {
42
+ * keys: { key: "rl:user:123" },
43
+ * args: { limit: 10, windowSeconds: 60 },
44
+ * })
45
+ * // result: { allowed: boolean, remaining: number }
46
+ * ```
47
+ *
48
+ * @packageDocumentation
49
+ * @module upstash-lua
50
+ * @since 0.1.0
51
+ */
52
+ export { VERSION } from "./version.ts";
53
+ export { defineScript } from "./define-script.ts";
54
+ export type { Script, DefineScriptBase, LuaFunction } from "./define-script.ts";
55
+ export { lua } from "./lua-template.ts";
56
+ export type { CompiledLua, LuaToken, TokenProxy } from "./lua-template.ts";
57
+ export type { RedisLike } from "./redis-like.ts";
58
+ export { hashResult } from "./hash-result.ts";
59
+ export type { AnyStandardSchema, StdInput, StdOutput, StringOutSchema, StringSchemaRecord, InputsOf, OutputsOf, ScriptCallInput, ScriptCallArgs, } from "./types.ts";
60
+ export { validateStandard, parseStandard, } from "./standard-schema.ts";
61
+ export type { ValidationSuccess, ValidationFailure, ValidationResult, ParseContext, } from "./standard-schema.ts";
62
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAGtC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACjD,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAG/E,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAA;AACvC,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAG1E,YAAY,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAGhD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAG7C,YAAY,EACV,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,eAAe,EACf,kBAAkB,EAClB,QAAQ,EACR,SAAS,EACT,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAA;AAGnB,OAAO,EACL,gBAAgB,EAChB,aAAa,GACd,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EACV,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,YAAY,GACb,MAAM,sBAAsB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,304 @@
1
+ // src/version.ts
2
+ var VERSION = "0.3.0";
3
+ // src/standard-schema.ts
4
+ async function validateStandard(schema, value) {
5
+ const result = schema["~standard"].validate(value);
6
+ const resolved = result instanceof Promise ? await result : result;
7
+ if ("issues" in resolved && resolved.issues) {
8
+ return {
9
+ ok: false,
10
+ issues: resolved.issues.map((issue) => ({
11
+ message: issue.message,
12
+ path: issue.path?.map((p) => typeof p === "object" && p !== null && ("key" in p) ? p.key : p)
13
+ }))
14
+ };
15
+ }
16
+ return {
17
+ ok: true,
18
+ value: resolved.value
19
+ };
20
+ }
21
+ async function parseStandard(schema, value, context) {
22
+ const result = await validateStandard(schema, value);
23
+ if (!result.ok) {
24
+ if (context.type === "input") {
25
+ const issueMessages = result.issues.map((i) => i.message).join(", ");
26
+ throw new Error(`[upstash-lua@${VERSION}] Script "${context.scriptName}" input validation failed at "${context.path}": ${issueMessages}`);
27
+ } else {
28
+ const issueMessages = result.issues.map((i) => i.message).join(", ");
29
+ throw new Error(`[upstash-lua@${VERSION}] Script "${context.scriptName}" return validation failed: ${issueMessages}`);
30
+ }
31
+ }
32
+ return result.value;
33
+ }
34
+
35
+ // src/eval-with-cache.ts
36
+ var loadCache = new WeakMap;
37
+ function isNoScriptError(error) {
38
+ if (error instanceof Error) {
39
+ return error.message.toUpperCase().includes("NOSCRIPT");
40
+ }
41
+ if (typeof error === "string") {
42
+ return error.toUpperCase().includes("NOSCRIPT");
43
+ }
44
+ return false;
45
+ }
46
+ function getClientCache(redis) {
47
+ let cache = loadCache.get(redis);
48
+ if (!cache) {
49
+ cache = new Map;
50
+ loadCache.set(redis, cache);
51
+ }
52
+ return cache;
53
+ }
54
+ async function ensureLoaded(redis, sha, script) {
55
+ const cache = getClientCache(redis);
56
+ const existing = cache.get(sha);
57
+ if (existing) {
58
+ return existing;
59
+ }
60
+ const loadPromise = (async () => {
61
+ await redis.scriptLoad(script);
62
+ })();
63
+ cache.set(sha, loadPromise);
64
+ try {
65
+ await loadPromise;
66
+ } catch (error) {
67
+ cache.delete(sha);
68
+ throw error;
69
+ }
70
+ }
71
+ async function evalWithCache(redis, options) {
72
+ const { script, sha, keys, args } = options;
73
+ try {
74
+ return await redis.evalsha(sha, keys, args);
75
+ } catch (error) {
76
+ if (!isNoScriptError(error)) {
77
+ throw error;
78
+ }
79
+ await ensureLoaded(redis, sha, script);
80
+ return await redis.evalsha(sha, keys, args);
81
+ }
82
+ }
83
+
84
+ // src/sha1.ts
85
+ async function sha1Hex(text) {
86
+ if (typeof crypto === "undefined" || !crypto.subtle) {
87
+ throw new Error("[upstash-lua] WebCrypto (crypto.subtle) is not available. " + "This library requires Node.js 18+, Bun, or an Edge runtime.");
88
+ }
89
+ const encoder = new TextEncoder;
90
+ const data = encoder.encode(text);
91
+ const hashBuffer = await crypto.subtle.digest("SHA-1", data);
92
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
93
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
94
+ }
95
+
96
+ // src/lua-template.ts
97
+ var LUA_TOKEN = Symbol("LUA_TOKEN");
98
+ function isLuaToken(value) {
99
+ return typeof value === "object" && value !== null && LUA_TOKEN in value && value[LUA_TOKEN] === true;
100
+ }
101
+ function isCompiledLua(value) {
102
+ return typeof value === "object" && value !== null && LUA_TOKEN in value && value[LUA_TOKEN] === "compiled";
103
+ }
104
+ function createTokenProxy(kind) {
105
+ return new Proxy({}, {
106
+ get(_target, prop) {
107
+ if (typeof prop !== "string") {
108
+ throw new TypeError(`[upstash-lua] ${kind.toUpperCase()}S proxy only accepts string keys`);
109
+ }
110
+ return {
111
+ [LUA_TOKEN]: true,
112
+ kind,
113
+ name: prop
114
+ };
115
+ }
116
+ });
117
+ }
118
+ function lua(strings, ...tokens) {
119
+ for (let i = 0;i < tokens.length; i++) {
120
+ const token = tokens[i];
121
+ if (!isLuaToken(token)) {
122
+ throw new TypeError(`[upstash-lua] lua template only accepts KEYS.* or ARGV.* interpolations. ` + `Got ${typeof token} at position ${i + 1}.`);
123
+ }
124
+ }
125
+ return {
126
+ [LUA_TOKEN]: "compiled",
127
+ strings,
128
+ tokens
129
+ };
130
+ }
131
+ function compileLua(compiled, keyNames, argNames) {
132
+ const { strings, tokens } = compiled;
133
+ const keyIndexMap = new Map;
134
+ for (let i = 0;i < keyNames.length; i++) {
135
+ keyIndexMap.set(keyNames[i], i + 1);
136
+ }
137
+ const argIndexMap = new Map;
138
+ for (let i = 0;i < argNames.length; i++) {
139
+ argIndexMap.set(argNames[i], i + 1);
140
+ }
141
+ let result = strings[0] ?? "";
142
+ for (let i = 0;i < tokens.length; i++) {
143
+ const token = tokens[i];
144
+ let replacement;
145
+ if (token.kind === "key") {
146
+ const index = keyIndexMap.get(token.name);
147
+ if (index === undefined) {
148
+ throw new Error(`[upstash-lua] Unknown key "${token.name}" in lua template. ` + `Available keys: ${keyNames.length > 0 ? keyNames.join(", ") : "(none)"}`);
149
+ }
150
+ replacement = `KEYS[${index}]`;
151
+ } else {
152
+ const index = argIndexMap.get(token.name);
153
+ if (index === undefined) {
154
+ throw new Error(`[upstash-lua] Unknown arg "${token.name}" in lua template. ` + `Available args: ${argNames.length > 0 ? argNames.join(", ") : "(none)"}`);
155
+ }
156
+ replacement = `ARGV[${index}]`;
157
+ }
158
+ result += replacement + (strings[i + 1] ?? "");
159
+ }
160
+ return result;
161
+ }
162
+
163
+ // src/define-script.ts
164
+ async function validateAndCollect(scriptName, keySchemas, argSchemas, keyNames, argNames, input) {
165
+ const keysArray = [];
166
+ const argsArray = [];
167
+ for (const keyName of keyNames) {
168
+ const schema = keySchemas[keyName];
169
+ const value = input.keys?.[keyName];
170
+ const validated = await parseStandard(schema, value, {
171
+ scriptName,
172
+ path: `keys.${keyName}`,
173
+ type: "input"
174
+ });
175
+ if (typeof validated !== "string") {
176
+ throw new TypeError(`[upstash-lua] Key "${keyName}" schema must output a string, got ${typeof validated}`);
177
+ }
178
+ keysArray.push(validated);
179
+ }
180
+ for (const argName of argNames) {
181
+ const schema = argSchemas[argName];
182
+ const value = input.args?.[argName];
183
+ const validated = await parseStandard(schema, value, {
184
+ scriptName,
185
+ path: `args.${argName}`,
186
+ type: "input"
187
+ });
188
+ if (typeof validated !== "string") {
189
+ throw new TypeError(`[upstash-lua] Arg "${argName}" schema must output a string, got ${typeof validated}`);
190
+ }
191
+ argsArray.push(validated);
192
+ }
193
+ return { keys: keysArray, args: argsArray };
194
+ }
195
+ function resolveLua(luaInput, keyNames, argNames) {
196
+ if (typeof luaInput === "string") {
197
+ return luaInput;
198
+ }
199
+ const KEYS = createTokenProxy("key");
200
+ const ARGV = createTokenProxy("arg");
201
+ const compiled = luaInput({ KEYS, ARGV });
202
+ if (!isCompiledLua(compiled)) {
203
+ throw new TypeError(`[upstash-lua] lua function must return a lua\`...\` template. ` + `Got ${typeof compiled}. Did you forget to use the lua tagged template?`);
204
+ }
205
+ return compileLua(compiled, keyNames, argNames);
206
+ }
207
+ function defineScript(def) {
208
+ const { name, lua: luaInput, keys: keySchemas = {}, args: argSchemas = {}, returns } = def;
209
+ const keyNames = Object.keys(keySchemas);
210
+ const argNames = Object.keys(argSchemas);
211
+ const lua2 = resolveLua(luaInput, keyNames, argNames);
212
+ let cachedSha;
213
+ async function getSha() {
214
+ if (cachedSha === undefined) {
215
+ cachedSha = await sha1Hex(lua2);
216
+ }
217
+ return cachedSha;
218
+ }
219
+ async function execute(redis, input) {
220
+ const { keys, args } = await validateAndCollect(name, keySchemas, argSchemas, keyNames, argNames, input);
221
+ const sha = await getSha();
222
+ return evalWithCache(redis, {
223
+ script: lua2,
224
+ sha,
225
+ keys,
226
+ args
227
+ });
228
+ }
229
+ return {
230
+ name,
231
+ lua: lua2,
232
+ keyNames,
233
+ argNames,
234
+ async runRaw(redis, ...inputArgs) {
235
+ const input = inputArgs[0] ?? {};
236
+ return execute(redis, input);
237
+ },
238
+ async run(redis, ...inputArgs) {
239
+ const input = inputArgs[0] ?? {};
240
+ const raw = await execute(redis, input);
241
+ if (!returns) {
242
+ return raw;
243
+ }
244
+ return parseStandard(returns, raw, {
245
+ scriptName: name,
246
+ path: "return",
247
+ type: "return",
248
+ raw
249
+ });
250
+ }
251
+ };
252
+ }
253
+ // src/hash-result.ts
254
+ function pairsToObject(arr) {
255
+ if (!Array.isArray(arr)) {
256
+ throw new Error(`Expected array of key-value pairs, got ${typeof arr}`);
257
+ }
258
+ if (arr.length % 2 !== 0) {
259
+ throw new Error(`Expected even number of elements (key-value pairs), got ${arr.length}`);
260
+ }
261
+ const result = {};
262
+ for (let i = 0;i < arr.length; i += 2) {
263
+ const key = arr[i];
264
+ const value = arr[i + 1];
265
+ if (typeof key !== "string") {
266
+ throw new Error(`Expected string key at index ${i}, got ${typeof key}`);
267
+ }
268
+ result[key] = value;
269
+ }
270
+ return result;
271
+ }
272
+ function hashResult(schema) {
273
+ return {
274
+ "~standard": {
275
+ version: 1,
276
+ vendor: "upstash-lua",
277
+ validate(value) {
278
+ let obj;
279
+ try {
280
+ obj = pairsToObject(value);
281
+ } catch (error) {
282
+ return {
283
+ issues: [{
284
+ message: error instanceof Error ? error.message : String(error)
285
+ }]
286
+ };
287
+ }
288
+ const result = schema["~standard"].validate(obj);
289
+ if (result instanceof Promise) {
290
+ return result;
291
+ }
292
+ return result;
293
+ }
294
+ }
295
+ };
296
+ }
297
+ export {
298
+ validateStandard,
299
+ parseStandard,
300
+ lua,
301
+ hashResult,
302
+ defineScript,
303
+ VERSION
304
+ };
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Type-safe Lua tagged template for defining Redis scripts.
3
+ *
4
+ * This module provides the `lua` tagged template function that enables
5
+ * type-safe `${KEYS.name}` and `${ARGV.name}` interpolations, which are
6
+ * compiled to `KEYS[n]` and `ARGV[n]` at definition time.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { lua, defineScript } from "upstash-lua"
11
+ * import { z } from "zod"
12
+ *
13
+ * const myScript = defineScript({
14
+ * name: "myScript",
15
+ * keys: { userKey: z.string() },
16
+ * args: { limit: z.number().transform(String) },
17
+ * lua: ({ KEYS, ARGV }) => lua`
18
+ * local k = ${KEYS.userKey}
19
+ * local limit = tonumber(${ARGV.limit})
20
+ * return { k, limit }
21
+ * `,
22
+ * returns: z.tuple([z.string(), z.number()]),
23
+ * })
24
+ * ```
25
+ *
26
+ * @module lua-template
27
+ * @since 0.2.0
28
+ */
29
+ /**
30
+ * Unique symbol used to brand Lua tokens and compiled templates.
31
+ * This ensures type safety and prevents accidental misuse.
32
+ *
33
+ * @since 0.2.0
34
+ */
35
+ export declare const LUA_TOKEN: unique symbol;
36
+ /**
37
+ * A token representing a KEYS or ARGV reference in a Lua template.
38
+ *
39
+ * These tokens are created by accessing properties on the typed
40
+ * `KEYS` and `ARGV` proxy objects passed to the lua function.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * // KEYS.userKey produces:
45
+ * { [LUA_TOKEN]: true, kind: "key", name: "userKey" }
46
+ *
47
+ * // ARGV.limit produces:
48
+ * { [LUA_TOKEN]: true, kind: "arg", name: "limit" }
49
+ * ```
50
+ *
51
+ * @since 0.2.0
52
+ */
53
+ export interface LuaToken {
54
+ readonly [LUA_TOKEN]: true;
55
+ readonly kind: "key" | "arg";
56
+ readonly name: string;
57
+ }
58
+ /**
59
+ * The result of the `lua` tagged template function.
60
+ *
61
+ * Contains the template strings and interpolated tokens, which are
62
+ * later compiled into a final Lua string with positional references.
63
+ *
64
+ * @since 0.2.0
65
+ */
66
+ export interface CompiledLua {
67
+ readonly [LUA_TOKEN]: "compiled";
68
+ readonly strings: TemplateStringsArray;
69
+ readonly tokens: readonly LuaToken[];
70
+ }
71
+ /**
72
+ * A typed proxy object that produces `LuaToken` values for property access.
73
+ *
74
+ * Used to type `KEYS` and `ARGV` objects passed to the lua function,
75
+ * providing autocomplete and compile-time errors for invalid references.
76
+ *
77
+ * @typeParam T - The schema record type (keys or args)
78
+ *
79
+ * @since 0.2.0
80
+ */
81
+ export type TokenProxy<T> = {
82
+ readonly [P in keyof T]: LuaToken;
83
+ };
84
+ /**
85
+ * Type guard to check if a value is a `LuaToken`.
86
+ *
87
+ * @param value - The value to check
88
+ * @returns `true` if the value is a `LuaToken`
89
+ *
90
+ * @since 0.2.0
91
+ */
92
+ export declare function isLuaToken(value: unknown): value is LuaToken;
93
+ /**
94
+ * Type guard to check if a value is a `CompiledLua` template.
95
+ *
96
+ * @param value - The value to check
97
+ * @returns `true` if the value is a `CompiledLua`
98
+ *
99
+ * @since 0.2.0
100
+ */
101
+ export declare function isCompiledLua(value: unknown): value is CompiledLua;
102
+ /**
103
+ * Creates a typed proxy that produces `LuaToken` values for any property access.
104
+ *
105
+ * @param kind - Whether this proxy is for "key" or "arg" tokens
106
+ * @returns A proxy object that returns tokens for any property access
107
+ *
108
+ * @internal
109
+ * @since 0.2.0
110
+ */
111
+ export declare function createTokenProxy<T extends Record<string, unknown>>(kind: "key" | "arg"): TokenProxy<T>;
112
+ /**
113
+ * Tagged template function for writing type-safe Lua scripts.
114
+ *
115
+ * Use this with the `KEYS` and `ARGV` proxies provided by `defineScript`
116
+ * to create Lua scripts with type-safe key and argument references.
117
+ *
118
+ * @param strings - Template literal strings
119
+ * @param tokens - Interpolated `LuaToken` values from `KEYS.*` or `ARGV.*`
120
+ * @returns A `CompiledLua` object ready for compilation
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * lua: ({ KEYS, ARGV }) => lua`
125
+ * local key = ${KEYS.userKey}
126
+ * local limit = tonumber(${ARGV.limit})
127
+ * return redis.call("GET", key)
128
+ * `
129
+ * ```
130
+ *
131
+ * @since 0.2.0
132
+ */
133
+ export declare function lua(strings: TemplateStringsArray, ...tokens: LuaToken[]): CompiledLua;
134
+ /**
135
+ * Compiles a `CompiledLua` template into a final Lua string.
136
+ *
137
+ * Replaces `KEYS.*` tokens with `KEYS[n]` and `ARGV.*` tokens with `ARGV[n]`
138
+ * based on the order of keys and args in the schema.
139
+ *
140
+ * @param compiled - The compiled lua template from the `lua` tagged template
141
+ * @param keyNames - Ordered list of key names from the schema
142
+ * @param argNames - Ordered list of arg names from the schema
143
+ * @returns The final Lua script string with positional references
144
+ *
145
+ * @throws {Error} If a token references a name not in the schema
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * // Given keyNames = ["userKey"] and argNames = ["limit", "window"]
150
+ * // ${KEYS.userKey} becomes KEYS[1]
151
+ * // ${ARGV.limit} becomes ARGV[1]
152
+ * // ${ARGV.window} becomes ARGV[2]
153
+ * ```
154
+ *
155
+ * @since 0.2.0
156
+ */
157
+ export declare function compileLua(compiled: CompiledLua, keyNames: readonly string[], argNames: readonly string[]): string;
158
+ //# sourceMappingURL=lua-template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lua-template.d.ts","sourceRoot":"","sources":["../src/lua-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,SAAS,EAAE,OAAO,MAA4B,CAAA;AAE3D;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,CAAC,SAAS,CAAC,EAAE,IAAI,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,KAAK,CAAA;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,CAAC,SAAS,CAAC,EAAE,UAAU,CAAA;IAChC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAA;IACtC,QAAQ,CAAC,MAAM,EAAE,SAAS,QAAQ,EAAE,CAAA;CACrC;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI;IAAE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ;CAAE,CAAA;AAEjE;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAO5D;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAOlE;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChE,IAAI,EAAE,KAAK,GAAG,KAAK,GAClB,UAAU,CAAC,CAAC,CAAC,CAaf;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,GAAG,WAAW,CAiBrF;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,UAAU,CACxB,QAAQ,EAAE,WAAW,EACrB,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,MAAM,CA4CR"}