@zlink-systems/framework-locations-redis 0.10.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,150 @@
1
+ import type {
2
+ ZLinkBlobPutResult,
3
+ ZLinkBlobReadResult,
4
+ ZLinkBlobReference,
5
+ ZLinkBlobRenewResult,
6
+ ZLinkRelocationStore
7
+ } from '@zlink-systems/framework';
8
+ import type { ZLinkRedisRelocationOptions } from './redis-options';
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';
15
+ import { asArray, asString, toNumber } from './redis-values';
16
+
17
+ const MAX_ENCODED_BLOB_BYTES = 64 * 1024 * 1024 + 23;
18
+
19
+ /** Redis implementation of immutable relocation blob storage. */
20
+ export class ZLinkRedisRelocationStore implements ZLinkRelocationStore {
21
+ private readonly connection: RedisConnection;
22
+ private readonly domain: string;
23
+
24
+ constructor(options: ZLinkRedisRelocationOptions) {
25
+ this.connection = new RedisConnection(options);
26
+ // {prefix}:{zlink-relocation-v1}:blob:{reference}
27
+ // (23-relocation-store-redis.md#8). Braced for Redis Cluster hash-tag
28
+ // co-location, matching the dotnet/java reference.
29
+ this.domain = `${options.keyPrefix}:{zlink-relocation-v1}`;
30
+ }
31
+
32
+ async put(
33
+ reference: ZLinkBlobReference,
34
+ payload: Uint8Array,
35
+ retentionMs: number,
36
+ signal?: AbortSignal
37
+ ): Promise<ZLinkBlobPutResult> {
38
+ const referenceValue = requireReference(reference);
39
+ requirePayload(payload);
40
+ 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
+ ));
47
+ const kind = asString(result[0]);
48
+ const storeNow = fromUnixMs(toNumber(result[1]));
49
+ if (kind === 'conflict') return { kind, storeNow };
50
+ return {
51
+ kind: kind === 'alreadyStored' ? 'alreadyStored' : 'stored',
52
+ storeNow,
53
+ expiresAt: fromUnixMs(toNumber(result[2]))
54
+ };
55
+ }
56
+
57
+ async read(
58
+ reference: ZLinkBlobReference,
59
+ signal?: AbortSignal
60
+ ): Promise<ZLinkBlobReadResult> {
61
+ const referenceValue = requireReference(reference);
62
+ const result = asArray(await this.connection.eval(
63
+ BLOB_READ_SCRIPT,
64
+ [this.blobKey(referenceValue)],
65
+ [],
66
+ signal
67
+ ));
68
+ const storeNow = fromUnixMs(toNumber(result[1]));
69
+ if (toNumber(result[0]) !== 1) return { kind: 'missing', storeNow };
70
+ return {
71
+ kind: 'found',
72
+ bytes: Uint8Array.from(asBuffer(result[2])),
73
+ expiresAt: fromUnixMs(toNumber(result[3])),
74
+ storeNow
75
+ };
76
+ }
77
+
78
+ async renew(
79
+ reference: ZLinkBlobReference,
80
+ retentionMs: number,
81
+ signal?: AbortSignal
82
+ ): Promise<ZLinkBlobRenewResult> {
83
+ const referenceValue = requireReference(reference);
84
+ 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
+ ));
91
+ const storeNow = fromUnixMs(toNumber(result[1]));
92
+ if (toNumber(result[0]) !== 1) return { kind: 'missing', storeNow };
93
+ return {
94
+ kind: 'renewed',
95
+ expiresAt: fromUnixMs(toNumber(result[2])),
96
+ storeNow
97
+ };
98
+ }
99
+
100
+ async delete(
101
+ reference: ZLinkBlobReference,
102
+ signal?: AbortSignal
103
+ ): Promise<void> {
104
+ const referenceValue = requireReference(reference);
105
+ await this.connection.command(['DEL', this.blobKey(referenceValue)], signal);
106
+ }
107
+
108
+ async dispose(): Promise<void> {
109
+ await this.connection.dispose();
110
+ }
111
+
112
+ private blobKey(reference: string): string {
113
+ return `${this.domain}:blob:${reference}`;
114
+ }
115
+ }
116
+
117
+ function requireReference(reference: ZLinkBlobReference): string {
118
+ const value = reference.value;
119
+ const bytes = Buffer.byteLength(value, 'utf8');
120
+ if (bytes < 1 || bytes > 4_096) {
121
+ throw new RangeError('Relocation Store reference must contain 1..4,096 UTF-8 bytes.');
122
+ }
123
+ return value;
124
+ }
125
+
126
+ function requirePayload(payload: Uint8Array): void {
127
+ if (payload.byteLength > MAX_ENCODED_BLOB_BYTES) {
128
+ throw new RangeError('Relocation Store encoded blob exceeds 64 MiB + 23 bytes.');
129
+ }
130
+ }
131
+
132
+ function requireRetention(retentionMs: number): number {
133
+ if (!Number.isSafeInteger(retentionMs) || retentionMs < 1) {
134
+ throw new RangeError('Relocation Store retention must be a positive safe integer.');
135
+ }
136
+ return retentionMs;
137
+ }
138
+
139
+ function asBuffer(value: unknown): Buffer {
140
+ if (Buffer.isBuffer(value)) return Buffer.from(value);
141
+ if (value instanceof Uint8Array) return Buffer.from(value);
142
+ return Buffer.from(asString(value), 'utf8');
143
+ }
144
+
145
+ function fromUnixMs(value: number): Date {
146
+ if (!Number.isSafeInteger(value) || value < 0) {
147
+ throw new Error('Redis Store returned an invalid provider timestamp.');
148
+ }
149
+ return new Date(value);
150
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "tsBuildInfoFile": "dist/.tsbuildinfo"
8
+ },
9
+ "include": [
10
+ "src/**/*.ts"
11
+ ],
12
+ "references": [
13
+ {
14
+ "path": "../framework"
15
+ }
16
+ ]
17
+ }