@peerbit/native-backbone 0.1.4 → 0.2.1
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/README.md +31 -0
- package/dist/src/durability/codec.d.ts +86 -0
- package/dist/src/durability/codec.d.ts.map +1 -0
- package/dist/src/durability/codec.js +365 -0
- package/dist/src/durability/codec.js.map +1 -0
- package/dist/src/durability/lease.d.ts +59 -0
- package/dist/src/durability/lease.d.ts.map +1 -0
- package/dist/src/durability/lease.js +48 -0
- package/dist/src/durability/lease.js.map +1 -0
- package/dist/src/durability/memory-storage.d.ts +37 -0
- package/dist/src/durability/memory-storage.d.ts.map +1 -0
- package/dist/src/durability/memory-storage.js +436 -0
- package/dist/src/durability/memory-storage.js.map +1 -0
- package/dist/src/durability/node-lease.d.ts +14 -0
- package/dist/src/durability/node-lease.d.ts.map +1 -0
- package/dist/src/durability/node-lease.js +214 -0
- package/dist/src/durability/node-lease.js.map +1 -0
- package/dist/src/durability/node-storage.d.ts +76 -0
- package/dist/src/durability/node-storage.d.ts.map +1 -0
- package/dist/src/durability/node-storage.js +1813 -0
- package/dist/src/durability/node-storage.js.map +1 -0
- package/dist/src/durability/storage.d.ts +224 -0
- package/dist/src/durability/storage.d.ts.map +1 -0
- package/dist/src/durability/storage.js +343 -0
- package/dist/src/durability/storage.js.map +1 -0
- package/dist/src/index.d.ts +93 -15
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +717 -199
- package/dist/src/index.js.map +1 -1
- package/dist/wasm/README.md +31 -0
- package/dist/wasm/native_backbone.d.ts +131 -115
- package/dist/wasm/native_backbone.js +96 -0
- package/dist/wasm/native_backbone_bg.wasm +0 -0
- package/dist/wasm/native_backbone_bg.wasm.d.ts +119 -115
- package/package.json +4 -3
- package/src/durability/codec.ts +683 -0
- package/src/durability/lease.ts +87 -0
- package/src/durability/memory-storage.ts +593 -0
- package/src/durability/node-lease.ts +293 -0
- package/src/durability/node-storage.ts +2798 -0
- package/src/durability/storage.ts +682 -0
- package/src/durability.rs +1872 -0
- package/src/index.ts +1392 -735
- package/src/lib.rs +1 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NATIVE_DURABILITY_MAX_U64,
|
|
3
|
+
NATIVE_DURABILITY_MAX_WRITER_ID_BYTES,
|
|
4
|
+
type NativeDurabilityFence,
|
|
5
|
+
NativeDurabilityFenceExhaustedError,
|
|
6
|
+
type NativeDurabilityLease,
|
|
7
|
+
NativeDurabilityLeaseClosedError,
|
|
8
|
+
NativeDurabilityLeaseDirectorySyncError,
|
|
9
|
+
NativeDurabilityLeaseStateError,
|
|
10
|
+
NativeDurabilityLeaseUnavailableError,
|
|
11
|
+
} from "./lease.js";
|
|
12
|
+
|
|
13
|
+
export const NATIVE_DURABILITY_NODE_LEASE_DIRECTORY_NAME =
|
|
14
|
+
".peerbit-native-durability-lease";
|
|
15
|
+
const LEASE_STATE_KEY = "fence";
|
|
16
|
+
const LEASE_STATE_VERSION = 1;
|
|
17
|
+
|
|
18
|
+
type PersistedLeaseState = {
|
|
19
|
+
version: typeof LEASE_STATE_VERSION;
|
|
20
|
+
epoch: string;
|
|
21
|
+
ownerId: string;
|
|
22
|
+
domainId: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type NativeLevelDatabase = {
|
|
26
|
+
readonly status: string;
|
|
27
|
+
open(): Promise<void>;
|
|
28
|
+
get(key: string): Promise<string | undefined>;
|
|
29
|
+
put(key: string, value: string, options: { sync: true }): Promise<void>;
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ClassicLevelConstructor = new (
|
|
34
|
+
location: string,
|
|
35
|
+
options: { keyEncoding: "utf8"; valueEncoding: "utf8" },
|
|
36
|
+
) => NativeLevelDatabase;
|
|
37
|
+
|
|
38
|
+
type NodeFsPromises = {
|
|
39
|
+
realpath(path: string): Promise<string>;
|
|
40
|
+
open(
|
|
41
|
+
path: string,
|
|
42
|
+
flags: "r",
|
|
43
|
+
): Promise<{
|
|
44
|
+
sync(): Promise<void>;
|
|
45
|
+
close(): Promise<void>;
|
|
46
|
+
}>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type NodePath = {
|
|
50
|
+
join(...parts: string[]): string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type NodeCrypto = {
|
|
54
|
+
randomUUID(): string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const dynamicImport = <T>(specifier: string): Promise<T> =>
|
|
58
|
+
import(/* @vite-ignore */ specifier) as Promise<T>;
|
|
59
|
+
|
|
60
|
+
const importClassicLevel = async (): Promise<ClassicLevelConstructor> => {
|
|
61
|
+
const module = await dynamicImport<{ ClassicLevel: ClassicLevelConstructor }>(
|
|
62
|
+
"classic-level",
|
|
63
|
+
);
|
|
64
|
+
return module.ClassicLevel;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const hasErrorCode = (error: unknown, expected: string): boolean => {
|
|
68
|
+
const visited = new Set<unknown>();
|
|
69
|
+
let current = error;
|
|
70
|
+
while (
|
|
71
|
+
current != null &&
|
|
72
|
+
typeof current === "object" &&
|
|
73
|
+
!visited.has(current)
|
|
74
|
+
) {
|
|
75
|
+
visited.add(current);
|
|
76
|
+
if ((current as { code?: unknown }).code === expected) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
current = (current as { cause?: unknown }).cause;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const isMissingKeyError = (error: unknown): boolean =>
|
|
85
|
+
hasErrorCode(error, "LEVEL_NOT_FOUND");
|
|
86
|
+
|
|
87
|
+
const decodeLeaseState = (
|
|
88
|
+
encoded: string,
|
|
89
|
+
directory: string,
|
|
90
|
+
): PersistedLeaseState => {
|
|
91
|
+
let decoded: unknown;
|
|
92
|
+
try {
|
|
93
|
+
decoded = JSON.parse(encoded);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new NativeDurabilityLeaseStateError(
|
|
96
|
+
directory,
|
|
97
|
+
`Invalid native durability lease state in ${directory}`,
|
|
98
|
+
{ cause: error },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const value = decoded as Partial<PersistedLeaseState> | null;
|
|
102
|
+
if (
|
|
103
|
+
value?.version !== LEASE_STATE_VERSION ||
|
|
104
|
+
typeof value.epoch !== "string" ||
|
|
105
|
+
!/^(0|[1-9][0-9]*)$/.test(value.epoch) ||
|
|
106
|
+
BigInt(value.epoch) > NATIVE_DURABILITY_MAX_U64 ||
|
|
107
|
+
typeof value.ownerId !== "string" ||
|
|
108
|
+
value.ownerId.length === 0 ||
|
|
109
|
+
new TextEncoder().encode(value.ownerId).byteLength >
|
|
110
|
+
NATIVE_DURABILITY_MAX_WRITER_ID_BYTES ||
|
|
111
|
+
typeof value.domainId !== "string" ||
|
|
112
|
+
value.domainId.length === 0 ||
|
|
113
|
+
new TextEncoder().encode(value.domainId).byteLength >
|
|
114
|
+
NATIVE_DURABILITY_MAX_WRITER_ID_BYTES
|
|
115
|
+
) {
|
|
116
|
+
throw new NativeDurabilityLeaseStateError(
|
|
117
|
+
directory,
|
|
118
|
+
`Invalid native durability lease state in ${directory}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return value as PersistedLeaseState;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const readLeaseState = async (
|
|
125
|
+
database: NativeLevelDatabase,
|
|
126
|
+
directory: string,
|
|
127
|
+
): Promise<PersistedLeaseState | undefined> => {
|
|
128
|
+
try {
|
|
129
|
+
const encoded = await database.get(LEASE_STATE_KEY);
|
|
130
|
+
return encoded == null ? undefined : decodeLeaseState(encoded, directory);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (isMissingKeyError(error)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const syncDirectory = async (
|
|
140
|
+
fs: NodeFsPromises,
|
|
141
|
+
directory: string,
|
|
142
|
+
): Promise<void> => {
|
|
143
|
+
let handle: Awaited<ReturnType<NodeFsPromises["open"]>> | undefined;
|
|
144
|
+
try {
|
|
145
|
+
handle = await fs.open(directory, "r");
|
|
146
|
+
await handle.sync();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new NativeDurabilityLeaseDirectorySyncError(directory, {
|
|
149
|
+
cause: error,
|
|
150
|
+
});
|
|
151
|
+
} finally {
|
|
152
|
+
await handle?.close();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
class NodeNativeDurabilityLease implements NativeDurabilityLease {
|
|
157
|
+
private state: "held" | "closing" | "closed" = "held";
|
|
158
|
+
private closePromise?: Promise<void>;
|
|
159
|
+
private activeOperations = 0;
|
|
160
|
+
private drainPromise?: Promise<void>;
|
|
161
|
+
private resolveDrain?: () => void;
|
|
162
|
+
|
|
163
|
+
constructor(
|
|
164
|
+
readonly fence: NativeDurabilityFence,
|
|
165
|
+
private readonly database: NativeLevelDatabase,
|
|
166
|
+
) {}
|
|
167
|
+
|
|
168
|
+
async assertHeld(): Promise<void> {
|
|
169
|
+
if (this.state !== "held" || this.database.status !== "open") {
|
|
170
|
+
throw new NativeDurabilityLeaseClosedError(this.fence);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async runWhileHeld<T>(operation: () => Promise<T>): Promise<T> {
|
|
175
|
+
if (this.state !== "held" || this.database.status !== "open") {
|
|
176
|
+
throw new NativeDurabilityLeaseClosedError(this.fence);
|
|
177
|
+
}
|
|
178
|
+
// The state check and increment are synchronous, so close() cannot release
|
|
179
|
+
// the database lock between them.
|
|
180
|
+
this.activeOperations++;
|
|
181
|
+
try {
|
|
182
|
+
return await operation();
|
|
183
|
+
} finally {
|
|
184
|
+
this.activeOperations--;
|
|
185
|
+
if (this.activeOperations === 0) {
|
|
186
|
+
this.resolveDrain?.();
|
|
187
|
+
this.resolveDrain = undefined;
|
|
188
|
+
this.drainPromise = undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
close(): Promise<void> {
|
|
194
|
+
if (this.closePromise) {
|
|
195
|
+
return this.closePromise;
|
|
196
|
+
}
|
|
197
|
+
this.state = "closing";
|
|
198
|
+
if (this.activeOperations > 0 && !this.drainPromise) {
|
|
199
|
+
this.drainPromise = new Promise<void>((resolve) => {
|
|
200
|
+
this.resolveDrain = resolve;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
this.closePromise = (this.drainPromise ?? Promise.resolve())
|
|
204
|
+
.then(() => this.database.close())
|
|
205
|
+
.finally(() => {
|
|
206
|
+
this.state = "closed";
|
|
207
|
+
});
|
|
208
|
+
return this.closePromise;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Acquire the crash-released Node ownership lease for an existing program
|
|
214
|
+
* directory.
|
|
215
|
+
*
|
|
216
|
+
* The dedicated ClassicLevel database lives beside, rather than inside, the
|
|
217
|
+
* transaction namespace. Its native LevelDB `LOCK` is held for this object's
|
|
218
|
+
* lifetime and is released by the operating system when the process dies. The
|
|
219
|
+
* lock database itself is intentionally retained so fence epochs cannot be
|
|
220
|
+
* reused after a normal reopen.
|
|
221
|
+
*/
|
|
222
|
+
export const acquireNativeDurabilityNodeLease = async (
|
|
223
|
+
programDirectory: string,
|
|
224
|
+
): Promise<NativeDurabilityLease> => {
|
|
225
|
+
const [fs, path, crypto, ClassicLevel] = await Promise.all([
|
|
226
|
+
dynamicImport<NodeFsPromises>("node:fs/promises"),
|
|
227
|
+
dynamicImport<NodePath>("node:path"),
|
|
228
|
+
dynamicImport<NodeCrypto>("node:crypto"),
|
|
229
|
+
importClassicLevel(),
|
|
230
|
+
]);
|
|
231
|
+
let canonicalDirectory: string;
|
|
232
|
+
try {
|
|
233
|
+
canonicalDirectory = await fs.realpath(programDirectory);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new NativeDurabilityLeaseStateError(
|
|
236
|
+
programDirectory,
|
|
237
|
+
`Native durability program directory must already exist: ${programDirectory}`,
|
|
238
|
+
{ cause: error },
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const leaseDirectory = path.join(
|
|
242
|
+
canonicalDirectory,
|
|
243
|
+
NATIVE_DURABILITY_NODE_LEASE_DIRECTORY_NAME,
|
|
244
|
+
);
|
|
245
|
+
const database = new ClassicLevel(leaseDirectory, {
|
|
246
|
+
keyEncoding: "utf8",
|
|
247
|
+
valueEncoding: "utf8",
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
await database.open();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (hasErrorCode(error, "LEVEL_LOCKED")) {
|
|
254
|
+
throw new NativeDurabilityLeaseUnavailableError(canonicalDirectory, {
|
|
255
|
+
cause: error,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const previous = await readLeaseState(database, canonicalDirectory);
|
|
263
|
+
if (BigInt(previous?.epoch ?? "0") === NATIVE_DURABILITY_MAX_U64) {
|
|
264
|
+
throw new NativeDurabilityFenceExhaustedError(canonicalDirectory);
|
|
265
|
+
}
|
|
266
|
+
const fence: NativeDurabilityFence = Object.freeze({
|
|
267
|
+
epoch: BigInt(previous?.epoch ?? "0") + 1n,
|
|
268
|
+
ownerId: crypto.randomUUID(),
|
|
269
|
+
domainId: previous?.domainId ?? crypto.randomUUID(),
|
|
270
|
+
});
|
|
271
|
+
const next: PersistedLeaseState = {
|
|
272
|
+
version: LEASE_STATE_VERSION,
|
|
273
|
+
epoch: fence.epoch.toString(),
|
|
274
|
+
ownerId: fence.ownerId,
|
|
275
|
+
domainId: fence.domainId,
|
|
276
|
+
};
|
|
277
|
+
await database.put(LEASE_STATE_KEY, JSON.stringify(next), { sync: true });
|
|
278
|
+
// LevelDB's sync option covers its value/WAL. Explicit directory syncs
|
|
279
|
+
// additionally make creation of the lease database and its files durable
|
|
280
|
+
// before the fence is handed to a transaction writer.
|
|
281
|
+
await syncDirectory(fs, leaseDirectory);
|
|
282
|
+
await syncDirectory(fs, canonicalDirectory);
|
|
283
|
+
return new NodeNativeDurabilityLease(fence, database);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
try {
|
|
286
|
+
await database.close();
|
|
287
|
+
} catch {
|
|
288
|
+
// The acquisition error is authoritative. A failed close must not make
|
|
289
|
+
// the partially initialized lease usable by this process.
|
|
290
|
+
}
|
|
291
|
+
throw error;
|
|
292
|
+
}
|
|
293
|
+
};
|