keri-ts 0.1.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.
Files changed (55) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +70 -0
  3. package/esm/_dnt.polyfills.js +127 -0
  4. package/esm/_dnt.shims.js +61 -0
  5. package/esm/app/cli/agent.js +37 -0
  6. package/esm/app/cli/cli-node.js +9 -0
  7. package/esm/app/cli/cli.js +195 -0
  8. package/esm/app/cli/db-dump.js +68 -0
  9. package/esm/app/cli/init.js +75 -0
  10. package/esm/app/server.js +77 -0
  11. package/esm/core/bytes.js +39 -0
  12. package/esm/core/errors.js +26 -0
  13. package/esm/core/index.js +7 -0
  14. package/esm/db/basing.js +168 -0
  15. package/esm/db/core/db.js +19 -0
  16. package/esm/db/core/lmdber.js +474 -0
  17. package/esm/db/core/path-manager.js +450 -0
  18. package/esm/db/index.js +4 -0
  19. package/esm/npm/index.js +4 -0
  20. package/esm/package.json +3 -0
  21. package/package.json +57 -0
  22. package/types/_dnt.polyfills.d.ts +101 -0
  23. package/types/_dnt.polyfills.d.ts.map +1 -0
  24. package/types/_dnt.shims.d.ts +6 -0
  25. package/types/_dnt.shims.d.ts.map +1 -0
  26. package/types/app/cli/agent.d.ts +9 -0
  27. package/types/app/cli/agent.d.ts.map +1 -0
  28. package/types/app/cli/cli-node.d.ts +2 -0
  29. package/types/app/cli/cli-node.d.ts.map +1 -0
  30. package/types/app/cli/cli.d.ts +7 -0
  31. package/types/app/cli/cli.d.ts.map +1 -0
  32. package/types/app/cli/db-dump.d.ts +11 -0
  33. package/types/app/cli/db-dump.d.ts.map +1 -0
  34. package/types/app/cli/init.d.ts +3 -0
  35. package/types/app/cli/init.d.ts.map +1 -0
  36. package/types/app/server.d.ts +8 -0
  37. package/types/app/server.d.ts.map +1 -0
  38. package/types/core/bytes.d.ts +17 -0
  39. package/types/core/bytes.d.ts.map +1 -0
  40. package/types/core/errors.d.ts +19 -0
  41. package/types/core/errors.d.ts.map +1 -0
  42. package/types/core/index.d.ts +8 -0
  43. package/types/core/index.d.ts.map +1 -0
  44. package/types/db/basing.d.ts +80 -0
  45. package/types/db/basing.d.ts.map +1 -0
  46. package/types/db/core/db.d.ts +5 -0
  47. package/types/db/core/db.d.ts.map +1 -0
  48. package/types/db/core/lmdber.d.ts +135 -0
  49. package/types/db/core/lmdber.d.ts.map +1 -0
  50. package/types/db/core/path-manager.d.ts +92 -0
  51. package/types/db/core/path-manager.d.ts.map +1 -0
  52. package/types/db/index.d.ts +5 -0
  53. package/types/db/index.d.ts.map +1 -0
  54. package/types/npm/index.d.ts +5 -0
  55. package/types/npm/index.d.ts.map +1 -0
@@ -0,0 +1,474 @@
1
+ /**
2
+ * LMDBer - Core LMDB database manager
3
+ *
4
+ * Manages LMDB database environments and provides CRUD operations.
5
+ * Uses composition with PathManager instead of inheritance.
6
+ */
7
+ import * as dntShim from "../../_dnt.shims.js";
8
+ import { open } from "lmdb";
9
+ import { startsWith } from "../../core/bytes.js";
10
+ import { DatabaseKeyError, DatabaseNotOpenError, DatabaseOperationError, } from "../../core/errors.js";
11
+ import { PathManager, } from "./path-manager.js";
12
+ // Module-level encoder/decoder instances (stateless, reusable)
13
+ const encoder = new TextEncoder();
14
+ const decoder = new TextDecoder();
15
+ // Short helpers for string ↔ Uint8Array conversion
16
+ // bytes from string/text (UTF-8)
17
+ export const b = (t) => encoder.encode(t);
18
+ // text/string from bytes (UTF-8)
19
+ export const t = (b) => decoder.decode(b);
20
+ export const LMDBER_DEFAULTS = {
21
+ headDirPath: "/usr/local/var",
22
+ tailDirPath: "keri/db",
23
+ cleanTailDirPath: "keri/clean/db",
24
+ altHeadDirPath: "~",
25
+ altTailDirPath: ".keri/db",
26
+ altCleanTailDirPath: ".keri/clean/db",
27
+ tempHeadDir: "/tmp",
28
+ tempPrefix: "keri_lmdb_",
29
+ tempSuffix: "_test",
30
+ perm: 0o1700,
31
+ mode: "r+",
32
+ fext: "text",
33
+ maxNamedDBs: 96,
34
+ mapSize: 4 * 1024 * 1024 * 1024, // 4GB default
35
+ };
36
+ /**
37
+ * LMDBer manages LMDB database environments
38
+ * Uses composition with PathManager for path management
39
+ */
40
+ export class LMDBer {
41
+ constructor(options = {}, defaults) {
42
+ Object.defineProperty(this, "pathManager", {
43
+ enumerable: true,
44
+ configurable: true,
45
+ writable: true,
46
+ value: void 0
47
+ });
48
+ Object.defineProperty(this, "env", {
49
+ enumerable: true,
50
+ configurable: true,
51
+ writable: true,
52
+ value: void 0
53
+ });
54
+ Object.defineProperty(this, "readonly", {
55
+ enumerable: true,
56
+ configurable: true,
57
+ writable: true,
58
+ value: void 0
59
+ });
60
+ Object.defineProperty(this, "defaults", {
61
+ enumerable: true,
62
+ configurable: true,
63
+ writable: true,
64
+ value: void 0
65
+ });
66
+ this.defaults = { ...LMDBER_DEFAULTS, ...defaults };
67
+ // Create PathManager with composition
68
+ const pathDefaults = {
69
+ headDirPath: this.defaults.headDirPath,
70
+ tailDirPath: this.defaults.tailDirPath,
71
+ cleanTailDirPath: this.defaults.cleanTailDirPath,
72
+ altHeadDirPath: this.defaults.altHeadDirPath,
73
+ altTailDirPath: this.defaults.altTailDirPath,
74
+ altCleanTailDirPath: this.defaults.altCleanTailDirPath,
75
+ tempHeadDir: this.defaults.tempHeadDir,
76
+ tempPrefix: this.defaults.tempPrefix,
77
+ tempSuffix: this.defaults.tempSuffix,
78
+ perm: this.defaults.perm,
79
+ };
80
+ this.pathManager = new PathManager(options, pathDefaults);
81
+ this.env = null;
82
+ this.readonly = options.readonly || false;
83
+ }
84
+ get name() {
85
+ return this.pathManager.name;
86
+ }
87
+ get base() {
88
+ return this.pathManager.base;
89
+ }
90
+ get opened() {
91
+ return this.pathManager.opened && this.env !== null;
92
+ }
93
+ get temp() {
94
+ return this.pathManager.temp;
95
+ }
96
+ get path() {
97
+ return this.pathManager.path;
98
+ }
99
+ formatDbKeyError(key, error) {
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ return new DatabaseKeyError(`Key is empty, too big, or wrong size: ${message}`, { key: Array.from(key) });
102
+ }
103
+ /**
104
+ * Reopen the LMDB database
105
+ * Closes existing database if open before opening a new one to prevent double-free errors
106
+ */
107
+ *reopen(options = {}) {
108
+ const readonly = options.readonly ?? this.readonly;
109
+ this.readonly = readonly;
110
+ // Close existing database if open (prevents double-free when reopening)
111
+ if (this.env) {
112
+ try {
113
+ // Close synchronously - LMDB close() is synchronous
114
+ this.env.close();
115
+ }
116
+ catch (error) {
117
+ // Ignore close errors (database might already be closed)
118
+ console.warn(`Warning: Error closing existing LMDB environment: ${error}`);
119
+ }
120
+ this.env = null;
121
+ }
122
+ // Reopen path manager (now an Effection operation)
123
+ yield* this.pathManager.reopen(options);
124
+ if (!this.pathManager.path) {
125
+ return false;
126
+ }
127
+ let dbPath = this.pathManager.path;
128
+ // Get map size from environment variable or use default
129
+ const mapSizeEnv = dntShim.Deno.env.get("KERI_LMDB_MAP_SIZE");
130
+ const mapSize = mapSizeEnv
131
+ ? parseInt(mapSizeEnv, 10)
132
+ : this.defaults.mapSize;
133
+ // Check if database files exist before opening
134
+ const dbExists = yield* this.checkDatabaseExists();
135
+ // If readonly and database doesn't exist, we need to handle that gracefully
136
+ // For readonly mode, database files must exist
137
+ if (readonly && !dbExists) {
138
+ console.error(`Cannot open readonly database: database files do not exist at ${dbPath}`);
139
+ this.env = null;
140
+ return false;
141
+ }
142
+ // For readonly opens of existing databases, use a large mapSize that's safe
143
+ // LMDB will use the actual map size from the database file, but the Node.js
144
+ // lmdb package requires mapSize to be >= the database's actual map size
145
+ // Use 4GB (KERIpy default) or larger to ensure compatibility
146
+ const effectiveMapSize = readonly && dbExists
147
+ ? Math.max(mapSize, 4 * 1024 * 1024 * 1024) // At least 4GB for existing databases
148
+ : mapSize;
149
+ const dbConfig = {
150
+ path: dbPath, // Use directory path (Node.js lmdb should handle this)
151
+ maxDbs: this.defaults.maxNamedDBs,
152
+ mapSize: effectiveMapSize,
153
+ readOnly: readonly,
154
+ compression: false, // Disable compression for compatibility
155
+ encoding: "binary", // to mimic KERIpy behavior
156
+ keyEncoding: "binary", // to mimic KERIpy behavior
157
+ };
158
+ console.log(`Opening LMDB at: ${dbPath} (readonly: ${readonly}, mapSize: ${effectiveMapSize})`);
159
+ // Open LMDB environment
160
+ // LMDB's open() will create data.mdb and lock.mdb if they don't exist
161
+ try {
162
+ // do sync because wrapping synchronous native operations in action() can cause
163
+ // memory management issues with native bindings (double-free errors)
164
+ this.env = open(dbConfig);
165
+ console.log(`LMDB environment opened successfully`);
166
+ // Set version if new database and not readonly
167
+ if (this.opened && !readonly && !dbExists && !this.temp) {
168
+ // Set version for new database
169
+ const version = "1.0.0"; // Default version
170
+ this.setVer(version);
171
+ }
172
+ return this.opened;
173
+ }
174
+ catch (error) {
175
+ console.error(`Failed to open LMDB: ${error}`);
176
+ this.env = null;
177
+ return false;
178
+ }
179
+ }
180
+ /**
181
+ * Check if database already exists by checking for database files
182
+ */
183
+ *checkDatabaseExists() {
184
+ if (!this.pathManager.path) {
185
+ return false;
186
+ }
187
+ // Check if database files exist (now an Effection operation)
188
+ return yield* this.pathManager.databaseFilesExist();
189
+ }
190
+ /**
191
+ * Close the LMDB database
192
+ */
193
+ *close(clear = false) {
194
+ if (this.env) {
195
+ try {
196
+ this.env.close();
197
+ }
198
+ catch (error) {
199
+ // Ignore close errors (database might already be closed)
200
+ console.warn(`Error closing LMDB: ${error}`);
201
+ }
202
+ this.env = null;
203
+ }
204
+ // Close path manager (now an Effection operation)
205
+ yield* this.pathManager.close(clear);
206
+ return true;
207
+ }
208
+ /**
209
+ * Get database version
210
+ */
211
+ getVer() {
212
+ const env = this.env;
213
+ try {
214
+ const versionBytes = env.get(b("__version__"));
215
+ const version = t(versionBytes);
216
+ return version || null;
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ }
222
+ /**
223
+ * Set database version
224
+ */
225
+ setVer(val) {
226
+ const env = this.env;
227
+ try {
228
+ env.transactionSync(() => {
229
+ env.putSync(b("__version__"), b(val));
230
+ });
231
+ }
232
+ catch (error) {
233
+ const message = error instanceof Error ? error.message : String(error);
234
+ throw new DatabaseOperationError(`Failed to set database version: ${message}`, { version: val });
235
+ }
236
+ }
237
+ /**
238
+ * Open a named sub-database
239
+ */
240
+ openDB(name, dupsort = false) {
241
+ const env = this.env;
242
+ return env.openDB(name, {
243
+ keyEncoding: "binary",
244
+ encoding: "binary", // Use binary encoding for values (raw bytes) to match KERIpy
245
+ dupSort: dupsort,
246
+ });
247
+ }
248
+ /**
249
+ * Put value (no overwrite)
250
+ *
251
+ * @param db - Named sub-database
252
+ * @param key - Key bytes
253
+ * @param val - Value bytes
254
+ * @returns True if successfully written, False if key already exists
255
+ */
256
+ putVal(db, key, val) {
257
+ const env = this.env;
258
+ try {
259
+ const result = env.transactionSync(() => {
260
+ const existing = db.get(key);
261
+ if (existing !== null && existing !== undefined) {
262
+ return false;
263
+ }
264
+ db.put(key, val);
265
+ return true;
266
+ });
267
+ return result;
268
+ }
269
+ catch (error) {
270
+ throw this.formatDbKeyError(key, error);
271
+ }
272
+ }
273
+ /**
274
+ * Set value (overwrite allowed)
275
+ *
276
+ * @param db - Named sub-database
277
+ * @param key - Key bytes
278
+ * @param val - Value bytes
279
+ * @returns True if successfully written
280
+ */
281
+ setVal(db, key, val) {
282
+ const env = this.env;
283
+ try {
284
+ env.transactionSync(() => {
285
+ db.put(key, val);
286
+ });
287
+ return true;
288
+ }
289
+ catch (error) {
290
+ throw this.formatDbKeyError(key, error);
291
+ }
292
+ }
293
+ /**
294
+ * Get value
295
+ *
296
+ * @param db - Named sub-database
297
+ * @param key - Key bytes
298
+ * @returns Value bytes or null if not found
299
+ */
300
+ getVal(db, key) {
301
+ try {
302
+ const val = db.get(key);
303
+ if (val === null || val === undefined) {
304
+ return null;
305
+ }
306
+ else {
307
+ return val instanceof Uint8Array ? val : new Uint8Array(val);
308
+ }
309
+ }
310
+ catch (error) {
311
+ throw this.formatDbKeyError(key, error);
312
+ }
313
+ }
314
+ /**
315
+ * Delete value
316
+ *
317
+ * @param db - Named sub-database
318
+ * @param key - Key bytes
319
+ * @returns True if key existed, False otherwise
320
+ */
321
+ delVal(db, key) {
322
+ const env = this.env;
323
+ try {
324
+ const result = env.transactionSync(() => {
325
+ const exists = db.get(key) !== null && db.get(key) !== undefined;
326
+ if (exists) {
327
+ db.remove(key);
328
+ }
329
+ return exists;
330
+ });
331
+ return result;
332
+ }
333
+ catch (error) {
334
+ throw this.formatDbKeyError(key, error);
335
+ }
336
+ }
337
+ /**
338
+ * Count all values in database
339
+ *
340
+ * @param db - Named sub-database
341
+ * @returns Count of entries
342
+ */
343
+ cnt(db) {
344
+ try {
345
+ let count = 0;
346
+ for (const _ of db.getRange({})) {
347
+ count++;
348
+ }
349
+ return count;
350
+ }
351
+ catch (error) {
352
+ const message = error instanceof Error ? error.message : String(error);
353
+ throw new DatabaseOperationError(`Failed to count database entries: ${message}`);
354
+ }
355
+ }
356
+ /**
357
+ * Iterates over branch of db given by top key
358
+ *
359
+ * Returns iterator of (full key, val) tuples over a branch of the db given by top key
360
+ * where: full key is full database key for val not truncated top key.
361
+ *
362
+ * Works for both dupsort==False and dupsort==True
363
+ * Because cursor.iternext() advances cursor after returning item its safe
364
+ * to delete the item within the iteration loop.
365
+ *
366
+ * @param db - Named sub-database
367
+ * @param top - Truncated top key, a key space prefix to get all the items
368
+ * from multiple branches of the key space. If top key is
369
+ * empty then gets all items in database.
370
+ * Empty Uint8Array matches all keys (like str.startswith('') always returns True)
371
+ * @returns Generator yielding (key, val) tuples
372
+ */
373
+ *getTopItemIter(db, top = new Uint8Array(0)) {
374
+ try {
375
+ // Use getRange with start position at top key
376
+ // With binary encoding, keys and values are always Uint8Array
377
+ const startKey = top.length > 0 ? top : undefined;
378
+ for (const entry of db.getRange({ start: startKey })) {
379
+ const keyBytes = entry.key;
380
+ const valBytes = entry.value;
381
+ // Check if key starts with top prefix
382
+ // If top is empty, match all keys (empty prefix matches everything)
383
+ if (top.length > 0 && !startsWith(keyBytes, top)) {
384
+ break; // Done - no more keys in this branch
385
+ }
386
+ yield [keyBytes, valBytes];
387
+ }
388
+ }
389
+ catch (error) {
390
+ // If iteration fails, return empty generator
391
+ console.warn(`Error iterating database: ${error}`);
392
+ }
393
+ }
394
+ }
395
+ // Class constants
396
+ Object.defineProperty(LMDBer, "HeadDirPath", {
397
+ enumerable: true,
398
+ configurable: true,
399
+ writable: true,
400
+ value: "/usr/local/var"
401
+ });
402
+ Object.defineProperty(LMDBer, "TailDirPath", {
403
+ enumerable: true,
404
+ configurable: true,
405
+ writable: true,
406
+ value: "keri/db"
407
+ });
408
+ Object.defineProperty(LMDBer, "CleanTailDirPath", {
409
+ enumerable: true,
410
+ configurable: true,
411
+ writable: true,
412
+ value: "keri/clean/db"
413
+ });
414
+ Object.defineProperty(LMDBer, "AltHeadDirPath", {
415
+ enumerable: true,
416
+ configurable: true,
417
+ writable: true,
418
+ value: "~"
419
+ });
420
+ Object.defineProperty(LMDBer, "AltTailDirPath", {
421
+ enumerable: true,
422
+ configurable: true,
423
+ writable: true,
424
+ value: ".keri/db"
425
+ });
426
+ Object.defineProperty(LMDBer, "AltCleanTailDirPath", {
427
+ enumerable: true,
428
+ configurable: true,
429
+ writable: true,
430
+ value: ".keri/clean/db"
431
+ });
432
+ Object.defineProperty(LMDBer, "TempHeadDir", {
433
+ enumerable: true,
434
+ configurable: true,
435
+ writable: true,
436
+ value: "/tmp"
437
+ });
438
+ Object.defineProperty(LMDBer, "TempPrefix", {
439
+ enumerable: true,
440
+ configurable: true,
441
+ writable: true,
442
+ value: "keri_lmdb_"
443
+ });
444
+ Object.defineProperty(LMDBer, "TempSuffix", {
445
+ enumerable: true,
446
+ configurable: true,
447
+ writable: true,
448
+ value: "_test"
449
+ });
450
+ Object.defineProperty(LMDBer, "Perm", {
451
+ enumerable: true,
452
+ configurable: true,
453
+ writable: true,
454
+ value: 0o1700
455
+ });
456
+ Object.defineProperty(LMDBer, "MaxNamedDBs", {
457
+ enumerable: true,
458
+ configurable: true,
459
+ writable: true,
460
+ value: 96
461
+ });
462
+ /**
463
+ * Create and open an LMDBer instance.
464
+ *
465
+ * Constructors cannot be async, so call this factory where an opened LMDBer is required.
466
+ */
467
+ export function* createLMDBer(options = {}, defaults) {
468
+ const lmdber = new LMDBer(options, defaults);
469
+ const opened = yield* lmdber.reopen(options);
470
+ if (!opened) {
471
+ throw new DatabaseNotOpenError("Failed to open LMDBer");
472
+ }
473
+ return lmdber;
474
+ }