@zlink-systems/framework-locations-redis 0.21.0 → 0.22.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.
@@ -39,7 +39,9 @@ local function encodeMember(originalKey, bytes, version, expiresAtMs, tombstone)
39
39
  })
40
40
  end
41
41
  `;
42
- exports.OPAQUE_READ_SCRIPT = PROLOGUE + DECODE_HELPERS + `
42
+ exports.OPAQUE_READ_SCRIPT = PROLOGUE +
43
+ DECODE_HELPERS +
44
+ `
43
45
  local record = liveRecordAt(KEYS[1], nowMs)
44
46
  if not record then return {0, nowMs} end
45
47
  return {1, nowMs, record[1], record[2], record[3], tostring(tonumber(record[4]))}
@@ -54,7 +56,9 @@ return {1, nowMs, record[1], record[2], record[3], tostring(tonumber(record[4]))
54
56
  // ['put', keyIndex, originalKey, retentionMsOrFalse]
55
57
  // | ['delete', keyIndex, originalKey]
56
58
  // ARGV[3..] = one raw-bytes argument per 'put' mutation, in mutation order.
57
- exports.OPAQUE_WRITE_SCRIPT = PROLOGUE + DECODE_HELPERS + `
59
+ exports.OPAQUE_WRITE_SCRIPT = PROLOGUE +
60
+ DECODE_HELPERS +
61
+ `
58
62
  local indexKey = KEYS[1]
59
63
  local mapKey = KEYS[2]
60
64
  local cleanupKey = KEYS[3]
@@ -163,7 +167,8 @@ return result
163
167
  // Shared body for point-in-time paged scanning. KEYS[1]=indexKey,
164
168
  // KEYS[2]=mapKey, KEYS[3]=snapshotKey, KEYS[4]=cleanupKey,
165
169
  // KEYS[5]=sequenceKey, KEYS[6]=snapshotExpiryKey, KEYS[7]=snapshotBoundaryKey.
166
- const SCAN_CLEANUP_AND_BOUNDARY = DECODE_HELPERS + `
170
+ const SCAN_CLEANUP_AND_BOUNDARY = DECODE_HELPERS +
171
+ `
167
172
  local expiredSnapshots = redis.call('ZRANGEBYSCORE', KEYS[6], '-inf', nowMs, 'LIMIT', 0, 128)
168
173
  for _, expiredId in ipairs(expiredSnapshots) do
169
174
  redis.call('ZREM', KEYS[6], expiredId)
@@ -267,7 +272,9 @@ end
267
272
  return result
268
273
  `;
269
274
  // ARGV = [prefix, limit, snapshotId]
270
- exports.OPAQUE_SCAN_START_SCRIPT = PROLOGUE + SCAN_CLEANUP_AND_BOUNDARY + `
275
+ exports.OPAQUE_SCAN_START_SCRIPT = PROLOGUE +
276
+ SCAN_CLEANUP_AND_BOUNDARY +
277
+ `
271
278
  local prefix = ARGV[1]
272
279
  local limit = tonumber(ARGV[2])
273
280
  local snapshotId = ARGV[3]
@@ -282,9 +289,12 @@ redis.call('HSET', KEYS[3], 'now', tostring(nowMs), 'boundary', tostring(boundar
282
289
  redis.call('PEXPIRE', KEYS[3], 60000)
283
290
  redis.call('ZADD', KEYS[6], nowMs + 60000, snapshotId)
284
291
  redis.call('ZADD', KEYS[7], boundary, snapshotId)
285
- ` + SCAN_PAGE_READ;
292
+ ` +
293
+ SCAN_PAGE_READ;
286
294
  // ARGV = [prefix, lastKeyHex, limit, snapshotId]
287
- exports.OPAQUE_SCAN_CONTINUE_SCRIPT = PROLOGUE + SCAN_CLEANUP_AND_BOUNDARY + `
295
+ exports.OPAQUE_SCAN_CONTINUE_SCRIPT = PROLOGUE +
296
+ SCAN_CLEANUP_AND_BOUNDARY +
297
+ `
288
298
  local prefix = ARGV[1]
289
299
  local lastKey = ARGV[2]
290
300
  local limit = tonumber(ARGV[3])
@@ -295,13 +305,15 @@ if redis.call('EXISTS', KEYS[3]) == 0 then
295
305
  redis.call('ZREM', KEYS[7], snapshotId)
296
306
  return {'expired'}
297
307
  end
298
- ` + SCAN_PAGE_READ;
308
+ ` +
309
+ SCAN_PAGE_READ;
299
310
  // Relocation Store: raw-bytes STRING payloads at
300
311
  // {prefix}:zlink-relocation-v1:blob:{reference}, retention via PSETEX/PX
301
312
  // (23-relocation-store-redis.md#8). KEYS[1] is the blob key -- the reference
302
313
  // itself is already the key's last segment, so identity on retry is decided
303
314
  // by comparing the stored bytes against ARGV[1].
304
- exports.BLOB_PUT_SCRIPT = PROLOGUE + `
315
+ exports.BLOB_PUT_SCRIPT = PROLOGUE +
316
+ `
305
317
  local existing = redis.call('GET', KEYS[1])
306
318
  if existing then
307
319
  if existing ~= ARGV[1] then
@@ -315,14 +327,16 @@ local retentionMs = tonumber(ARGV[2])
315
327
  redis.call('SET', KEYS[1], ARGV[1], 'PX', retentionMs)
316
328
  return {'stored', nowMs, tostring(nowMs + retentionMs)}
317
329
  `;
318
- exports.BLOB_READ_SCRIPT = PROLOGUE + `
330
+ exports.BLOB_READ_SCRIPT = PROLOGUE +
331
+ `
319
332
  local bytes = redis.call('GET', KEYS[1])
320
333
  if not bytes then return {0, nowMs} end
321
334
  local ttl = redis.call('PTTL', KEYS[1])
322
335
  local expiresAtMs = nowMs + math.max(ttl, 0)
323
336
  return {1, nowMs, bytes, tostring(expiresAtMs)}
324
337
  `;
325
- exports.BLOB_RENEW_SCRIPT = PROLOGUE + `
338
+ exports.BLOB_RENEW_SCRIPT = PROLOGUE +
339
+ `
326
340
  if redis.call('EXISTS', KEYS[1]) == 0 then return {0, nowMs} end
327
341
  local retentionMs = tonumber(ARGV[1])
328
342
  redis.call('PEXPIRE', KEYS[1], retentionMs)
@@ -50,7 +50,7 @@ class ZLinkRedisLocationStore {
50
50
  this.sequenceKey(),
51
51
  this.snapshotExpiryKey(),
52
52
  this.snapshotBoundaryKey(),
53
- ...encoded.keys.map(key => this.rowKey(key))
53
+ ...encoded.keys.map((key) => this.rowKey(key))
54
54
  ], [
55
55
  JSON.stringify(encoded.conditions),
56
56
  JSON.stringify(encoded.mutations),
@@ -161,10 +161,10 @@ class ZLinkRedisLocationStore {
161
161
  }
162
162
  exports.ZLinkRedisLocationStore = ZLinkRedisLocationStore;
163
163
  function encodeWrite(request) {
164
- const conditionKeys = request.conditions.map(condition => requireKey(condition.key));
165
- const mutationKeys = request.mutations.map(mutation => requireKey(mutation.key));
166
- if (new Set(conditionKeys).size !== conditionKeys.length
167
- || new Set(mutationKeys).size !== mutationKeys.length) {
164
+ const conditionKeys = request.conditions.map((condition) => requireKey(condition.key));
165
+ const mutationKeys = request.mutations.map((mutation) => requireKey(mutation.key));
166
+ if (new Set(conditionKeys).size !== conditionKeys.length ||
167
+ new Set(mutationKeys).size !== mutationKeys.length) {
168
168
  throw new RangeError('Location Store condition and mutation keys must be unique.');
169
169
  }
170
170
  const keys = [...new Set([...conditionKeys, ...mutationKeys])];
@@ -175,7 +175,7 @@ function encodeWrite(request) {
175
175
  // adds 6 to this 1-based index before indexing into KEYS.
176
176
  const keyIndex = new Map(keys.map((key, index) => [key, index + 1]));
177
177
  let encodedBytes = 0;
178
- const conditions = request.conditions.map(condition => {
178
+ const conditions = request.conditions.map((condition) => {
179
179
  const key = requireKey(condition.key);
180
180
  encodedBytes += Buffer.byteLength(key, 'utf8');
181
181
  if (condition.kind === 'missing')
@@ -185,7 +185,7 @@ function encodeWrite(request) {
185
185
  return ['version', keyIndex.get(key), key, expected];
186
186
  });
187
187
  const putBytes = [];
188
- const mutations = request.mutations.map(mutation => {
188
+ const mutations = request.mutations.map((mutation) => {
189
189
  const key = requireKey(mutation.key);
190
190
  encodedBytes += Buffer.byteLength(key, 'utf8');
191
191
  if (mutation.kind === 'delete')
@@ -193,12 +193,7 @@ function encodeWrite(request) {
193
193
  requireValue(mutation.bytes, mutation.retentionMs);
194
194
  encodedBytes += mutation.bytes.byteLength;
195
195
  putBytes.push(Buffer.from(mutation.bytes));
196
- return [
197
- 'put',
198
- keyIndex.get(key),
199
- key,
200
- mutation.retentionMs ?? false
201
- ];
196
+ return ['put', keyIndex.get(key), key, mutation.retentionMs ?? false];
202
197
  });
203
198
  if (encodedBytes > MAX_WRITE_BYTES) {
204
199
  throw new RangeError('Location Store write exceeds 4 MiB encoded input.');
@@ -248,8 +243,7 @@ function requireValue(bytes, retentionMs) {
248
243
  if (bytes.byteLength > MAX_VALUE_BYTES) {
249
244
  throw new RangeError('Location Store value exceeds 1 MiB.');
250
245
  }
251
- if (retentionMs !== undefined
252
- && (!Number.isSafeInteger(retentionMs) || retentionMs < 1)) {
246
+ if (retentionMs !== undefined && (!Number.isSafeInteger(retentionMs) || retentionMs < 1)) {
253
247
  throw new RangeError('Location Store retention must be a positive safe integer.');
254
248
  }
255
249
  }
@@ -258,9 +252,9 @@ function parseCursor(cursor) {
258
252
  const separator = value.lastIndexOf(':');
259
253
  const snapshotId = separator < 0 ? '' : value.slice(0, separator);
260
254
  const lastKeyHex = separator < 0 ? '' : value.slice(separator + 1);
261
- if (!/^[0-9a-f-]{36}$/.test(snapshotId)
262
- || !/^[0-9a-f]*$/.test(lastKeyHex)
263
- || lastKeyHex.length % 2 !== 0) {
255
+ if (!/^[0-9a-f-]{36}$/.test(snapshotId) ||
256
+ !/^[0-9a-f]*$/.test(lastKeyHex) ||
257
+ lastKeyHex.length % 2 !== 0) {
264
258
  throw new RangeError('Location Store scan cursor is invalid.');
265
259
  }
266
260
  return { snapshotId, lastKey: Buffer.from(lastKeyHex, 'hex').toString('utf8') };
@@ -97,14 +97,13 @@ function requireOptions(options) {
97
97
  if (options.keyPrefix.includes('{') || options.keyPrefix.includes('}')) {
98
98
  throw new Error('Redis Store keyPrefix must not contain hash-tag braces.');
99
99
  }
100
- if (options.client === undefined
101
- && options.url === undefined
102
- && options.clientOptions === undefined) {
100
+ if (options.client === undefined &&
101
+ options.url === undefined &&
102
+ options.clientOptions === undefined) {
103
103
  throw new Error('Redis Store requires url, clientOptions, or client.');
104
104
  }
105
- if (options.operationTimeoutMs !== undefined
106
- && (!Number.isSafeInteger(options.operationTimeoutMs)
107
- || options.operationTimeoutMs < 1)) {
105
+ if (options.operationTimeoutMs !== undefined &&
106
+ (!Number.isSafeInteger(options.operationTimeoutMs) || options.operationTimeoutMs < 1)) {
108
107
  throw new RangeError('Redis Store operationTimeoutMs must be a positive safe integer.');
109
108
  }
110
109
  }
@@ -131,6 +130,6 @@ async function waitForOperation(operation, signal, timeoutMs) {
131
130
  if (timeoutMs !== undefined) {
132
131
  timeout = setTimeout(() => settle(() => reject(new Error(`Redis Store operation timed out after ${timeoutMs} ms.`))), timeoutMs);
133
132
  }
134
- operation.then(value => settle(() => resolve(value)), error => settle(() => reject(error)));
133
+ operation.then((value) => settle(() => resolve(value)), (error) => settle(() => reject(error)));
135
134
  });
136
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zlink-systems/framework-locations-redis",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  }
18
18
  },
19
19
  "dependencies": {
20
- "@zlink-systems/framework": "0.21.0",
20
+ "@zlink-systems/framework": "0.22.0",
21
21
  "@zlink-systems/zlink": "1.2.1",
22
22
  "redis": "^6.1.0"
23
23
  }
@@ -38,7 +38,10 @@ local function encodeMember(originalKey, bytes, version, expiresAtMs, tombstone)
38
38
  end
39
39
  `;
40
40
 
41
- export const OPAQUE_READ_SCRIPT = PROLOGUE + DECODE_HELPERS + `
41
+ export const OPAQUE_READ_SCRIPT =
42
+ PROLOGUE +
43
+ DECODE_HELPERS +
44
+ `
42
45
  local record = liveRecordAt(KEYS[1], nowMs)
43
46
  if not record then return {0, nowMs} end
44
47
  return {1, nowMs, record[1], record[2], record[3], tostring(tonumber(record[4]))}
@@ -54,7 +57,10 @@ return {1, nowMs, record[1], record[2], record[3], tostring(tonumber(record[4]))
54
57
  // ['put', keyIndex, originalKey, retentionMsOrFalse]
55
58
  // | ['delete', keyIndex, originalKey]
56
59
  // ARGV[3..] = one raw-bytes argument per 'put' mutation, in mutation order.
57
- export const OPAQUE_WRITE_SCRIPT = PROLOGUE + DECODE_HELPERS + `
60
+ export const OPAQUE_WRITE_SCRIPT =
61
+ PROLOGUE +
62
+ DECODE_HELPERS +
63
+ `
58
64
  local indexKey = KEYS[1]
59
65
  local mapKey = KEYS[2]
60
66
  local cleanupKey = KEYS[3]
@@ -164,7 +170,9 @@ return result
164
170
  // Shared body for point-in-time paged scanning. KEYS[1]=indexKey,
165
171
  // KEYS[2]=mapKey, KEYS[3]=snapshotKey, KEYS[4]=cleanupKey,
166
172
  // KEYS[5]=sequenceKey, KEYS[6]=snapshotExpiryKey, KEYS[7]=snapshotBoundaryKey.
167
- const SCAN_CLEANUP_AND_BOUNDARY = DECODE_HELPERS + `
173
+ const SCAN_CLEANUP_AND_BOUNDARY =
174
+ DECODE_HELPERS +
175
+ `
168
176
  local expiredSnapshots = redis.call('ZRANGEBYSCORE', KEYS[6], '-inf', nowMs, 'LIMIT', 0, 128)
169
177
  for _, expiredId in ipairs(expiredSnapshots) do
170
178
  redis.call('ZREM', KEYS[6], expiredId)
@@ -270,7 +278,10 @@ return result
270
278
  `;
271
279
 
272
280
  // ARGV = [prefix, limit, snapshotId]
273
- export const OPAQUE_SCAN_START_SCRIPT = PROLOGUE + SCAN_CLEANUP_AND_BOUNDARY + `
281
+ export const OPAQUE_SCAN_START_SCRIPT =
282
+ PROLOGUE +
283
+ SCAN_CLEANUP_AND_BOUNDARY +
284
+ `
274
285
  local prefix = ARGV[1]
275
286
  local limit = tonumber(ARGV[2])
276
287
  local snapshotId = ARGV[3]
@@ -285,10 +296,14 @@ redis.call('HSET', KEYS[3], 'now', tostring(nowMs), 'boundary', tostring(boundar
285
296
  redis.call('PEXPIRE', KEYS[3], 60000)
286
297
  redis.call('ZADD', KEYS[6], nowMs + 60000, snapshotId)
287
298
  redis.call('ZADD', KEYS[7], boundary, snapshotId)
288
- ` + SCAN_PAGE_READ;
299
+ ` +
300
+ SCAN_PAGE_READ;
289
301
 
290
302
  // ARGV = [prefix, lastKeyHex, limit, snapshotId]
291
- export const OPAQUE_SCAN_CONTINUE_SCRIPT = PROLOGUE + SCAN_CLEANUP_AND_BOUNDARY + `
303
+ export const OPAQUE_SCAN_CONTINUE_SCRIPT =
304
+ PROLOGUE +
305
+ SCAN_CLEANUP_AND_BOUNDARY +
306
+ `
292
307
  local prefix = ARGV[1]
293
308
  local lastKey = ARGV[2]
294
309
  local limit = tonumber(ARGV[3])
@@ -299,14 +314,17 @@ if redis.call('EXISTS', KEYS[3]) == 0 then
299
314
  redis.call('ZREM', KEYS[7], snapshotId)
300
315
  return {'expired'}
301
316
  end
302
- ` + SCAN_PAGE_READ;
317
+ ` +
318
+ SCAN_PAGE_READ;
303
319
 
304
320
  // Relocation Store: raw-bytes STRING payloads at
305
321
  // {prefix}:zlink-relocation-v1:blob:{reference}, retention via PSETEX/PX
306
322
  // (23-relocation-store-redis.md#8). KEYS[1] is the blob key -- the reference
307
323
  // itself is already the key's last segment, so identity on retry is decided
308
324
  // by comparing the stored bytes against ARGV[1].
309
- export const BLOB_PUT_SCRIPT = PROLOGUE + `
325
+ export const BLOB_PUT_SCRIPT =
326
+ PROLOGUE +
327
+ `
310
328
  local existing = redis.call('GET', KEYS[1])
311
329
  if existing then
312
330
  if existing ~= ARGV[1] then
@@ -321,7 +339,9 @@ redis.call('SET', KEYS[1], ARGV[1], 'PX', retentionMs)
321
339
  return {'stored', nowMs, tostring(nowMs + retentionMs)}
322
340
  `;
323
341
 
324
- export const BLOB_READ_SCRIPT = PROLOGUE + `
342
+ export const BLOB_READ_SCRIPT =
343
+ PROLOGUE +
344
+ `
325
345
  local bytes = redis.call('GET', KEYS[1])
326
346
  if not bytes then return {0, nowMs} end
327
347
  local ttl = redis.call('PTTL', KEYS[1])
@@ -329,7 +349,9 @@ local expiresAtMs = nowMs + math.max(ttl, 0)
329
349
  return {1, nowMs, bytes, tostring(expiresAtMs)}
330
350
  `;
331
351
 
332
- export const BLOB_RENEW_SCRIPT = PROLOGUE + `
352
+ export const BLOB_RENEW_SCRIPT =
353
+ PROLOGUE +
354
+ `
333
355
  if redis.call('EXISTS', KEYS[1]) == 0 then return {0, nowMs} end
334
356
  local retentionMs = tonumber(ARGV[1])
335
357
  redis.call('PEXPIRE', KEYS[1], retentionMs)
@@ -43,17 +43,11 @@ export class ZLinkRedisLocationStore implements ZLinkLocationStore {
43
43
  this.domain = `${options.keyPrefix}:${NAMESPACE}`;
44
44
  }
45
45
 
46
- async read(
47
- key: ZLinkStoreKey,
48
- signal?: AbortSignal
49
- ): Promise<ZLinkStoreReadResult> {
46
+ async read(key: ZLinkStoreKey, signal?: AbortSignal): Promise<ZLinkStoreReadResult> {
50
47
  const logicalKey = requireKey(key);
51
- const result = asArray(await this.connection.eval(
52
- OPAQUE_READ_SCRIPT,
53
- [this.rowKey(logicalKey)],
54
- [],
55
- signal
56
- ));
48
+ const result = asArray(
49
+ await this.connection.eval(OPAQUE_READ_SCRIPT, [this.rowKey(logicalKey)], [], signal)
50
+ );
57
51
  const storeNow = fromUnixMs(toNumber(result[1]));
58
52
  if (toNumber(result[0]) !== 1) return { kind: 'missing', storeNow };
59
53
  requireMatchingKey(asString(result[2]), logicalKey);
@@ -73,24 +67,26 @@ export class ZLinkRedisLocationStore implements ZLinkLocationStore {
73
67
  signal?: AbortSignal
74
68
  ): Promise<ZLinkStoreWriteResult> {
75
69
  const encoded = encodeWrite(request);
76
- const result = asArray(await this.connection.eval(
77
- OPAQUE_WRITE_SCRIPT,
78
- [
79
- this.indexKey(),
80
- this.mapKey(),
81
- this.cleanupKey(),
82
- this.sequenceKey(),
83
- this.snapshotExpiryKey(),
84
- this.snapshotBoundaryKey(),
85
- ...encoded.keys.map(key => this.rowKey(key))
86
- ],
87
- [
88
- JSON.stringify(encoded.conditions),
89
- JSON.stringify(encoded.mutations),
90
- ...encoded.putBytes
91
- ],
92
- signal
93
- ));
70
+ const result = asArray(
71
+ await this.connection.eval(
72
+ OPAQUE_WRITE_SCRIPT,
73
+ [
74
+ this.indexKey(),
75
+ this.mapKey(),
76
+ this.cleanupKey(),
77
+ this.sequenceKey(),
78
+ this.snapshotExpiryKey(),
79
+ this.snapshotBoundaryKey(),
80
+ ...encoded.keys.map((key) => this.rowKey(key))
81
+ ],
82
+ [
83
+ JSON.stringify(encoded.conditions),
84
+ JSON.stringify(encoded.mutations),
85
+ ...encoded.putBytes
86
+ ],
87
+ signal
88
+ )
89
+ );
94
90
  const storeNow = fromUnixMs(toNumber(result[1]));
95
91
  const outcome = asString(result[0]);
96
92
  if (outcome === 'conflict') return { kind: 'conflict', storeNow };
@@ -110,10 +106,7 @@ export class ZLinkRedisLocationStore implements ZLinkLocationStore {
110
106
  return { kind: 'applied', putVersions, storeNow };
111
107
  }
112
108
 
113
- async scan(
114
- request: ZLinkStoreScanRequest,
115
- signal?: AbortSignal
116
- ): Promise<ZLinkStoreScanResult> {
109
+ async scan(request: ZLinkStoreScanRequest, signal?: AbortSignal): Promise<ZLinkStoreScanResult> {
117
110
  requireScanRequest(request);
118
111
  if (request.cursor === undefined) {
119
112
  const snapshotId = randomUUID();
@@ -143,10 +136,7 @@ export class ZLinkRedisLocationStore implements ZLinkLocationStore {
143
136
  await this.connection.dispose();
144
137
  }
145
138
 
146
- private async readScanPage(
147
- snapshotId: string,
148
- raw: unknown
149
- ): Promise<ZLinkStoreScanResult> {
139
+ private async readScanPage(snapshotId: string, raw: unknown): Promise<ZLinkStoreScanResult> {
150
140
  const result = asArray(raw);
151
141
  const outcome = asString(result[0]);
152
142
  if (outcome === 'expired') return { kind: 'expired' };
@@ -174,9 +164,10 @@ export class ZLinkRedisLocationStore implements ZLinkLocationStore {
174
164
  kind: 'page',
175
165
  value: {
176
166
  items,
177
- nextCursor: nextKey.length === 0
178
- ? undefined
179
- : scanCursor(`${snapshotId}:${Buffer.from(nextKey, 'utf8').toString('hex')}`),
167
+ nextCursor:
168
+ nextKey.length === 0
169
+ ? undefined
170
+ : scanCursor(`${snapshotId}:${Buffer.from(nextKey, 'utf8').toString('hex')}`),
180
171
  storeNow
181
172
  }
182
173
  };
@@ -235,11 +226,11 @@ interface EncodedWrite {
235
226
  }
236
227
 
237
228
  function encodeWrite(request: ZLinkStoreWriteRequest): EncodedWrite {
238
- const conditionKeys = request.conditions.map(condition => requireKey(condition.key));
239
- const mutationKeys = request.mutations.map(mutation => requireKey(mutation.key));
229
+ const conditionKeys = request.conditions.map((condition) => requireKey(condition.key));
230
+ const mutationKeys = request.mutations.map((mutation) => requireKey(mutation.key));
240
231
  if (
241
- new Set(conditionKeys).size !== conditionKeys.length
242
- || new Set(mutationKeys).size !== mutationKeys.length
232
+ new Set(conditionKeys).size !== conditionKeys.length ||
233
+ new Set(mutationKeys).size !== mutationKeys.length
243
234
  ) {
244
235
  throw new RangeError('Location Store condition and mutation keys must be unique.');
245
236
  }
@@ -251,7 +242,7 @@ function encodeWrite(request: ZLinkStoreWriteRequest): EncodedWrite {
251
242
  // adds 6 to this 1-based index before indexing into KEYS.
252
243
  const keyIndex = new Map(keys.map((key, index) => [key, index + 1]));
253
244
  let encodedBytes = 0;
254
- const conditions = request.conditions.map(condition => {
245
+ const conditions = request.conditions.map((condition) => {
255
246
  const key = requireKey(condition.key);
256
247
  encodedBytes += Buffer.byteLength(key, 'utf8');
257
248
  if (condition.kind === 'missing') return ['missing', keyIndex.get(key), key];
@@ -260,19 +251,14 @@ function encodeWrite(request: ZLinkStoreWriteRequest): EncodedWrite {
260
251
  return ['version', keyIndex.get(key), key, expected];
261
252
  });
262
253
  const putBytes: Buffer[] = [];
263
- const mutations = request.mutations.map(mutation => {
254
+ const mutations = request.mutations.map((mutation) => {
264
255
  const key = requireKey(mutation.key);
265
256
  encodedBytes += Buffer.byteLength(key, 'utf8');
266
257
  if (mutation.kind === 'delete') return ['delete', keyIndex.get(key), key];
267
258
  requireValue(mutation.bytes, mutation.retentionMs);
268
259
  encodedBytes += mutation.bytes.byteLength;
269
260
  putBytes.push(Buffer.from(mutation.bytes));
270
- return [
271
- 'put',
272
- keyIndex.get(key),
273
- key,
274
- mutation.retentionMs ?? false
275
- ];
261
+ return ['put', keyIndex.get(key), key, mutation.retentionMs ?? false];
276
262
  });
277
263
  if (encodedBytes > MAX_WRITE_BYTES) {
278
264
  throw new RangeError('Location Store write exceeds 4 MiB encoded input.');
@@ -327,10 +313,7 @@ function requireValue(bytes: Uint8Array, retentionMs: number | undefined): void
327
313
  if (bytes.byteLength > MAX_VALUE_BYTES) {
328
314
  throw new RangeError('Location Store value exceeds 1 MiB.');
329
315
  }
330
- if (
331
- retentionMs !== undefined
332
- && (!Number.isSafeInteger(retentionMs) || retentionMs < 1)
333
- ) {
316
+ if (retentionMs !== undefined && (!Number.isSafeInteger(retentionMs) || retentionMs < 1)) {
334
317
  throw new RangeError('Location Store retention must be a positive safe integer.');
335
318
  }
336
319
  }
@@ -344,9 +327,9 @@ function parseCursor(cursor: ZLinkStoreScanCursor): {
344
327
  const snapshotId = separator < 0 ? '' : value.slice(0, separator);
345
328
  const lastKeyHex = separator < 0 ? '' : value.slice(separator + 1);
346
329
  if (
347
- !/^[0-9a-f-]{36}$/.test(snapshotId)
348
- || !/^[0-9a-f]*$/.test(lastKeyHex)
349
- || lastKeyHex.length % 2 !== 0
330
+ !/^[0-9a-f-]{36}$/.test(snapshotId) ||
331
+ !/^[0-9a-f]*$/.test(lastKeyHex) ||
332
+ lastKeyHex.length % 2 !== 0
350
333
  ) {
351
334
  throw new RangeError('Location Store scan cursor is invalid.');
352
335
  }
@@ -1,9 +1,6 @@
1
1
  import { createClient, RESP_TYPES } from 'redis';
2
2
  import type { RedisClientType } from 'redis';
3
- import type {
4
- ZLinkRedisLocationOptions,
5
- ZLinkRedisRelocationOptions
6
- } from './redis-options';
3
+ import type { ZLinkRedisLocationOptions, ZLinkRedisRelocationOptions } from './redis-options';
7
4
 
8
5
  export type RedisCommandValue = string | Buffer;
9
6
  export type RedisCommandClient = Pick<
@@ -56,10 +53,7 @@ export class RedisConnection {
56
53
  args: readonly RedisCommandValue[],
57
54
  signal?: AbortSignal
58
55
  ): Promise<unknown> {
59
- return await this.command(
60
- ['EVAL', script, String(keys.length), ...keys, ...args],
61
- signal
62
- );
56
+ return await this.command(['EVAL', script, String(keys.length), ...keys, ...args], signal);
63
57
  }
64
58
 
65
59
  async dispose(): Promise<void> {
@@ -110,9 +104,7 @@ function isClientReady(client: RedisCommandClient): boolean {
110
104
  return client.isReady === true;
111
105
  }
112
106
 
113
- function requireOptions(
114
- options: ZLinkRedisLocationOptions | ZLinkRedisRelocationOptions
115
- ): void {
107
+ function requireOptions(options: ZLinkRedisLocationOptions | ZLinkRedisRelocationOptions): void {
116
108
  if (options.keyPrefix.length === 0) {
117
109
  throw new Error('Redis Store keyPrefix is required.');
118
110
  }
@@ -120,16 +112,15 @@ function requireOptions(
120
112
  throw new Error('Redis Store keyPrefix must not contain hash-tag braces.');
121
113
  }
122
114
  if (
123
- options.client === undefined
124
- && options.url === undefined
125
- && options.clientOptions === undefined
115
+ options.client === undefined &&
116
+ options.url === undefined &&
117
+ options.clientOptions === undefined
126
118
  ) {
127
119
  throw new Error('Redis Store requires url, clientOptions, or client.');
128
120
  }
129
121
  if (
130
- options.operationTimeoutMs !== undefined
131
- && (!Number.isSafeInteger(options.operationTimeoutMs)
132
- || options.operationTimeoutMs < 1)
122
+ options.operationTimeoutMs !== undefined &&
123
+ (!Number.isSafeInteger(options.operationTimeoutMs) || options.operationTimeoutMs < 1)
133
124
  ) {
134
125
  throw new RangeError('Redis Store operationTimeoutMs must be a positive safe integer.');
135
126
  }
@@ -154,21 +145,19 @@ async function waitForOperation<T>(
154
145
  cleanup();
155
146
  action();
156
147
  };
157
- const onAbort = () => settle(
158
- () => reject(signal?.reason ?? new Error('Redis Store operation aborted.'))
159
- );
148
+ const onAbort = () =>
149
+ settle(() => reject(signal?.reason ?? new Error('Redis Store operation aborted.')));
160
150
  signal?.addEventListener('abort', onAbort, { once: true });
161
151
  if (timeoutMs !== undefined) {
162
152
  timeout = setTimeout(
163
- () => settle(() =>
164
- reject(new Error(`Redis Store operation timed out after ${timeoutMs} ms.`))
165
- ),
153
+ () =>
154
+ settle(() => reject(new Error(`Redis Store operation timed out after ${timeoutMs} ms.`))),
166
155
  timeoutMs
167
156
  );
168
157
  }
169
158
  operation.then(
170
- value => settle(() => resolve(value)),
171
- error => settle(() => reject(error))
159
+ (value) => settle(() => resolve(value)),
160
+ (error) => settle(() => reject(error))
172
161
  );
173
162
  });
174
163
  }
@@ -7,11 +7,7 @@ import type {
7
7
  } from '@zlink-systems/framework';
8
8
  import type { ZLinkRedisRelocationOptions } from './redis-options';
9
9
  import { RedisConnection } from './redis-connection';
10
- import {
11
- BLOB_PUT_SCRIPT,
12
- BLOB_READ_SCRIPT,
13
- BLOB_RENEW_SCRIPT
14
- } from './opaque-redis-scripts';
10
+ import { BLOB_PUT_SCRIPT, BLOB_READ_SCRIPT, BLOB_RENEW_SCRIPT } from './opaque-redis-scripts';
15
11
  import { asArray, asString, toNumber } from './redis-values';
16
12
 
17
13
  const MAX_ENCODED_BLOB_BYTES = 64 * 1024 * 1024 + 23;
@@ -38,12 +34,14 @@ export class ZLinkRedisRelocationStore implements ZLinkRelocationStore {
38
34
  const referenceValue = requireReference(reference);
39
35
  requirePayload(payload);
40
36
  const retention = requireRetention(retentionMs);
41
- const result = asArray(await this.connection.eval(
42
- BLOB_PUT_SCRIPT,
43
- [this.blobKey(referenceValue)],
44
- [Buffer.from(payload), String(retention)],
45
- signal
46
- ));
37
+ const result = asArray(
38
+ await this.connection.eval(
39
+ BLOB_PUT_SCRIPT,
40
+ [this.blobKey(referenceValue)],
41
+ [Buffer.from(payload), String(retention)],
42
+ signal
43
+ )
44
+ );
47
45
  const kind = asString(result[0]);
48
46
  const storeNow = fromUnixMs(toNumber(result[1]));
49
47
  if (kind === 'conflict') return { kind, storeNow };
@@ -54,17 +52,11 @@ export class ZLinkRedisRelocationStore implements ZLinkRelocationStore {
54
52
  };
55
53
  }
56
54
 
57
- async read(
58
- reference: ZLinkBlobReference,
59
- signal?: AbortSignal
60
- ): Promise<ZLinkBlobReadResult> {
55
+ async read(reference: ZLinkBlobReference, signal?: AbortSignal): Promise<ZLinkBlobReadResult> {
61
56
  const referenceValue = requireReference(reference);
62
- const result = asArray(await this.connection.eval(
63
- BLOB_READ_SCRIPT,
64
- [this.blobKey(referenceValue)],
65
- [],
66
- signal
67
- ));
57
+ const result = asArray(
58
+ await this.connection.eval(BLOB_READ_SCRIPT, [this.blobKey(referenceValue)], [], signal)
59
+ );
68
60
  const storeNow = fromUnixMs(toNumber(result[1]));
69
61
  if (toNumber(result[0]) !== 1) return { kind: 'missing', storeNow };
70
62
  return {
@@ -82,12 +74,14 @@ export class ZLinkRedisRelocationStore implements ZLinkRelocationStore {
82
74
  ): Promise<ZLinkBlobRenewResult> {
83
75
  const referenceValue = requireReference(reference);
84
76
  const retention = requireRetention(retentionMs);
85
- const result = asArray(await this.connection.eval(
86
- BLOB_RENEW_SCRIPT,
87
- [this.blobKey(referenceValue)],
88
- [String(retention)],
89
- signal
90
- ));
77
+ const result = asArray(
78
+ await this.connection.eval(
79
+ BLOB_RENEW_SCRIPT,
80
+ [this.blobKey(referenceValue)],
81
+ [String(retention)],
82
+ signal
83
+ )
84
+ );
91
85
  const storeNow = fromUnixMs(toNumber(result[1]));
92
86
  if (toNumber(result[0]) !== 1) return { kind: 'missing', storeNow };
93
87
  return {
@@ -97,10 +91,7 @@ export class ZLinkRedisRelocationStore implements ZLinkRelocationStore {
97
91
  };
98
92
  }
99
93
 
100
- async delete(
101
- reference: ZLinkBlobReference,
102
- signal?: AbortSignal
103
- ): Promise<void> {
94
+ async delete(reference: ZLinkBlobReference, signal?: AbortSignal): Promise<void> {
104
95
  const referenceValue = requireReference(reference);
105
96
  await this.connection.command(['DEL', this.blobKey(referenceValue)], signal);
106
97
  }