@enbox/dwn-sql-store 0.0.1 → 0.0.3

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 (85) hide show
  1. package/README.md +36 -53
  2. package/dist/esm/src/data-store-sql.js +5 -5
  3. package/dist/esm/src/data-store-sql.js.map +1 -1
  4. package/dist/esm/src/dialect/bun-sqlite-adapter.js +46 -0
  5. package/dist/esm/src/dialect/bun-sqlite-adapter.js.map +1 -0
  6. package/dist/esm/src/dialect/mysql-dialect.js +1 -1
  7. package/dist/esm/src/dialect/mysql-dialect.js.map +1 -1
  8. package/dist/esm/src/dialect/postgres-dialect.js +1 -1
  9. package/dist/esm/src/dialect/postgres-dialect.js.map +1 -1
  10. package/dist/esm/src/dialect/sqlite-dialect.js +1 -1
  11. package/dist/esm/src/dialect/sqlite-dialect.js.map +1 -1
  12. package/dist/esm/src/main.js +3 -1
  13. package/dist/esm/src/main.js.map +1 -1
  14. package/dist/esm/src/message-store-sql.js +54 -25
  15. package/dist/esm/src/message-store-sql.js.map +1 -1
  16. package/dist/esm/src/resumable-task-store-sql.js +5 -6
  17. package/dist/esm/src/resumable-task-store-sql.js.map +1 -1
  18. package/dist/esm/src/smt-store-sql.js +151 -0
  19. package/dist/esm/src/smt-store-sql.js.map +1 -0
  20. package/dist/esm/src/state-index-sql.js +234 -0
  21. package/dist/esm/src/state-index-sql.js.map +1 -0
  22. package/dist/esm/src/utils/filter.js +3 -3
  23. package/dist/esm/src/utils/filter.js.map +1 -1
  24. package/dist/esm/src/utils/sanitize.js +7 -8
  25. package/dist/esm/src/utils/sanitize.js.map +1 -1
  26. package/dist/esm/src/utils/tags.js +3 -6
  27. package/dist/esm/src/utils/tags.js.map +1 -1
  28. package/dist/esm/src/utils/transaction.js +3 -21
  29. package/dist/esm/src/utils/transaction.js.map +1 -1
  30. package/dist/types/src/data-store-sql.d.ts +3 -4
  31. package/dist/types/src/data-store-sql.d.ts.map +1 -1
  32. package/dist/types/src/dialect/bun-sqlite-adapter.d.ts +33 -0
  33. package/dist/types/src/dialect/bun-sqlite-adapter.d.ts.map +1 -0
  34. package/dist/types/src/dialect/dialect.d.ts +1 -2
  35. package/dist/types/src/dialect/dialect.d.ts.map +1 -1
  36. package/dist/types/src/dialect/mysql-dialect.d.ts +3 -2
  37. package/dist/types/src/dialect/mysql-dialect.d.ts.map +1 -1
  38. package/dist/types/src/dialect/postgres-dialect.d.ts +3 -2
  39. package/dist/types/src/dialect/postgres-dialect.d.ts.map +1 -1
  40. package/dist/types/src/dialect/sqlite-dialect.d.ts +3 -2
  41. package/dist/types/src/dialect/sqlite-dialect.d.ts.map +1 -1
  42. package/dist/types/src/main.d.ts +3 -1
  43. package/dist/types/src/main.d.ts.map +1 -1
  44. package/dist/types/src/message-store-sql.d.ts +4 -3
  45. package/dist/types/src/message-store-sql.d.ts.map +1 -1
  46. package/dist/types/src/resumable-task-store-sql.d.ts +2 -2
  47. package/dist/types/src/resumable-task-store-sql.d.ts.map +1 -1
  48. package/dist/types/src/smt-store-sql.d.ts +37 -0
  49. package/dist/types/src/smt-store-sql.d.ts.map +1 -0
  50. package/dist/types/src/state-index-sql.d.ts +44 -0
  51. package/dist/types/src/state-index-sql.d.ts.map +1 -0
  52. package/dist/types/src/types.d.ts +24 -42
  53. package/dist/types/src/types.d.ts.map +1 -1
  54. package/dist/types/src/utils/filter.d.ts +3 -3
  55. package/dist/types/src/utils/filter.d.ts.map +1 -1
  56. package/dist/types/src/utils/sanitize.d.ts +2 -2
  57. package/dist/types/src/utils/sanitize.d.ts.map +1 -1
  58. package/dist/types/src/utils/tags.d.ts +3 -5
  59. package/dist/types/src/utils/tags.d.ts.map +1 -1
  60. package/dist/types/src/utils/transaction.d.ts +4 -4
  61. package/dist/types/src/utils/transaction.d.ts.map +1 -1
  62. package/package.json +19 -31
  63. package/src/data-store-sql.ts +11 -9
  64. package/src/dialect/bun-sqlite-adapter.ts +82 -0
  65. package/src/dialect/dialect.ts +4 -5
  66. package/src/dialect/mysql-dialect.ts +8 -6
  67. package/src/dialect/postgres-dialect.ts +11 -6
  68. package/src/dialect/sqlite-dialect.ts +11 -6
  69. package/src/main.ts +4 -2
  70. package/src/message-store-sql.ts +90 -45
  71. package/src/resumable-task-store-sql.ts +9 -7
  72. package/src/smt-store-sql.ts +206 -0
  73. package/src/state-index-sql.ts +283 -0
  74. package/src/types.ts +32 -47
  75. package/src/utils/filter.ts +8 -6
  76. package/src/utils/sanitize.ts +19 -20
  77. package/src/utils/tags.ts +6 -7
  78. package/src/utils/transaction.ts +7 -23
  79. package/dist/cjs/main.js +0 -3784
  80. package/dist/cjs/package.json +0 -1
  81. package/dist/esm/src/event-log-sql.js +0 -169
  82. package/dist/esm/src/event-log-sql.js.map +0 -1
  83. package/dist/types/src/event-log-sql.d.ts +0 -24
  84. package/dist/types/src/event-log-sql.d.ts.map +0 -1
  85. package/src/event-log-sql.ts +0 -227
@@ -0,0 +1,206 @@
1
+ /**
2
+ * SQL-backed implementation of SMTNodeStore.
3
+ *
4
+ * Storage layout:
5
+ * - Nodes are stored in the `stateIndexNodes` table, keyed by hex-encoded hash
6
+ * - The root hash is stored in the `stateIndexRoots` table
7
+ * - Node values are serialized with Uint8Array fields encoded as hex strings
8
+ *
9
+ * Each SMTStoreSql instance represents a single tree (identified by a tenant + scope key).
10
+ * Multiple instances share the same underlying Kysely database connection.
11
+ */
12
+
13
+ import type { DwnDatabaseType } from './types.js';
14
+ import type { Kysely } from 'kysely';
15
+
16
+ import type { Hash, SMTInternalNode, SMTLeafNode, SMTNode, SMTNodeStore } from '@enbox/dwn-sdk-js';
17
+
18
+ import { hashToHex, hexToHash } from '@enbox/dwn-sdk-js';
19
+
20
+ export type SMTStoreSqlParams = {
21
+ /** The shared Kysely database instance. */
22
+ db: Kysely<DwnDatabaseType>;
23
+ /** The tenant DID that owns this tree. */
24
+ tenant: string;
25
+ /** The scope key for this tree (e.g. '' for global, protocol URI for protocol-scoped). */
26
+ scope: string;
27
+ };
28
+
29
+ export class SMTStoreSql implements SMTNodeStore {
30
+ #db: Kysely<DwnDatabaseType>;
31
+ #tenant: string;
32
+ #scope: string;
33
+
34
+ constructor({ db, tenant, scope }: SMTStoreSqlParams) {
35
+ this.#db = db;
36
+ this.#tenant = tenant;
37
+ this.#scope = scope;
38
+ }
39
+
40
+ async open(): Promise<void> {
41
+ // No-op: the Kysely DB is already open and tables are created by StateIndexSql.
42
+ }
43
+
44
+ async close(): Promise<void> {
45
+ // No-op: the Kysely DB lifecycle is managed by StateIndexSql.
46
+ }
47
+
48
+ async clear(): Promise<void> {
49
+ await this.#db
50
+ .deleteFrom('stateIndexNodes')
51
+ .where('tenant', '=', this.#tenant)
52
+ .where('scope', '=', this.#scope)
53
+ .execute();
54
+
55
+ await this.#db
56
+ .deleteFrom('stateIndexRoots')
57
+ .where('tenant', '=', this.#tenant)
58
+ .where('scope', '=', this.#scope)
59
+ .execute();
60
+ }
61
+
62
+ async getNode(hash: Hash): Promise<SMTNode | undefined> {
63
+ const hexKey = hashToHex(hash);
64
+
65
+ const result = await this.#db
66
+ .selectFrom('stateIndexNodes')
67
+ .selectAll()
68
+ .where('tenant', '=', this.#tenant)
69
+ .where('scope', '=', this.#scope)
70
+ .where('nodeHash', '=', hexKey)
71
+ .executeTakeFirst();
72
+
73
+ if (!result) {
74
+ return undefined;
75
+ }
76
+
77
+ return this.deserializeNode(result);
78
+ }
79
+
80
+ async putNode(hash: Hash, node: SMTNode): Promise<void> {
81
+ const hexKey = hashToHex(hash);
82
+ const values = this.serializeNodeToRow(hexKey, node);
83
+
84
+ // Use INSERT ... ON CONFLICT REPLACE pattern.
85
+ // Since different SQL dialects handle upsert differently, we use
86
+ // delete-then-insert as a portable approach.
87
+ await this.#db
88
+ .deleteFrom('stateIndexNodes')
89
+ .where('tenant', '=', this.#tenant)
90
+ .where('scope', '=', this.#scope)
91
+ .where('nodeHash', '=', hexKey)
92
+ .execute();
93
+
94
+ await this.#db
95
+ .insertInto('stateIndexNodes')
96
+ .values(values)
97
+ .execute();
98
+ }
99
+
100
+ async deleteNode(hash: Hash): Promise<void> {
101
+ const hexKey = hashToHex(hash);
102
+
103
+ await this.#db
104
+ .deleteFrom('stateIndexNodes')
105
+ .where('tenant', '=', this.#tenant)
106
+ .where('scope', '=', this.#scope)
107
+ .where('nodeHash', '=', hexKey)
108
+ .execute();
109
+ }
110
+
111
+ async getRoot(): Promise<Hash | undefined> {
112
+ const result = await this.#db
113
+ .selectFrom('stateIndexRoots')
114
+ .select('rootHash')
115
+ .where('tenant', '=', this.#tenant)
116
+ .where('scope', '=', this.#scope)
117
+ .executeTakeFirst();
118
+
119
+ if (!result) {
120
+ return undefined;
121
+ }
122
+
123
+ return hexToHash(result.rootHash);
124
+ }
125
+
126
+ async setRoot(hash: Hash): Promise<void> {
127
+ const hexRoot = hashToHex(hash);
128
+
129
+ // Delete-then-insert as a portable upsert.
130
+ await this.#db
131
+ .deleteFrom('stateIndexRoots')
132
+ .where('tenant', '=', this.#tenant)
133
+ .where('scope', '=', this.#scope)
134
+ .execute();
135
+
136
+ await this.#db
137
+ .insertInto('stateIndexRoots')
138
+ .values({
139
+ tenant : this.#tenant,
140
+ scope : this.#scope,
141
+ rootHash : hexRoot,
142
+ })
143
+ .execute();
144
+ }
145
+
146
+ // ─── Serialization helpers ──────────────────────────────────────────────
147
+
148
+ private serializeNodeToRow(hexKey: string, node: SMTNode): {
149
+ tenant: string;
150
+ scope: string;
151
+ nodeHash: string;
152
+ nodeType: string;
153
+ leftHash: string | null;
154
+ rightHash: string | null;
155
+ leafKeyHash: string | null;
156
+ leafValueCid: string | null;
157
+ } {
158
+ if (node.type === 'internal') {
159
+ return {
160
+ tenant : this.#tenant,
161
+ scope : this.#scope,
162
+ nodeHash : hexKey,
163
+ nodeType : 'internal',
164
+ leftHash : hashToHex(node.leftHash),
165
+ rightHash : hashToHex(node.rightHash),
166
+ leafKeyHash : null,
167
+ leafValueCid : null,
168
+ };
169
+ }
170
+
171
+ return {
172
+ tenant : this.#tenant,
173
+ scope : this.#scope,
174
+ nodeHash : hexKey,
175
+ nodeType : 'leaf',
176
+ leftHash : null,
177
+ rightHash : null,
178
+ leafKeyHash : hashToHex(node.keyHash),
179
+ leafValueCid : node.valueCid,
180
+ };
181
+ }
182
+
183
+ private deserializeNode(row: {
184
+ nodeType: string;
185
+ leftHash: string | null;
186
+ rightHash: string | null;
187
+ leafKeyHash: string | null;
188
+ leafValueCid: string | null;
189
+ }): SMTNode {
190
+ if (row.nodeType === 'internal') {
191
+ const node: SMTInternalNode = {
192
+ type : 'internal',
193
+ leftHash : hexToHash(row.leftHash!),
194
+ rightHash : hexToHash(row.rightHash!),
195
+ };
196
+ return node;
197
+ }
198
+
199
+ const node: SMTLeafNode = {
200
+ type : 'leaf',
201
+ keyHash : hexToHash(row.leafKeyHash!),
202
+ valueCid : row.leafValueCid!,
203
+ };
204
+ return node;
205
+ }
206
+ }
@@ -0,0 +1,283 @@
1
+ /**
2
+ * SQL-backed implementation of the StateIndex interface.
3
+ *
4
+ * Manages per-tenant Sparse Merkle Trees (global + per-protocol sub-trees) backed by SQL tables.
5
+ *
6
+ * Tables:
7
+ * - `stateIndexNodes`: stores SMT nodes (internal + leaf), keyed by (tenant, scope, nodeHash)
8
+ * - `stateIndexRoots`: stores the current root hash per (tenant, scope)
9
+ * - `stateIndexMeta`: reverse lookup from messageCid → protocol (for deletion)
10
+ */
11
+
12
+ import type { Dialect } from './dialect/dialect.js';
13
+ import type { DwnDatabaseType } from './types.js';
14
+ import type { Hash } from '@enbox/dwn-sdk-js';
15
+ import type { KeyValues } from '@enbox/dwn-sdk-js';
16
+ import type { StateIndex } from '@enbox/dwn-sdk-js';
17
+
18
+ import { initDefaultHashes } from '@enbox/dwn-sdk-js';
19
+ import { Kysely } from 'kysely';
20
+ import { SMTStoreSql } from './smt-store-sql.js';
21
+ import { SparseMerkleTree } from '@enbox/dwn-sdk-js';
22
+
23
+ export class StateIndexSql implements StateIndex {
24
+ #dialect: Dialect;
25
+ #db: Kysely<DwnDatabaseType> | null = null;
26
+
27
+ /**
28
+ * Cache of per-tenant global SMTs. Lazily populated on first access.
29
+ * Stores promises to avoid race conditions when multiple concurrent operations
30
+ * trigger lazy initialization for the same tenant.
31
+ */
32
+ #globalTrees: Map<string, Promise<SparseMerkleTree>> = new Map();
33
+
34
+ /**
35
+ * Cache of per-tenant, per-protocol SMTs. Key format: `{tenant}\x00{protocol}`.
36
+ * Stores promises to avoid race conditions.
37
+ */
38
+ #protocolTrees: Map<string, Promise<SparseMerkleTree>> = new Map();
39
+
40
+ constructor(dialect: Dialect) {
41
+ this.#dialect = dialect;
42
+ }
43
+
44
+ async open(): Promise<void> {
45
+ if (this.#db) {
46
+ return;
47
+ }
48
+
49
+ this.#db = new Kysely<DwnDatabaseType>({ dialect: this.#dialect });
50
+
51
+ // Ensure default hashes are initialized for the SMT
52
+ await initDefaultHashes();
53
+
54
+ // ─── Create stateIndexNodes table ─────────────────────────────────────
55
+ const nodesTableName = 'stateIndexNodes';
56
+ const nodesTableExists = await this.#dialect.hasTable(this.#db, nodesTableName);
57
+ if (!nodesTableExists) {
58
+ await this.#db.schema
59
+ .createTable(nodesTableName)
60
+ .ifNotExists()
61
+ .addColumn('tenant', 'varchar(255)', (col) => col.notNull())
62
+ .addColumn('scope', 'varchar(200)', (col) => col.notNull())
63
+ .addColumn('nodeHash', 'varchar(64)', (col) => col.notNull())
64
+ .addColumn('nodeType', 'varchar(10)', (col) => col.notNull())
65
+ .addColumn('leftHash', 'varchar(64)')
66
+ .addColumn('rightHash', 'varchar(64)')
67
+ .addColumn('leafKeyHash', 'varchar(64)')
68
+ .addColumn('leafValueCid', 'varchar(60)')
69
+ .execute();
70
+
71
+ // Not UNIQUE because the delete-then-insert upsert pattern in SMTStoreSql
72
+ // can race under concurrent access, causing duplicate key violations.
73
+ await this.#db.schema
74
+ .createIndex('index_stateIndexNodes_tenant_scope_nodeHash')
75
+ .on(nodesTableName)
76
+ .columns(['tenant', 'scope', 'nodeHash'])
77
+ .execute();
78
+ }
79
+
80
+ // ─── Create stateIndexRoots table ─────────────────────────────────────
81
+ const rootsTableName = 'stateIndexRoots';
82
+ const rootsTableExists = await this.#dialect.hasTable(this.#db, rootsTableName);
83
+ if (!rootsTableExists) {
84
+ await this.#db.schema
85
+ .createTable(rootsTableName)
86
+ .ifNotExists()
87
+ .addColumn('tenant', 'varchar(255)', (col) => col.notNull())
88
+ .addColumn('scope', 'varchar(200)', (col) => col.notNull())
89
+ .addColumn('rootHash', 'varchar(64)', (col) => col.notNull())
90
+ .execute();
91
+
92
+ await this.#db.schema
93
+ .createIndex('index_stateIndexRoots_tenant_scope')
94
+ .on(rootsTableName)
95
+ .columns(['tenant', 'scope'])
96
+ .execute();
97
+ }
98
+
99
+ // ─── Create stateIndexMeta table ──────────────────────────────────────
100
+ const metaTableName = 'stateIndexMeta';
101
+ const metaTableExists = await this.#dialect.hasTable(this.#db, metaTableName);
102
+ if (!metaTableExists) {
103
+ await this.#db.schema
104
+ .createTable(metaTableName)
105
+ .ifNotExists()
106
+ .addColumn('tenant', 'varchar(255)', (col) => col.notNull())
107
+ .addColumn('messageCid', 'varchar(60)', (col) => col.notNull())
108
+ .addColumn('protocol', 'varchar(200)')
109
+ .execute();
110
+
111
+ await this.#db.schema
112
+ .createIndex('index_stateIndexMeta_tenant_messageCid')
113
+ .on(metaTableName)
114
+ .columns(['tenant', 'messageCid'])
115
+ .execute();
116
+ }
117
+ }
118
+
119
+ async close(): Promise<void> {
120
+ this.#globalTrees.clear();
121
+ this.#protocolTrees.clear();
122
+ await this.#db?.destroy();
123
+ this.#db = null;
124
+ }
125
+
126
+ async clear(): Promise<void> {
127
+ if (!this.#db) {
128
+ throw new Error('Connection to database not open. Call `open` before using `clear`.');
129
+ }
130
+
131
+ this.#globalTrees.clear();
132
+ this.#protocolTrees.clear();
133
+
134
+ await this.#db.deleteFrom('stateIndexNodes').execute();
135
+ await this.#db.deleteFrom('stateIndexRoots').execute();
136
+ await this.#db.deleteFrom('stateIndexMeta').execute();
137
+ }
138
+
139
+ async insert(tenant: string, messageCid: string, indexes: KeyValues): Promise<void> {
140
+ if (!this.#db) {
141
+ throw new Error('Connection to database not open. Call `open` before using `insert`.');
142
+ }
143
+
144
+ // Insert into the global tree
145
+ const globalSmt = await this.getGlobalTree(tenant);
146
+ await globalSmt.insert(messageCid);
147
+
148
+ // If the message is associated with a protocol, insert into the protocol-scoped tree
149
+ const protocol = indexes.protocol as string | undefined;
150
+ if (protocol !== undefined) {
151
+ const protoSmt = await this.getProtocolTree(tenant, protocol);
152
+ await protoSmt.insert(messageCid);
153
+ }
154
+
155
+ // Store reverse lookup metadata for deletion
156
+ await this.#db
157
+ .insertInto('stateIndexMeta')
158
+ .values({
159
+ tenant,
160
+ messageCid,
161
+ protocol: protocol ?? null,
162
+ })
163
+ .execute();
164
+ }
165
+
166
+ async delete(tenant: string, messageCids: string[]): Promise<void> {
167
+ if (!this.#db) {
168
+ throw new Error('Connection to database not open. Call `open` before using `delete`.');
169
+ }
170
+
171
+ if (messageCids.length === 0) {
172
+ return;
173
+ }
174
+
175
+ const globalSmt = await this.getGlobalTree(tenant);
176
+
177
+ for (const messageCid of messageCids) {
178
+ // Look up stored metadata to find the protocol
179
+ const meta = await this.#db
180
+ .selectFrom('stateIndexMeta')
181
+ .select('protocol')
182
+ .where('tenant', '=', tenant)
183
+ .where('messageCid', '=', messageCid)
184
+ .executeTakeFirst();
185
+
186
+ // Delete from global tree
187
+ await globalSmt.delete(messageCid);
188
+
189
+ // Delete from protocol tree if applicable
190
+ if (meta?.protocol) {
191
+ const protoSmt = await this.getProtocolTree(tenant, meta.protocol);
192
+ await protoSmt.delete(messageCid);
193
+ }
194
+
195
+ // Remove the reverse lookup
196
+ await this.#db
197
+ .deleteFrom('stateIndexMeta')
198
+ .where('tenant', '=', tenant)
199
+ .where('messageCid', '=', messageCid)
200
+ .execute();
201
+ }
202
+ }
203
+
204
+ async getRoot(tenant: string): Promise<Hash> {
205
+ const smt = await this.getGlobalTree(tenant);
206
+ return smt.getRoot();
207
+ }
208
+
209
+ async getProtocolRoot(tenant: string, protocol: string): Promise<Hash> {
210
+ const smt = await this.getProtocolTree(tenant, protocol);
211
+ return smt.getRoot();
212
+ }
213
+
214
+ async getSubtreeHash(tenant: string, prefix: boolean[]): Promise<Hash> {
215
+ const smt = await this.getGlobalTree(tenant);
216
+ return smt.getSubtreeHash(prefix);
217
+ }
218
+
219
+ async getProtocolSubtreeHash(tenant: string, protocol: string, prefix: boolean[]): Promise<Hash> {
220
+ const smt = await this.getProtocolTree(tenant, protocol);
221
+ return smt.getSubtreeHash(prefix);
222
+ }
223
+
224
+ async getLeaves(tenant: string, prefix: boolean[]): Promise<string[]> {
225
+ const smt = await this.getGlobalTree(tenant);
226
+ return smt.getLeaves(prefix);
227
+ }
228
+
229
+ async getProtocolLeaves(tenant: string, protocol: string, prefix: boolean[]): Promise<string[]> {
230
+ const smt = await this.getProtocolTree(tenant, protocol);
231
+ return smt.getLeaves(prefix);
232
+ }
233
+
234
+ // ─── Private helpers ────────────────────────────────────────────────────
235
+
236
+ /**
237
+ * Get or create the global SMT for a tenant.
238
+ * Uses a promise-based cache to prevent concurrent callers from racing.
239
+ */
240
+ private getGlobalTree(tenant: string): Promise<SparseMerkleTree> {
241
+ let smtPromise = this.#globalTrees.get(tenant);
242
+ if (smtPromise !== undefined) {
243
+ return smtPromise;
244
+ }
245
+
246
+ smtPromise = this.createTree(tenant, '');
247
+ this.#globalTrees.set(tenant, smtPromise);
248
+ return smtPromise;
249
+ }
250
+
251
+ /**
252
+ * Get or create a protocol-scoped SMT for a tenant.
253
+ * Uses a promise-based cache to prevent concurrent callers from racing.
254
+ */
255
+ private getProtocolTree(tenant: string, protocol: string): Promise<SparseMerkleTree> {
256
+ const cacheKey = `${tenant}\x00${protocol}`;
257
+ let smtPromise = this.#protocolTrees.get(cacheKey);
258
+ if (smtPromise !== undefined) {
259
+ return smtPromise;
260
+ }
261
+
262
+ smtPromise = this.createTree(tenant, protocol);
263
+ this.#protocolTrees.set(cacheKey, smtPromise);
264
+ return smtPromise;
265
+ }
266
+
267
+ /**
268
+ * Create and initialize a new SparseMerkleTree backed by SQL via SMTStoreSql.
269
+ */
270
+ private async createTree(tenant: string, scope: string): Promise<SparseMerkleTree> {
271
+ const store = new SMTStoreSql({
272
+ db: this.#db!,
273
+ tenant,
274
+ scope,
275
+ });
276
+ const smt = new SparseMerkleTree(store);
277
+ await smt.initialize();
278
+ return smt;
279
+ }
280
+
281
+ }
282
+
283
+
package/src/types.ts CHANGED
@@ -1,39 +1,7 @@
1
1
  import type { Generated } from 'kysely';
2
+ import type { KeyValues } from '@enbox/dwn-sdk-js';
2
3
 
3
- export type KeyValues = { [key:string]: string | number | boolean | string[] | number[] };
4
-
5
- type EventLogTable = {
6
- watermark: Generated<number>;
7
- tenant: string;
8
- messageCid: string;
9
-
10
- // "indexes" start
11
- interface: string | null;
12
- method: string | null;
13
- schema: string | null;
14
- dataCid: string | null;
15
- dataSize: number | null;
16
- dateCreated: string | null;
17
- messageTimestamp: string | null;
18
- dataFormat: string | null;
19
- isLatestBaseState: boolean | null;
20
- published: boolean | null;
21
- author: string | null;
22
- recordId: string | null;
23
- entryId: string | null;
24
- datePublished: string | null;
25
- latest: string | null;
26
- protocol: string | null;
27
- permissionsRequestId: string | null;
28
- attester: string | null;
29
- protocolPath: string | null;
30
- recipient: string | null;
31
- contextId: string | null;
32
- parentId: string | null;
33
- permissionGrantId: string | null;
34
- prune: boolean | null;
35
- // "indexes" end
36
- }
4
+ export type { KeyValues };
37
5
 
38
6
  type MessageStoreTable = {
39
7
  id: Generated<number>;
@@ -57,7 +25,6 @@ type MessageStoreTable = {
57
25
  entryId: string | null;
58
26
  datePublished: string | null;
59
27
  protocol: string | null;
60
- permissionsRequestId: string | null;
61
28
  attester: string | null;
62
29
  protocolPath: string | null;
63
30
  recipient: string | null;
@@ -66,7 +33,7 @@ type MessageStoreTable = {
66
33
  permissionGrantId: string | null;
67
34
  prune: boolean | null;
68
35
  // "indexes" end
69
- }
36
+ };
70
37
 
71
38
  type MessageStoreRecordsTagsTable = {
72
39
  id: Generated<number>;
@@ -76,12 +43,29 @@ type MessageStoreRecordsTagsTable = {
76
43
  valueNumber: number | null;
77
44
  };
78
45
 
79
- type EventLogRecordsTagsTable = {
80
- id: Generated<number>;
81
- tag: string;
82
- eventWatermark: number;
83
- valueString: string | null;
84
- valueNumber: number | null;
46
+ // ─── StateIndex SMT tables ────────────────────────────────────────────────
47
+
48
+ type StateIndexNodeTable = {
49
+ tenant: string;
50
+ scope: string;
51
+ nodeHash: string;
52
+ nodeType: string;
53
+ leftHash: string | null;
54
+ rightHash: string | null;
55
+ leafKeyHash: string | null;
56
+ leafValueCid: string | null;
57
+ };
58
+
59
+ type StateIndexRootTable = {
60
+ tenant: string;
61
+ scope: string;
62
+ rootHash: string;
63
+ };
64
+
65
+ type StateIndexMetaTable = {
66
+ tenant: string;
67
+ messageCid: string;
68
+ protocol: string | null;
85
69
  };
86
70
 
87
71
  type DataStoreTable = {
@@ -90,20 +74,21 @@ type DataStoreTable = {
90
74
  recordId: string;
91
75
  dataCid: string;
92
76
  data: Uint8Array;
93
- }
77
+ };
94
78
 
95
79
  type ResumableTaskTable = {
96
80
  id: string;
97
81
  task: string;
98
82
  timeout: number;
99
83
  retryCount: number;
100
- }
84
+ };
101
85
 
102
86
  export type DwnDatabaseType = {
103
- eventLogMessages: EventLogTable;
104
- eventLogRecordsTags: EventLogRecordsTagsTable;
105
87
  messageStoreMessages: MessageStoreTable;
106
88
  messageStoreRecordsTags: MessageStoreRecordsTagsTable;
107
89
  dataStore: DataStoreTable;
108
90
  resumableTasks: ResumableTaskTable;
109
- }
91
+ stateIndexNodes: StateIndexNodeTable;
92
+ stateIndexRoots: StateIndexRootTable;
93
+ stateIndexMeta: StateIndexMetaTable;
94
+ };
@@ -1,7 +1,9 @@
1
- import { Filter } from '@enbox/dwn-sdk-js';
2
- import { DynamicModule, ExpressionBuilder, OperandExpression, SelectQueryBuilder, SqlBool } from 'kysely';
3
- import { sanitizeFiltersAndSeparateTags, sanitizedValue } from './sanitize.js';
4
- import { DwnDatabaseType } from '../types.js';
1
+ import type { DwnDatabaseType } from '../types.js';
2
+ import type { Filter } from '@enbox/dwn-sdk-js';
3
+ import type { ExpressionBuilder, OperandExpression, SelectQueryBuilder, SqlBool } from 'kysely';
4
+
5
+ import { DynamicModule } from 'kysely';
6
+ import { sanitizedValue, sanitizeFiltersAndSeparateTags } from './sanitize.js';
5
7
 
6
8
  /**
7
9
  * Takes multiple Filters and returns a single query.
@@ -44,7 +46,7 @@ function processFilter<DB = DwnDatabaseType, TB extends keyof DB = keyof DB>(
44
46
  andOperands: OperandExpression<SqlBool>[],
45
47
  filter: Filter
46
48
  ): void {
47
- for (let property in filter) {
49
+ for (const property in filter) {
48
50
  const value = filter[property];
49
51
  const column = new DynamicModule().ref(property);
50
52
  if (Array.isArray(value)) { // OneOfFilter
@@ -87,7 +89,7 @@ function processTags<DB = DwnDatabaseType, TB extends keyof DB = keyof DB>(
87
89
  const valueString = new DynamicModule().ref('valueString');
88
90
 
89
91
  // process each tag and add it to the andOperands from the rest of the filters
90
- for (let property in tags) {
92
+ for (const property in tags) {
91
93
  andOperands.push(eb(tagColumn, '=', property));
92
94
  const value = tags[property];
93
95
  if (Array.isArray(value)) { // OneOfFilter