@bcts/hubert 1.0.0-alpha.17
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 +48 -0
- package/README.md +18 -0
- package/dist/arid-derivation-1CJuU-kZ.cjs +150 -0
- package/dist/arid-derivation-1CJuU-kZ.cjs.map +1 -0
- package/dist/arid-derivation-CbqACjdg.mjs +126 -0
- package/dist/arid-derivation-CbqACjdg.mjs.map +1 -0
- package/dist/bin/hubert.cjs +384 -0
- package/dist/bin/hubert.cjs.map +1 -0
- package/dist/bin/hubert.d.cts +1 -0
- package/dist/bin/hubert.d.mts +1 -0
- package/dist/bin/hubert.mjs +383 -0
- package/dist/bin/hubert.mjs.map +1 -0
- package/dist/chunk-CbDLau6x.cjs +34 -0
- package/dist/hybrid/index.cjs +14 -0
- package/dist/hybrid/index.d.cts +3 -0
- package/dist/hybrid/index.d.mts +3 -0
- package/dist/hybrid/index.mjs +6 -0
- package/dist/hybrid-BZhumygj.mjs +356 -0
- package/dist/hybrid-BZhumygj.mjs.map +1 -0
- package/dist/hybrid-dX5JLumO.cjs +410 -0
- package/dist/hybrid-dX5JLumO.cjs.map +1 -0
- package/dist/index-BEzpUC7r.d.mts +380 -0
- package/dist/index-BEzpUC7r.d.mts.map +1 -0
- package/dist/index-C2F6ugLL.d.mts +210 -0
- package/dist/index-C2F6ugLL.d.mts.map +1 -0
- package/dist/index-CUnDouMb.d.mts +215 -0
- package/dist/index-CUnDouMb.d.mts.map +1 -0
- package/dist/index-CV6lZJqY.d.cts +380 -0
- package/dist/index-CV6lZJqY.d.cts.map +1 -0
- package/dist/index-CY3TCzIm.d.cts +217 -0
- package/dist/index-CY3TCzIm.d.cts.map +1 -0
- package/dist/index-DEr4SR1J.d.cts +215 -0
- package/dist/index-DEr4SR1J.d.cts.map +1 -0
- package/dist/index-T1LHanIb.d.mts +217 -0
- package/dist/index-T1LHanIb.d.mts.map +1 -0
- package/dist/index-jyzuOhFB.d.cts +210 -0
- package/dist/index-jyzuOhFB.d.cts.map +1 -0
- package/dist/index.cjs +60 -0
- package/dist/index.d.cts +161 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +161 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +10 -0
- package/dist/ipfs/index.cjs +13 -0
- package/dist/ipfs/index.d.cts +3 -0
- package/dist/ipfs/index.d.mts +3 -0
- package/dist/ipfs/index.mjs +5 -0
- package/dist/ipfs-BRMMCBjv.mjs +1 -0
- package/dist/ipfs-CetOVQcO.cjs +0 -0
- package/dist/kv-BAmhmMOo.cjs +425 -0
- package/dist/kv-BAmhmMOo.cjs.map +1 -0
- package/dist/kv-C-emxv0w.mjs +375 -0
- package/dist/kv-C-emxv0w.mjs.map +1 -0
- package/dist/kv-DJiKvypY.mjs +403 -0
- package/dist/kv-DJiKvypY.mjs.map +1 -0
- package/dist/kv-store-DmngWWuw.d.mts +183 -0
- package/dist/kv-store-DmngWWuw.d.mts.map +1 -0
- package/dist/kv-store-ww-AUyLd.d.cts +183 -0
- package/dist/kv-store-ww-AUyLd.d.cts.map +1 -0
- package/dist/kv-yjvQa_LH.cjs +457 -0
- package/dist/kv-yjvQa_LH.cjs.map +1 -0
- package/dist/logging-hmzNzifq.mjs +158 -0
- package/dist/logging-hmzNzifq.mjs.map +1 -0
- package/dist/logging-qc9uMgil.cjs +212 -0
- package/dist/logging-qc9uMgil.cjs.map +1 -0
- package/dist/mainline/index.cjs +12 -0
- package/dist/mainline/index.d.cts +3 -0
- package/dist/mainline/index.d.mts +3 -0
- package/dist/mainline/index.mjs +5 -0
- package/dist/mainline-D_jfeFMh.cjs +0 -0
- package/dist/mainline-cFIuXbo-.mjs +1 -0
- package/dist/server/index.cjs +14 -0
- package/dist/server/index.d.cts +3 -0
- package/dist/server/index.d.mts +3 -0
- package/dist/server/index.mjs +3 -0
- package/dist/server-BBNRZ30D.cjs +912 -0
- package/dist/server-BBNRZ30D.cjs.map +1 -0
- package/dist/server-DVyk9gqU.mjs +836 -0
- package/dist/server-DVyk9gqU.mjs.map +1 -0
- package/package.json +125 -0
- package/src/arid-derivation.ts +155 -0
- package/src/bin/hubert.ts +667 -0
- package/src/error.ts +89 -0
- package/src/hybrid/error.ts +77 -0
- package/src/hybrid/index.ts +24 -0
- package/src/hybrid/kv.ts +236 -0
- package/src/hybrid/reference.ts +176 -0
- package/src/index.ts +145 -0
- package/src/ipfs/error.ts +83 -0
- package/src/ipfs/index.ts +24 -0
- package/src/ipfs/kv.ts +476 -0
- package/src/ipfs/value.ts +85 -0
- package/src/kv-store.ts +128 -0
- package/src/logging.ts +88 -0
- package/src/mainline/error.ts +108 -0
- package/src/mainline/index.ts +23 -0
- package/src/mainline/kv.ts +411 -0
- package/src/server/error.ts +83 -0
- package/src/server/index.ts +29 -0
- package/src/server/kv.ts +211 -0
- package/src/server/memory-kv.ts +191 -0
- package/src/server/server-kv.ts +92 -0
- package/src/server/server.ts +369 -0
- package/src/server/sqlite-kv.ts +295 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server module for Hubert distributed storage.
|
|
3
|
+
*
|
|
4
|
+
* This module provides server-side storage implementations and the HTTP server.
|
|
5
|
+
*
|
|
6
|
+
* Port of server/mod.rs from hubert-rust.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Error types
|
|
12
|
+
export {
|
|
13
|
+
ServerError,
|
|
14
|
+
ServerGeneralError,
|
|
15
|
+
ServerNetworkError,
|
|
16
|
+
ServerParseError,
|
|
17
|
+
SqliteError,
|
|
18
|
+
} from "./error.js";
|
|
19
|
+
|
|
20
|
+
// Storage backends
|
|
21
|
+
export { MemoryKv } from "./memory-kv.js";
|
|
22
|
+
export { SqliteKv } from "./sqlite-kv.js";
|
|
23
|
+
export { type ServerKv, createMemoryKv, createSqliteKv } from "./server-kv.js";
|
|
24
|
+
|
|
25
|
+
// HTTP server
|
|
26
|
+
export { Server, type ServerConfig, defaultServerConfig } from "./server.js";
|
|
27
|
+
|
|
28
|
+
// HTTP client
|
|
29
|
+
export { ServerKvClient } from "./kv.js";
|
package/src/server/kv.ts
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-backed key-value store using HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* Port of server/kv.rs from hubert-rust.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type ARID } from "@bcts/components";
|
|
10
|
+
import { Envelope } from "@bcts/envelope";
|
|
11
|
+
|
|
12
|
+
import { AlreadyExistsError } from "../error.js";
|
|
13
|
+
import { type KvStore } from "../kv-store.js";
|
|
14
|
+
import { verboseNewline, verbosePrintDot, verbosePrintln } from "../logging.js";
|
|
15
|
+
import { ServerGeneralError, ServerNetworkError, ServerParseError } from "./error.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Server-backed key-value store using HTTP API.
|
|
19
|
+
*
|
|
20
|
+
* This implementation communicates with a Hubert server via HTTP POST requests.
|
|
21
|
+
*
|
|
22
|
+
* Port of `struct ServerKvClient` from server/kv.rs lines 6-37.
|
|
23
|
+
*
|
|
24
|
+
* @category Server Backend
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const store = new ServerKvClient("http://127.0.0.1:45678");
|
|
29
|
+
* const arid = ARID.new();
|
|
30
|
+
* const envelope = Envelope.new("Hello, Server!");
|
|
31
|
+
*
|
|
32
|
+
* // Put envelope (write-once)
|
|
33
|
+
* await store.put(arid, envelope);
|
|
34
|
+
*
|
|
35
|
+
* // Get envelope with verbose logging
|
|
36
|
+
* const retrieved = await store.get(arid, undefined, true);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class ServerKvClient implements KvStore {
|
|
40
|
+
private readonly baseUrl: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a new server KV store client.
|
|
44
|
+
*
|
|
45
|
+
* Port of `ServerKvClient::new()` from server/kv.rs lines 39-46.
|
|
46
|
+
*
|
|
47
|
+
* @param baseUrl - Base URL of the Hubert server (e.g., "http://127.0.0.1:45678")
|
|
48
|
+
*/
|
|
49
|
+
constructor(baseUrl: string) {
|
|
50
|
+
this.baseUrl = baseUrl;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Store an envelope at the given ARID.
|
|
55
|
+
*
|
|
56
|
+
* Port of `KvStore::put()` implementation from server/kv.rs lines 67-122.
|
|
57
|
+
*/
|
|
58
|
+
async put(
|
|
59
|
+
arid: ARID,
|
|
60
|
+
envelope: Envelope,
|
|
61
|
+
ttlSeconds?: number,
|
|
62
|
+
verbose?: boolean,
|
|
63
|
+
): Promise<string> {
|
|
64
|
+
if (verbose) {
|
|
65
|
+
verbosePrintln("Starting server put operation");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Format body with optional TTL on third line
|
|
69
|
+
let body: string;
|
|
70
|
+
if (ttlSeconds !== undefined) {
|
|
71
|
+
body = `${arid.urString()}\n${envelope.urString()}\n${ttlSeconds}`;
|
|
72
|
+
} else {
|
|
73
|
+
body = `${arid.urString()}\n${envelope.urString()}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (verbose) {
|
|
77
|
+
verbosePrintln("Sending PUT request to server");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetch(`${this.baseUrl}/put`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body,
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "text/plain",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (response.status === 200) {
|
|
90
|
+
if (verbose) {
|
|
91
|
+
verbosePrintln("Server put operation completed");
|
|
92
|
+
}
|
|
93
|
+
return "Stored successfully";
|
|
94
|
+
} else if (response.status === 409) {
|
|
95
|
+
if (verbose) {
|
|
96
|
+
verbosePrintln("Server put operation failed");
|
|
97
|
+
}
|
|
98
|
+
throw new AlreadyExistsError(arid.urString());
|
|
99
|
+
} else {
|
|
100
|
+
if (verbose) {
|
|
101
|
+
verbosePrintln("Server put operation failed");
|
|
102
|
+
}
|
|
103
|
+
const errorMsg = await response.text();
|
|
104
|
+
throw new ServerGeneralError(errorMsg);
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error instanceof AlreadyExistsError || error instanceof ServerGeneralError) {
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
throw new ServerNetworkError(error instanceof Error ? error.message : String(error));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Retrieve an envelope for the given ARID.
|
|
116
|
+
*
|
|
117
|
+
* Port of `KvStore::get()` implementation from server/kv.rs lines 124-212.
|
|
118
|
+
*/
|
|
119
|
+
async get(arid: ARID, timeoutSeconds?: number, verbose?: boolean): Promise<Envelope | null> {
|
|
120
|
+
let printedDot = false;
|
|
121
|
+
|
|
122
|
+
if (verbose) {
|
|
123
|
+
verbosePrintln("Starting server get operation");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const timeout = timeoutSeconds ?? 30; // Default 30 seconds
|
|
127
|
+
const deadline = Date.now() + timeout * 1000;
|
|
128
|
+
// Changed to 1000ms for verbose mode polling
|
|
129
|
+
const pollInterval = 1000;
|
|
130
|
+
|
|
131
|
+
if (verbose) {
|
|
132
|
+
verbosePrintln("Polling server for value");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
while (true) {
|
|
136
|
+
const body = arid.urString();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const response = await fetch(`${this.baseUrl}/get`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
body,
|
|
142
|
+
headers: {
|
|
143
|
+
"Content-Type": "text/plain",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (response.status === 200) {
|
|
148
|
+
if (verbose && printedDot) {
|
|
149
|
+
verboseNewline();
|
|
150
|
+
}
|
|
151
|
+
if (verbose) {
|
|
152
|
+
verbosePrintln("Value found on server");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const envelopeStr = await response.text();
|
|
156
|
+
try {
|
|
157
|
+
const envelope = Envelope.fromUrString(envelopeStr);
|
|
158
|
+
|
|
159
|
+
if (verbose) {
|
|
160
|
+
verbosePrintln("Server get operation completed");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return envelope;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
throw new ServerParseError(error instanceof Error ? error.message : String(error));
|
|
166
|
+
}
|
|
167
|
+
} else if (response.status === 404) {
|
|
168
|
+
// Not found yet - check if we should keep polling
|
|
169
|
+
if (Date.now() >= deadline) {
|
|
170
|
+
// Timeout reached
|
|
171
|
+
if (verbose && printedDot) {
|
|
172
|
+
verboseNewline();
|
|
173
|
+
}
|
|
174
|
+
if (verbose) {
|
|
175
|
+
verbosePrintln("Timeout reached, value not found");
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Print polling dot if verbose
|
|
181
|
+
if (verbose) {
|
|
182
|
+
verbosePrintDot();
|
|
183
|
+
printedDot = true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Wait before retrying (now 1000ms)
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
188
|
+
} else {
|
|
189
|
+
const errorMsg = await response.text();
|
|
190
|
+
throw new ServerGeneralError(errorMsg);
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error instanceof ServerGeneralError || error instanceof ServerParseError) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
throw new ServerNetworkError(error instanceof Error ? error.message : String(error));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Check if an envelope exists at the given ARID.
|
|
203
|
+
*
|
|
204
|
+
* Port of `KvStore::exists()` implementation from server/kv.rs lines 214-218.
|
|
205
|
+
*/
|
|
206
|
+
async exists(arid: ARID): Promise<boolean> {
|
|
207
|
+
// Use a short timeout for exists check (1 second), no verbose
|
|
208
|
+
const result = await this.get(arid, 1, false);
|
|
209
|
+
return result !== null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory key-value store for Gordian Envelopes.
|
|
3
|
+
*
|
|
4
|
+
* Port of server/memory_kv.rs from hubert-rust.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type ARID } from "@bcts/components";
|
|
10
|
+
import { type Envelope } from "@bcts/envelope";
|
|
11
|
+
|
|
12
|
+
import { AlreadyExistsError } from "../error.js";
|
|
13
|
+
import { type KvStore } from "../kv-store.js";
|
|
14
|
+
import { verbosePrintln } from "../logging.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Storage entry with envelope data and optional expiration.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
interface StorageEntry {
|
|
21
|
+
/** CBOR-encoded envelope data */
|
|
22
|
+
envelopeCbor: Uint8Array;
|
|
23
|
+
/** Expiration timestamp in milliseconds, or undefined for no expiration */
|
|
24
|
+
expiresAt?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* In-memory key-value store for Gordian Envelopes.
|
|
29
|
+
*
|
|
30
|
+
* Provides volatile storage with TTL support and automatic cleanup of
|
|
31
|
+
* expired entries.
|
|
32
|
+
*
|
|
33
|
+
* Port of `struct MemoryKv` from server/memory_kv.rs lines 14-21.
|
|
34
|
+
*
|
|
35
|
+
* @category Server Backend
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const store = new MemoryKv();
|
|
40
|
+
* const arid = ARID.new();
|
|
41
|
+
* const envelope = Envelope.new("Hello, Memory!");
|
|
42
|
+
*
|
|
43
|
+
* await store.put(arid, envelope, 3600); // 1 hour TTL
|
|
44
|
+
* const result = await store.get(arid);
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export class MemoryKv implements KvStore {
|
|
48
|
+
private readonly storage: Map<string, StorageEntry>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a new in-memory key-value store.
|
|
52
|
+
*
|
|
53
|
+
* Port of `MemoryKv::new()` from server/memory_kv.rs lines 29-33.
|
|
54
|
+
*/
|
|
55
|
+
constructor() {
|
|
56
|
+
this.storage = new Map();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if an ARID exists and is not expired.
|
|
61
|
+
*
|
|
62
|
+
* Port of `check_exists()` from server/memory_kv.rs lines 36-53.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
private checkExists(arid: ARID): boolean {
|
|
67
|
+
const key = arid.urString();
|
|
68
|
+
const entry = this.storage.get(key);
|
|
69
|
+
|
|
70
|
+
if (entry) {
|
|
71
|
+
if (entry.expiresAt !== undefined && Date.now() >= entry.expiresAt) {
|
|
72
|
+
// Entry is expired, remove it
|
|
73
|
+
this.storage.delete(key);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Store an envelope at the given ARID.
|
|
83
|
+
*
|
|
84
|
+
* Port of `KvStore::put()` implementation from server/memory_kv.rs lines 62-102.
|
|
85
|
+
*/
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
87
|
+
async put(
|
|
88
|
+
arid: ARID,
|
|
89
|
+
envelope: Envelope,
|
|
90
|
+
ttlSeconds?: number,
|
|
91
|
+
verbose?: boolean,
|
|
92
|
+
): Promise<string> {
|
|
93
|
+
const key = arid.urString();
|
|
94
|
+
|
|
95
|
+
// Check if already exists
|
|
96
|
+
if (this.storage.has(key)) {
|
|
97
|
+
if (verbose) {
|
|
98
|
+
verbosePrintln(`PUT ${key} ALREADY_EXISTS`);
|
|
99
|
+
}
|
|
100
|
+
throw new AlreadyExistsError(key);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const expiresAt = ttlSeconds !== undefined ? Date.now() + ttlSeconds * 1000 : undefined;
|
|
104
|
+
const envelopeCbor = envelope.taggedCborData();
|
|
105
|
+
|
|
106
|
+
const entry: StorageEntry = { envelopeCbor };
|
|
107
|
+
if (expiresAt !== undefined) {
|
|
108
|
+
entry.expiresAt = expiresAt;
|
|
109
|
+
}
|
|
110
|
+
this.storage.set(key, entry);
|
|
111
|
+
|
|
112
|
+
if (verbose) {
|
|
113
|
+
const ttlMsg = ttlSeconds !== undefined ? ` (TTL ${ttlSeconds}s)` : "";
|
|
114
|
+
verbosePrintln(`PUT ${key}${ttlMsg} OK (Memory)`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return "Stored in memory";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Retrieve an envelope for the given ARID.
|
|
122
|
+
*
|
|
123
|
+
* Port of `KvStore::get()` implementation from server/memory_kv.rs lines 104-181.
|
|
124
|
+
*/
|
|
125
|
+
async get(arid: ARID, timeoutSeconds?: number, verbose?: boolean): Promise<Envelope | null> {
|
|
126
|
+
const timeout = timeoutSeconds ?? 30;
|
|
127
|
+
const start = Date.now();
|
|
128
|
+
let firstAttempt = true;
|
|
129
|
+
const key = arid.urString();
|
|
130
|
+
|
|
131
|
+
// Dynamic import to avoid circular dependencies
|
|
132
|
+
const { EnvelopeDecoder } = await import("@bcts/envelope");
|
|
133
|
+
|
|
134
|
+
while (true) {
|
|
135
|
+
const entry = this.storage.get(key);
|
|
136
|
+
|
|
137
|
+
if (entry) {
|
|
138
|
+
// Check if expired
|
|
139
|
+
if (entry.expiresAt !== undefined && Date.now() >= entry.expiresAt) {
|
|
140
|
+
// Entry is expired, remove it
|
|
141
|
+
this.storage.delete(key);
|
|
142
|
+
if (verbose) {
|
|
143
|
+
verbosePrintln(`GET ${key} EXPIRED`);
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse CBOR bytes back to Envelope
|
|
149
|
+
try {
|
|
150
|
+
const envelope = EnvelopeDecoder.tryFromCborData(entry.envelopeCbor);
|
|
151
|
+
if (verbose) {
|
|
152
|
+
verbosePrintln(`GET ${key} OK (Memory)`);
|
|
153
|
+
}
|
|
154
|
+
return envelope;
|
|
155
|
+
} catch {
|
|
156
|
+
// If parsing fails, treat as not found
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Not found yet
|
|
162
|
+
const elapsed = (Date.now() - start) / 1000;
|
|
163
|
+
if (elapsed >= timeout) {
|
|
164
|
+
if (verbose) {
|
|
165
|
+
verbosePrintln(`GET ${key} NOT_FOUND (timeout after ${timeout}s)`);
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (firstAttempt && verbose) {
|
|
171
|
+
verbosePrintln(`Polling for ${key} (timeout: ${timeout}s)`);
|
|
172
|
+
firstAttempt = false;
|
|
173
|
+
} else if (verbose) {
|
|
174
|
+
process.stdout.write(".");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Wait 500ms before polling again
|
|
178
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if an envelope exists at the given ARID.
|
|
184
|
+
*
|
|
185
|
+
* Port of `KvStore::exists()` implementation from server/memory_kv.rs lines 183-186.
|
|
186
|
+
*/
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
188
|
+
async exists(arid: ARID): Promise<boolean> {
|
|
189
|
+
return this.checkExists(arid);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side key-value storage backend union type.
|
|
3
|
+
*
|
|
4
|
+
* Port of server/server_kv.rs from hubert-rust.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type ARID } from "@bcts/components";
|
|
10
|
+
import { type Envelope } from "@bcts/envelope";
|
|
11
|
+
|
|
12
|
+
import { type KvStore } from "../kv-store.js";
|
|
13
|
+
import { MemoryKv } from "./memory-kv.js";
|
|
14
|
+
import { type SqliteKv } from "./sqlite-kv.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Server-side key-value storage backend.
|
|
18
|
+
*
|
|
19
|
+
* This type allows selecting between in-memory and SQLite storage
|
|
20
|
+
* at server setup time.
|
|
21
|
+
*
|
|
22
|
+
* Port of `enum ServerKv` from server/server_kv.rs lines 7-15.
|
|
23
|
+
*
|
|
24
|
+
* @category Server Backend
|
|
25
|
+
*/
|
|
26
|
+
export type ServerKv = MemoryKv | SqliteKv;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new in-memory server KV store.
|
|
30
|
+
*
|
|
31
|
+
* Port of `ServerKv::memory()` from server/server_kv.rs line 19.
|
|
32
|
+
*
|
|
33
|
+
* @returns A new MemoryKv instance
|
|
34
|
+
* @category Server Backend
|
|
35
|
+
*/
|
|
36
|
+
export function createMemoryKv(): MemoryKv {
|
|
37
|
+
return new MemoryKv();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a new SQLite-backed server KV store.
|
|
42
|
+
*
|
|
43
|
+
* Port of `ServerKv::sqlite()` from server/server_kv.rs line 22.
|
|
44
|
+
*
|
|
45
|
+
* @param store - The SqliteKv instance to use
|
|
46
|
+
* @returns The same SqliteKv instance
|
|
47
|
+
* @category Server Backend
|
|
48
|
+
*/
|
|
49
|
+
export function createSqliteKv(store: SqliteKv): SqliteKv {
|
|
50
|
+
return store;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Synchronously put an envelope into the store.
|
|
55
|
+
*
|
|
56
|
+
* This function wraps the async KvStore trait implementation for use
|
|
57
|
+
* in synchronous HTTP handlers.
|
|
58
|
+
*
|
|
59
|
+
* Port of `put_sync()` from server/server_kv.rs lines 27-52.
|
|
60
|
+
*
|
|
61
|
+
* @param store - The KvStore to use
|
|
62
|
+
* @param arid - The ARID to store at
|
|
63
|
+
* @param envelope - The envelope to store
|
|
64
|
+
* @param ttlSeconds - TTL in seconds
|
|
65
|
+
* @returns Promise that resolves when storage is complete
|
|
66
|
+
* @internal
|
|
67
|
+
*/
|
|
68
|
+
export async function putSync(
|
|
69
|
+
store: KvStore,
|
|
70
|
+
arid: ARID,
|
|
71
|
+
envelope: Envelope,
|
|
72
|
+
ttlSeconds: number,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
await store.put(arid, envelope, ttlSeconds, false);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Synchronously get an envelope from the store.
|
|
79
|
+
*
|
|
80
|
+
* This function wraps the async KvStore trait implementation for use
|
|
81
|
+
* in synchronous HTTP handlers.
|
|
82
|
+
*
|
|
83
|
+
* Port of `get_sync()` from server/server_kv.rs lines 58-71.
|
|
84
|
+
*
|
|
85
|
+
* @param store - The KvStore to use
|
|
86
|
+
* @param arid - The ARID to retrieve
|
|
87
|
+
* @returns The envelope if found, or null
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
export async function getSync(store: KvStore, arid: ARID): Promise<Envelope | null> {
|
|
91
|
+
return await store.get(arid, 0, false);
|
|
92
|
+
}
|