@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.
- package/LICENSE +105 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +19 -0
- package/dist/opaque-redis-scripts.d.ts +7 -0
- package/dist/opaque-redis-scripts.js +330 -0
- package/dist/opaque-store.d.ts +22 -0
- package/dist/opaque-store.js +295 -0
- package/dist/redis-connection.d.ts +16 -0
- package/dist/redis-connection.js +136 -0
- package/dist/redis-options.d.ts +15 -0
- package/dist/redis-options.js +2 -0
- package/dist/redis-values.d.ts +3 -0
- package/dist/redis-values.js +26 -0
- package/dist/relocation-store.d.ts +14 -0
- package/dist/relocation-store.js +103 -0
- package/package.json +18 -0
- package/src/index.ts +3 -0
- package/src/opaque-redis-scripts.ts +337 -0
- package/src/opaque-store.ts +387 -0
- package/src/redis-connection.ts +174 -0
- package/src/redis-options.ts +17 -0
- package/src/redis-values.ts +23 -0
- package/src/relocation-store.ts +150 -0
- package/tsconfig.json +17 -0
|
@@ -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
|
+
}
|