@zlink-systems/framework-locations-redis 0.26.0 → 0.28.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.
@@ -1,3 +1,12 @@
1
+ import {
2
+ ZLINK_PROVIDER_MAX_KEY_BYTES,
3
+ ZLINK_PROVIDER_MAX_VALUE_BYTES,
4
+ ZLINK_PROVIDER_MAX_VERSION_BYTES,
5
+ ZLINK_PROVIDER_MAX_WRITE_KEYS,
6
+ ZLINK_PROVIDER_MAX_WRITE_BYTES,
7
+ ZLINK_PROVIDER_MAX_SCAN_CURSOR_BYTES,
8
+ ZLINK_PROVIDER_MAX_PAGE_SIZE
9
+ } from './framework-internal';
1
10
  import { createHash, randomUUID } from 'node:crypto';
2
11
  import type {
3
12
  ZLinkLocationStore,
@@ -13,6 +22,7 @@ import type {
13
22
  import type { ZLinkRedisLocationOptions } from './redis-options';
14
23
  import { RedisConnection } from './redis-connection';
15
24
  import {
25
+ REDIS_STORE_TOKEN,
16
26
  OPAQUE_READ_SCRIPT,
17
27
  OPAQUE_SCAN_CONTINUE_SCRIPT,
18
28
  OPAQUE_SCAN_START_SCRIPT,
@@ -20,10 +30,6 @@ import {
20
30
  } from './opaque-redis-scripts';
21
31
  import { asArray, asString, toNumber } from './redis-values';
22
32
 
23
- const MAX_VALUE_BYTES = 1024 * 1024;
24
- const MAX_WRITE_KEYS = 2_048;
25
- const MAX_WRITE_BYTES = 4 * 1024 * 1024;
26
-
27
33
  // {prefix}:{zlink-location-v3}:opaque:{sha256hex(preimage)} is the public
28
34
  // contract (21-location-runtime.md#2.4, 22-location-store-redis.md#7). The
29
35
  // braces are a Redis Cluster hash tag: every key this provider's scripts
@@ -82,6 +88,7 @@ export class ZLinkRedisLocationStore implements ZLinkLocationStore {
82
88
  [
83
89
  JSON.stringify(encoded.conditions),
84
90
  JSON.stringify(encoded.mutations),
91
+ ...encoded.expectedBytes,
85
92
  ...encoded.putBytes
86
93
  ],
87
94
  signal
@@ -89,11 +96,11 @@ export class ZLinkRedisLocationStore implements ZLinkLocationStore {
89
96
  );
90
97
  const storeNow = fromUnixMs(toNumber(result[1]));
91
98
  const outcome = asString(result[0]);
92
- if (outcome === 'conflict') return { kind: 'conflict', storeNow };
93
- if (outcome === 'backlog') {
99
+ if (outcome === REDIS_STORE_TOKEN.Conflict) return { kind: 'conflict', storeNow };
100
+ if (outcome === REDIS_STORE_TOKEN.Backlog) {
94
101
  throw new Error('Redis Location Store version backlog is full.');
95
102
  }
96
- if (outcome !== 'applied') {
103
+ if (outcome !== REDIS_STORE_TOKEN.Applied) {
97
104
  throw new Error('Redis Location Store returned an unrecognized write outcome.');
98
105
  }
99
106
  const putVersions = [];
@@ -121,6 +128,9 @@ export class ZLinkRedisLocationStore implements ZLinkLocationStore {
121
128
  );
122
129
  }
123
130
  const cursor = parseCursor(request.cursor);
131
+ if (cursor === undefined) {
132
+ return { kind: 'expired' };
133
+ }
124
134
  return await this.readScanPage(
125
135
  cursor.snapshotId,
126
136
  await this.connection.eval(
@@ -139,11 +149,11 @@ export class ZLinkRedisLocationStore implements ZLinkLocationStore {
139
149
  private async readScanPage(snapshotId: string, raw: unknown): Promise<ZLinkStoreScanResult> {
140
150
  const result = asArray(raw);
141
151
  const outcome = asString(result[0]);
142
- if (outcome === 'expired') return { kind: 'expired' };
143
- if (outcome === 'capacity') {
152
+ if (outcome === REDIS_STORE_TOKEN.Expired) return { kind: 'expired' };
153
+ if (outcome === REDIS_STORE_TOKEN.Capacity) {
144
154
  throw new Error('Redis Location Store snapshot capacity is full.');
145
155
  }
146
- if (outcome !== 'page') {
156
+ if (outcome !== REDIS_STORE_TOKEN.Page) {
147
157
  throw new Error('Redis Location Store returned an unrecognized scan outcome.');
148
158
  }
149
159
  const storeNow = fromUnixMs(toNumber(result[1]));
@@ -222,6 +232,7 @@ interface EncodedWrite {
222
232
  readonly keys: readonly string[];
223
233
  readonly conditions: readonly unknown[];
224
234
  readonly mutations: readonly unknown[];
235
+ readonly expectedBytes: readonly Buffer[];
225
236
  readonly putBytes: readonly Buffer[];
226
237
  }
227
238
 
@@ -235,43 +246,62 @@ function encodeWrite(request: ZLinkStoreWriteRequest): EncodedWrite {
235
246
  throw new RangeError('Location Store condition and mutation keys must be unique.');
236
247
  }
237
248
  const keys = [...new Set([...conditionKeys, ...mutationKeys])];
238
- if (keys.length > MAX_WRITE_KEYS) {
239
- throw new RangeError('Location Store write exceeds 2,048 unique keys.');
249
+ if (keys.length > ZLINK_PROVIDER_MAX_WRITE_KEYS) {
250
+ throw new RangeError(
251
+ `Location Store write exceeds ${ZLINK_PROVIDER_MAX_WRITE_KEYS.toLocaleString('en-US')} unique keys.`
252
+ );
240
253
  }
241
254
  // Row keys are appended after the six fixed auxiliary keys; the script
242
255
  // adds 6 to this 1-based index before indexing into KEYS.
243
256
  const keyIndex = new Map(keys.map((key, index) => [key, index + 1]));
244
257
  let encodedBytes = 0;
258
+ const expectedBytes: Buffer[] = [];
245
259
  const conditions = request.conditions.map((condition) => {
246
260
  const key = requireKey(condition.key);
247
261
  encodedBytes += Buffer.byteLength(key, 'utf8');
248
- if (condition.kind === 'missing') return ['missing', keyIndex.get(key), key];
262
+ if (condition.kind === 'missing') return [REDIS_STORE_TOKEN.Missing, keyIndex.get(key), key];
263
+ if (condition.kind === 'value') {
264
+ requireValue(condition.expected, undefined);
265
+ encodedBytes += condition.expected.byteLength;
266
+ expectedBytes.push(Buffer.from(condition.expected));
267
+ return [REDIS_STORE_TOKEN.Value, keyIndex.get(key), key, expectedBytes.length + 2];
268
+ }
249
269
  const expected = requireVersion(condition.expected);
250
270
  encodedBytes += Buffer.byteLength(expected, 'utf8');
251
- return ['version', keyIndex.get(key), key, expected];
271
+ return [REDIS_STORE_TOKEN.Version, keyIndex.get(key), key, expected];
252
272
  });
253
273
  const putBytes: Buffer[] = [];
254
274
  const mutations = request.mutations.map((mutation) => {
255
275
  const key = requireKey(mutation.key);
256
276
  encodedBytes += Buffer.byteLength(key, 'utf8');
257
- if (mutation.kind === 'delete') return ['delete', keyIndex.get(key), key];
277
+ if (mutation.kind === 'delete') return [REDIS_STORE_TOKEN.Delete, keyIndex.get(key), key];
258
278
  const retentionMs = requireValue(mutation.bytes, mutation.retentionMs);
259
279
  encodedBytes += mutation.bytes.byteLength;
260
280
  putBytes.push(Buffer.from(mutation.bytes));
261
- return ['put', keyIndex.get(key), key, retentionMs ?? false];
281
+ return [REDIS_STORE_TOKEN.Put, keyIndex.get(key), key, retentionMs ?? false];
262
282
  });
263
- if (encodedBytes > MAX_WRITE_BYTES) {
264
- throw new RangeError('Location Store write exceeds 4 MiB encoded input.');
283
+ if (encodedBytes > ZLINK_PROVIDER_MAX_WRITE_BYTES) {
284
+ throw new RangeError(
285
+ `Location Store write exceeds ${ZLINK_PROVIDER_MAX_WRITE_BYTES / (1024 * 1024)} MiB encoded input.`
286
+ );
265
287
  }
266
- return { keys, conditions, mutations, putBytes };
288
+ return { keys, conditions, mutations, expectedBytes, putBytes };
267
289
  }
268
290
 
269
291
  function requireScanRequest(request: ZLinkStoreScanRequest): void {
270
- if (Buffer.byteLength(request.prefix, 'utf8') > 1_024) {
271
- throw new RangeError('Location Store scan prefix exceeds 1,024 UTF-8 bytes.');
292
+ if (Buffer.byteLength(request.prefix, 'utf8') > ZLINK_PROVIDER_MAX_KEY_BYTES) {
293
+ throw new RangeError(
294
+ `Location Store scan prefix exceeds ${ZLINK_PROVIDER_MAX_KEY_BYTES.toLocaleString('en-US')} UTF-8 bytes.`
295
+ );
272
296
  }
273
- if (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 1_000) {
274
- throw new RangeError('Location Store scan limit must be in 1..1000.');
297
+ if (
298
+ !Number.isSafeInteger(request.limit) ||
299
+ request.limit < 1 ||
300
+ request.limit > ZLINK_PROVIDER_MAX_PAGE_SIZE
301
+ ) {
302
+ throw new RangeError(
303
+ `Location Store scan limit must be in 1..${ZLINK_PROVIDER_MAX_PAGE_SIZE}.`
304
+ );
275
305
  }
276
306
  if (request.cursor !== undefined) requireCursor(request.cursor);
277
307
  }
@@ -279,8 +309,10 @@ function requireScanRequest(request: ZLinkStoreScanRequest): void {
279
309
  function requireKey(key: ZLinkStoreKey): string {
280
310
  const value = key.value;
281
311
  const bytes = Buffer.byteLength(value, 'utf8');
282
- if (bytes < 1 || bytes > 1_024) {
283
- throw new RangeError('Location Store key must contain 1..1,024 UTF-8 bytes.');
312
+ if (bytes < 1 || bytes > ZLINK_PROVIDER_MAX_KEY_BYTES) {
313
+ throw new RangeError(
314
+ `Location Store key must contain 1..${ZLINK_PROVIDER_MAX_KEY_BYTES.toLocaleString('en-US')} UTF-8 bytes.`
315
+ );
284
316
  }
285
317
  return value;
286
318
  }
@@ -294,8 +326,10 @@ function requireMatchingKey(actual: string, expected: string): void {
294
326
  function requireVersion(version: ZLinkStoreVersion): string {
295
327
  const value = version.value;
296
328
  const bytes = Buffer.byteLength(value, 'utf8');
297
- if (bytes < 1 || bytes > 4_096) {
298
- throw new RangeError('Location Store version must contain 1..4,096 UTF-8 bytes.');
329
+ if (bytes < 1 || bytes > ZLINK_PROVIDER_MAX_VERSION_BYTES) {
330
+ throw new RangeError(
331
+ `Location Store version must contain 1..${ZLINK_PROVIDER_MAX_VERSION_BYTES.toLocaleString('en-US')} UTF-8 bytes.`
332
+ );
299
333
  }
300
334
  return value;
301
335
  }
@@ -303,15 +337,19 @@ function requireVersion(version: ZLinkStoreVersion): string {
303
337
  function requireCursor(cursor: ZLinkStoreScanCursor): string {
304
338
  const value = cursor.value;
305
339
  const bytes = Buffer.byteLength(value, 'utf8');
306
- if (bytes < 1 || bytes > 4_096) {
307
- throw new RangeError('Location Store cursor must contain 1..4,096 UTF-8 bytes.');
340
+ if (bytes < 1 || bytes > ZLINK_PROVIDER_MAX_SCAN_CURSOR_BYTES) {
341
+ throw new RangeError(
342
+ `Location Store cursor must contain 1..${ZLINK_PROVIDER_MAX_SCAN_CURSOR_BYTES.toLocaleString('en-US')} UTF-8 bytes.`
343
+ );
308
344
  }
309
345
  return value;
310
346
  }
311
347
 
312
348
  function requireValue(bytes: Uint8Array, retentionMs: number | undefined): number | undefined {
313
- if (bytes.byteLength > MAX_VALUE_BYTES) {
314
- throw new RangeError('Location Store value exceeds 1 MiB.');
349
+ if (bytes.byteLength > ZLINK_PROVIDER_MAX_VALUE_BYTES) {
350
+ throw new RangeError(
351
+ `Location Store value exceeds ${ZLINK_PROVIDER_MAX_VALUE_BYTES / (1024 * 1024)} MiB.`
352
+ );
315
353
  }
316
354
  if (
317
355
  retentionMs !== undefined &&
@@ -324,11 +362,13 @@ function requireValue(bytes: Uint8Array, retentionMs: number | undefined): numbe
324
362
  return retentionMs === undefined ? undefined : Math.ceil(retentionMs);
325
363
  }
326
364
 
327
- function parseCursor(cursor: ZLinkStoreScanCursor): {
328
- readonly snapshotId: string;
329
- readonly lastKey: string;
330
- } {
331
- const value = requireCursor(cursor);
365
+ function parseCursor(cursor: ZLinkStoreScanCursor):
366
+ | {
367
+ readonly snapshotId: string;
368
+ readonly lastKey: string;
369
+ }
370
+ | undefined {
371
+ const value = cursor.value;
332
372
  const separator = value.lastIndexOf(':');
333
373
  const snapshotId = separator < 0 ? '' : value.slice(0, separator);
334
374
  const lastKeyHex = separator < 0 ? '' : value.slice(separator + 1);
@@ -337,7 +377,7 @@ function parseCursor(cursor: ZLinkStoreScanCursor): {
337
377
  !/^[0-9a-f]*$/.test(lastKeyHex) ||
338
378
  lastKeyHex.length % 2 !== 0
339
379
  ) {
340
- throw new RangeError('Location Store scan cursor is invalid.');
380
+ return undefined;
341
381
  }
342
382
  return { snapshotId, lastKey: Buffer.from(lastKeyHex, 'hex').toString('utf8') };
343
383
  }
@@ -48,7 +48,7 @@ export class RedisConnection {
48
48
  }
49
49
 
50
50
  async eval(
51
- script: string,
51
+ script: RedisCommandValue,
52
52
  keys: readonly string[],
53
53
  args: readonly RedisCommandValue[],
54
54
  signal?: AbortSignal
@@ -1,3 +1,4 @@
1
+ import { ZLINK_RELOCATION_MAX_BLOB_REFERENCE_BYTES } from './framework-internal';
1
2
  import type {
2
3
  ZLinkBlobPutResult,
3
4
  ZLinkBlobReadResult,
@@ -7,7 +8,12 @@ import type {
7
8
  } from '@zlink-systems/framework';
8
9
  import type { ZLinkRedisRelocationOptions } from './redis-options';
9
10
  import { RedisConnection } from './redis-connection';
10
- import { BLOB_PUT_SCRIPT, BLOB_READ_SCRIPT, BLOB_RENEW_SCRIPT } from './opaque-redis-scripts';
11
+ import {
12
+ REDIS_STORE_TOKEN,
13
+ BLOB_PUT_SCRIPT,
14
+ BLOB_READ_SCRIPT,
15
+ BLOB_RENEW_SCRIPT
16
+ } from './opaque-redis-scripts';
11
17
  import { asArray, asString, toNumber } from './redis-values';
12
18
 
13
19
  const MAX_ENCODED_BLOB_BYTES = 64 * 1024 * 1024 + 23;
@@ -42,11 +48,11 @@ export class ZLinkRedisRelocationStore implements ZLinkRelocationStore {
42
48
  signal
43
49
  )
44
50
  );
45
- const kind = asString(result[0]);
51
+ const outcome = asString(result[0]);
46
52
  const storeNow = fromUnixMs(toNumber(result[1]));
47
- if (kind === 'conflict') return { kind, storeNow };
53
+ if (outcome === REDIS_STORE_TOKEN.Conflict) return { kind: 'conflict', storeNow };
48
54
  return {
49
- kind: kind === 'alreadyStored' ? 'alreadyStored' : 'stored',
55
+ kind: outcome === REDIS_STORE_TOKEN.AlreadyStored ? 'alreadyStored' : 'stored',
50
56
  storeNow,
51
57
  expiresAt: fromUnixMs(toNumber(result[2]))
52
58
  };
@@ -108,15 +114,19 @@ export class ZLinkRedisRelocationStore implements ZLinkRelocationStore {
108
114
  function requireReference(reference: ZLinkBlobReference): string {
109
115
  const value = reference.value;
110
116
  const bytes = Buffer.byteLength(value, 'utf8');
111
- if (bytes < 1 || bytes > 4_096) {
112
- throw new RangeError('Relocation Store reference must contain 1..4,096 UTF-8 bytes.');
117
+ if (bytes < 1 || bytes > ZLINK_RELOCATION_MAX_BLOB_REFERENCE_BYTES) {
118
+ throw new RangeError(
119
+ `Relocation Store reference must contain 1..${ZLINK_RELOCATION_MAX_BLOB_REFERENCE_BYTES.toLocaleString('en-US')} UTF-8 bytes.`
120
+ );
113
121
  }
114
122
  return value;
115
123
  }
116
124
 
117
125
  function requirePayload(payload: Uint8Array): void {
118
126
  if (payload.byteLength > MAX_ENCODED_BLOB_BYTES) {
119
- throw new RangeError('Relocation Store encoded blob exceeds 64 MiB + 23 bytes.');
127
+ throw new RangeError(
128
+ `Relocation Store encoded blob exceeds ${Math.floor(MAX_ENCODED_BLOB_BYTES / (1024 * 1024))} MiB + ${MAX_ENCODED_BLOB_BYTES % (1024 * 1024)} bytes.`
129
+ );
120
130
  }
121
131
  }
122
132