@zlink-systems/framework-locations-redis 0.26.0 → 0.27.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/framework-internal.d.ts +1 -0
- package/dist/framework-internal.js +12 -0
- package/dist/opaque-redis-scripts.d.ts +17 -2
- package/dist/opaque-redis-scripts.js +126 -114
- package/dist/opaque-store.js +43 -32
- package/dist/redis-connection.d.ts +1 -1
- package/dist/relocation-store.js +8 -7
- package/package.json +3 -3
- package/src/framework-internal.ts +20 -0
- package/src/opaque-redis-scripts.ts +135 -116
- package/src/opaque-store.ts +77 -37
- package/src/redis-connection.ts +1 -1
- package/src/relocation-store.ts +17 -7
package/src/opaque-store.ts
CHANGED
|
@@ -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 ===
|
|
93
|
-
if (outcome ===
|
|
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 !==
|
|
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 ===
|
|
143
|
-
if (outcome ===
|
|
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 !==
|
|
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 >
|
|
239
|
-
throw new RangeError(
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
281
|
+
return [REDIS_STORE_TOKEN.Put, keyIndex.get(key), key, retentionMs ?? false];
|
|
262
282
|
});
|
|
263
|
-
if (encodedBytes >
|
|
264
|
-
throw new RangeError(
|
|
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') >
|
|
271
|
-
throw new RangeError(
|
|
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 (
|
|
274
|
-
|
|
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 >
|
|
283
|
-
throw new RangeError(
|
|
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 >
|
|
298
|
-
throw new RangeError(
|
|
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 >
|
|
307
|
-
throw new RangeError(
|
|
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 >
|
|
314
|
-
throw new RangeError(
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
380
|
+
return undefined;
|
|
341
381
|
}
|
|
342
382
|
return { snapshotId, lastKey: Buffer.from(lastKeyHex, 'hex').toString('utf8') };
|
|
343
383
|
}
|
package/src/redis-connection.ts
CHANGED
package/src/relocation-store.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
51
|
+
const outcome = asString(result[0]);
|
|
46
52
|
const storeNow = fromUnixMs(toNumber(result[1]));
|
|
47
|
-
if (
|
|
53
|
+
if (outcome === REDIS_STORE_TOKEN.Conflict) return { kind: 'conflict', storeNow };
|
|
48
54
|
return {
|
|
49
|
-
kind:
|
|
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 >
|
|
112
|
-
throw new RangeError(
|
|
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(
|
|
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
|
|