@bodil/bdb 0.1.2 → 0.2.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/src/table.ts CHANGED
@@ -17,27 +17,130 @@ export type TableEvent<A extends object, PK> =
17
17
  | { type: "update"; item: A }
18
18
  | { type: "delete"; key: PK };
19
19
 
20
- export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
21
- extends Emitter<TableEvent<A, PI["keyType"]>>
22
- implements Disposable, Iterable<Readonly<A>>
20
+ /**
21
+ * A database table.
22
+ *
23
+ * To learn how to create a table, see the static method {@link Table.create}.
24
+ *
25
+ * @template Document The type of the documents this table stores.
26
+ */
27
+ export class Table<
28
+ Document extends object,
29
+ PrimaryIndex extends UnitIndex<Document>,
30
+ Indices extends object,
31
+ >
32
+ extends Emitter<TableEvent<Document, PrimaryIndex["keyType"]>>
33
+ implements Disposable, Iterable<Readonly<Document>>
23
34
  {
24
- ready: Promise<void> = Promise.resolve();
25
-
26
- readonly primaryIndex: PI;
27
- readonly primary = new BTree<PI["keyType"], Readonly<A>>();
28
- readonly indices: { [K in Exclude<keyof Ix & string, PI["name"]>]: Index<A> } = {} as any;
35
+ /**
36
+ * A promise which resolves when this table is ready to use.
37
+ *
38
+ * If the table isn't connected to a {@link StorageBackend}, this is a
39
+ * promise which resolves immediately, and you don't really need to await
40
+ * it. The table will be ready for use immediately.
41
+ *
42
+ * With an attached {@link StorageBackend}, this promise will resolve when
43
+ * the table has finished restoring its contents from the storage. You
44
+ * should not under any circumstances use the table before this is complete.
45
+ */
46
+ get ready(): Promise<void> {
47
+ return this.#ready;
48
+ }
49
+ #ready: Promise<void> = Promise.resolve();
50
+
51
+ /** @ignore */
52
+ readonly primaryIndex: PrimaryIndex;
53
+ /** @ignore */
54
+ readonly primary = new BTree<PrimaryIndex["keyType"], Readonly<Document>>();
55
+ /** @ignore */
56
+ readonly indices: {
57
+ [K in Exclude<keyof Indices & string, PrimaryIndex["name"]>]: Index<Document>;
58
+ } = {} as any;
59
+ /** @ignore */
29
60
  readonly indexTables: {
30
- [K in Exclude<keyof Ix & string, PI["name"]>]: BTree<Ix[K], Array<A>>;
61
+ [K in Exclude<keyof Indices & string, PrimaryIndex["name"]>]: BTree<
62
+ Indices[K],
63
+ Array<Document>
64
+ >;
31
65
  } = {} as any;
32
66
 
33
- readonly changed = Signal.from(0);
67
+ readonly #changed = Signal.from(null, { equals: () => false });
68
+ /**
69
+ * A signal which updates whenever the table has been modified.
70
+ */
71
+ get changed(): Signal.Computed<null> {
72
+ return this.#changed.readOnly;
73
+ }
34
74
 
35
- #storage?: TableStorage<A, PI>;
36
- readonly #signals = new BTree<PI["keyType"], WeakRef<Signal.State<Readonly<A> | undefined>>>();
75
+ #storage?: TableStorage<Document, PrimaryIndex>;
76
+ readonly #signals = new BTree<
77
+ PrimaryIndex["keyType"],
78
+ WeakRef<Signal.State<Readonly<Document> | undefined>>
79
+ >();
37
80
  readonly #signalCleanupRegistry = new FinalizationRegistry(this.#signalCleanup.bind(this));
38
81
  readonly #context = new DisposableContext();
39
82
 
40
- constructor(primaryIndex: PI) {
83
+ /**
84
+ * Create a database {@link Table}.
85
+ *
86
+ * `Table.create` is a function which takes one type argument, the type of the
87
+ * object (which we'll call a *document*) you want the table to store, and no
88
+ * value arguments.
89
+ *
90
+ * This returns an object with one method: `withPrimaryIndex`. This method is
91
+ * what actually creates the table. So, the full incantation is, for instance:
92
+ *
93
+ * ```ts
94
+ * type Document = { id: string; value: number };
95
+ * const table = Table.create<Document>()
96
+ * .withPrimaryIndex(index<Document>().key("id"));
97
+ * ```
98
+ *
99
+ * In order to look up something in a database table, you need an index. You can
100
+ * create an index using the {@link index} function, which, like `Table.create`,
101
+ * takes the document you're creating an index for as its type argument, and
102
+ * returns a selection of index constructors, of which the most straightforward
103
+ * one is {@link IndexConstructor.key}. This creates an index for a single named
104
+ * property of the document containing a primitive comparable value (a string, a
105
+ * number, or a bigint), and allows you to search for documents where the given
106
+ * property matches any given value. You can also create an index over multiple
107
+ * keys using {@link IndexConstructor.keys} or over a key containing an array
108
+ * using {@link IndexConstructor.array}. You can even create a completely
109
+ * customised index using {@link IndexConstructor.custom}.
110
+ *
111
+ * The primary index, unlike a regular index, is a unique identifier, and only
112
+ * one document can exist at any given time under the key or keys represented by
113
+ * the primary index. A table is required to have a primary index, which is why
114
+ * `Table.create` doesn't actually create the table until you call
115
+ * `withPrimaryIndex` on it. You can then add as many extra indices as you like
116
+ * using `withIndex`. To extend our example above with an additional index over
117
+ * the `value` property:
118
+ *
119
+ * ```ts
120
+ * type Document = { id: string; value: number };
121
+ * const table = Table.create<Document>()
122
+ * .withPrimaryIndex(index<Document>().key("id"))
123
+ * .withIndex(index<Document>().key("value"));
124
+ * ```
125
+ *
126
+ * @see {@link index}
127
+ *
128
+ * @template Document The type of the document this table stores.
129
+ */
130
+ static create<Document extends object>(): {
131
+ withPrimaryIndex: <PrimaryIndex extends UnitIndex<Document>>(
132
+ primaryIndex: PrimaryIndex,
133
+ ) => Table<Document, PrimaryIndex, PrimaryIndex["record"]>;
134
+ } {
135
+ return {
136
+ withPrimaryIndex(primaryIndex) {
137
+ return new Table(primaryIndex);
138
+ },
139
+ };
140
+ }
141
+
142
+ /** @internal */
143
+ private constructor(primaryIndex: PrimaryIndex) {
41
144
  super();
42
145
  this.primaryIndex = primaryIndex;
43
146
  }
@@ -47,9 +150,22 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
47
150
  this.#context.dispose();
48
151
  }
49
152
 
50
- withIndex<I extends Index<A>>(
153
+ /**
154
+ * Add an index to a table.
155
+ *
156
+ * @example
157
+ * type Document = { id: string; value: number };
158
+ * const table = Table.create<Document>()
159
+ * .withPrimaryIndex(index<Document>().key("id"))
160
+ * .withIndex(index<Document>().key("value"));
161
+ */
162
+ withIndex<I extends Index<Document>>(
51
163
  index: I,
52
- ): Table<A, PI, { [K in keyof (Ix & I["record"])]: (Ix & I["record"])[K] }> {
164
+ ): Table<
165
+ Document,
166
+ PrimaryIndex,
167
+ { [K in keyof (Indices & I["record"])]: (Indices & I["record"])[K] }
168
+ > {
53
169
  assert(
54
170
  (this.indices as any)[index.name] === undefined,
55
171
  `duplicate index definition: "${index.name}"`,
@@ -57,14 +173,23 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
57
173
  (this.indices as any)[index.name] = index;
58
174
  (this.indexTables as any)[index.name] =
59
175
  index instanceof CustomIndex ? index.makeIndex() : new BTree();
60
- return this as Table<A, PI, { [K in keyof (Ix & I["record"])]: (Ix & I["record"])[K] }>;
176
+ return this as Table<
177
+ Document,
178
+ PrimaryIndex,
179
+ { [K in keyof (Indices & I["record"])]: (Indices & I["record"])[K] }
180
+ >;
61
181
  }
62
182
 
183
+ /**
184
+ * Attach a table to a {@link StorageBackend}.
185
+ *
186
+ * See {@link IndexedDBBackend.open} for an example of how to use this.
187
+ */
63
188
  withStorage(backend: StorageBackend, name: string): this {
64
189
  this.#storage = new TableStorage(backend, name, this.primaryIndex);
65
- this.ready = this.#storage.ready;
190
+ this.#ready = this.#storage.ready;
66
191
  this.#context.use(
67
- this.#storage.on((event: TableEvent<A, PI["keyType"]>) => {
192
+ this.#storage.on((event: TableEvent<Document, PrimaryIndex["keyType"]>) => {
68
193
  switch (event.type) {
69
194
  case "update":
70
195
  this.add(event.item);
@@ -79,20 +204,20 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
79
204
  return this;
80
205
  }
81
206
 
82
- signal(primaryKey: PI["keyType"]): Signal.Computed<Readonly<A> | undefined> {
207
+ signal(primaryKey: PrimaryIndex["keyType"]): Signal.Computed<Readonly<Document> | undefined> {
83
208
  const active = this.#signals.get(primaryKey)?.deref();
84
209
  if (active !== undefined) {
85
- return active.readOnly();
210
+ return active.readOnly;
86
211
  }
87
212
  const sig = Signal.from(this.get(primaryKey), {
88
213
  equals: () => false,
89
214
  });
90
215
  this.#signalCleanupRegistry.register(sig, primaryKey);
91
216
  this.#signals.set(primaryKey, new WeakRef(sig));
92
- return sig.readOnly();
217
+ return sig.readOnly;
93
218
  }
94
219
 
95
- get(primaryKey: PI["keyType"]): Readonly<A> | undefined {
220
+ get(primaryKey: PrimaryIndex["keyType"]): Readonly<Document> | undefined {
96
221
  return this.primary.get(primaryKey);
97
222
  }
98
223
 
@@ -102,39 +227,56 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
102
227
  * value before passing it to the update function.
103
228
  */
104
229
  createAndUpdate(
105
- primaryKey: PI["keyType"],
106
- create: () => A,
107
- update: (item: Draft<A>) => Draft<A> | void,
108
- ): Readonly<A> {
230
+ primaryKey: PrimaryIndex["keyType"],
231
+ create: () => Document,
232
+ update: (item: Draft<Document>) => Draft<Document> | void,
233
+ ): Readonly<Document> {
109
234
  const oldItem = this.primary.get(primaryKey) ?? create();
110
235
  const item = produce(oldItem, update);
111
236
  return this.#setItem(item, oldItem, primaryKey);
112
237
  }
113
238
 
114
239
  /**
115
- * Given a primary key, apply an update function to the value stored under
116
- * that key. If there's no value, call the create function to create a new
117
- * value. In this case, the update function is not called.
240
+ * Create or update a document.
241
+ *
242
+ * Given a primary key, apply the `update` function to the document stored
243
+ * under that key.
244
+ *
245
+ * If there's no such document, call the `create` function to create a new
246
+ * document and insert that under the given primary key. In this case, the
247
+ * update function is not called.
248
+ *
249
+ * The `update` function is passed to {@link produce | Immer.produce} to
250
+ * perform the update. It should modify the provided document in place, and
251
+ * it's not necessary to return it.
118
252
  */
119
253
  createOrUpdate(
120
- primaryKey: PI["keyType"],
121
- create: () => A,
122
- update: (item: Draft<A>) => Draft<A> | void,
123
- ): Readonly<A> {
254
+ primaryKey: PrimaryIndex["keyType"],
255
+ create: () => Document,
256
+ update: (item: Draft<Document>) => Draft<Document> | void,
257
+ ): Readonly<Document> {
124
258
  const oldItem = this.primary.get(primaryKey);
125
259
  const item = oldItem === undefined ? create() : produce(oldItem, update);
126
260
  return this.#setItem(item, oldItem, primaryKey);
127
261
  }
128
262
 
129
263
  /**
130
- * Apply an update function to the value stored under the given primary key.
131
- * If no value exists, the update function is not called and `undefined` is
132
- * returned. Otherwise, the updated value is returned.
264
+ * Update a document.
265
+ *
266
+ * Apply the `update` function to the document stored under the given
267
+ * primary key.
268
+ *
269
+ * If no such document exists, the update function is not called and
270
+ * `undefined` is returned. Otherwise, the updated document is returned.
271
+ *
272
+ * The `update` function is passed to {@link produce | Immer.produce} to
273
+ * perform the update. It should modify the provided document in place, and
274
+ * it's not necessary to return it.
133
275
  */
134
276
  update(
135
- primaryKey: PI["keyType"],
136
- update: (item: Draft<A>) => void | Draft<A>,
137
- ): Readonly<A> | undefined {
277
+ primaryKey: PrimaryIndex["keyType"],
278
+ update: (item: Draft<Document>) => void | Draft<Document>,
279
+ ): Readonly<Document> | undefined {
138
280
  const oldItem = this.primary.get(primaryKey);
139
281
  if (oldItem === undefined) {
140
282
  return undefined;
@@ -144,63 +286,77 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
144
286
  }
145
287
 
146
288
  /**
147
- * Add items to the table, overwriting any previous values under their
289
+ * Add documents to the table, overwriting any previous values under their
148
290
  * primary keys.
149
291
  */
150
- add(...items: Array<A>): void;
151
- add(items: Iterable<A>): void;
152
- add(...items: Array<A | Iterable<A>>) {
292
+ add(...items: Array<Document>): void;
293
+ add(items: Iterable<Document>): void;
294
+ add(...items: Array<Document | Iterable<Document>>) {
153
295
  return this.#addFrom(
154
- items.length === 1 && isIterable(items[0]) ? items[0] : (items as Array<A>),
296
+ items.length === 1 && isIterable(items[0]) ? items[0] : (items as Array<Document>),
155
297
  );
156
298
  }
157
299
 
158
300
  /**
159
- * Delete items from the table by their primary keys.
301
+ * Delete documents from the table by their primary keys.
160
302
  */
161
- delete(...primaryKeys: Array<PI["keyType"]>): number;
162
- delete(primaryKeys: Iterable<PI["keyType"]>): number;
163
- delete(...items: Array<PI["keyType"] | Iterable<PI["keyType"]>>): number {
303
+ delete(...primaryKeys: Array<PrimaryIndex["keyType"]>): number;
304
+ delete(primaryKeys: Iterable<PrimaryIndex["keyType"]>): number;
305
+ delete(...items: Array<PrimaryIndex["keyType"] | Iterable<PrimaryIndex["keyType"]>>): number {
164
306
  return this.#deleteFrom(
165
- items.length === 1 && isIterable(items[0]) ? items[0] : (items as Array<A>),
307
+ items.length === 1 && isIterable(items[0]) ? items[0] : (items as Array<Document>),
166
308
  );
167
309
  }
168
310
 
169
311
  /**
170
- * Delete all data from the table.
312
+ * Delete all documents from the table.
171
313
  */
172
314
  clear() {
173
315
  this.delete(...this.primary.keys());
174
316
  }
175
317
 
176
- [Symbol.iterator](): IterableIterator<Readonly<A>> {
177
- return this.primary.values();
318
+ /**
319
+ * Iterate over the documents in the table, ordered by primary key.
320
+ */
321
+ [Symbol.iterator](): IteratorObject<Readonly<Document>> {
322
+ return Iterator.from(this.primary.values());
178
323
  }
179
324
 
180
- where<K extends keyof Ix & string, I extends Index<A> & Ix[K]>(
325
+ /**
326
+ * Perform an {@link IndexQuery} using the specified index.
327
+ */
328
+ where<K extends keyof Indices & string, I extends Index<Document> & Indices[K]>(
181
329
  index: K,
182
- ): IndexQuery<A, PI, I, Ix> {
330
+ ): IndexQuery<Document, PrimaryIndex, I, Indices> {
183
331
  return new IndexQuery(this, index, IteratorDirection.Ascending);
184
332
  }
185
333
 
186
- orderBy<K extends keyof Ix & string, I extends Index<A> & Ix[K]>(
334
+ /**
335
+ * Perform an {@link IndexQuery} using the specified index.
336
+ *
337
+ * This is an alias for {@link Table.where}.
338
+ */
339
+ orderBy<K extends keyof Indices & string, I extends Index<Document> & Indices[K]>(
187
340
  index: K,
188
- ): IndexQuery<A, PI, I, Ix> {
341
+ ): IndexQuery<Document, PrimaryIndex, I, Indices> {
189
342
  return this.where(index);
190
343
  }
191
344
 
345
+ /**
346
+ * Get the number of documents currently stored in the table.
347
+ */
192
348
  size(): number {
193
349
  return this.primary.size;
194
350
  }
195
351
 
196
- #addFrom(items: Iterable<A>) {
352
+ #addFrom(items: Iterable<Document>) {
197
353
  for (const item of items) {
198
354
  const key = this.primaryIndex.extractKey(item);
199
355
  this.#setItem(item, this.primary.get(key), key);
200
356
  }
201
357
  }
202
358
 
203
- #deleteFrom(primaryKeys: Iterable<PI["keyType"]>): number {
359
+ #deleteFrom(primaryKeys: Iterable<PrimaryIndex["keyType"]>): number {
204
360
  let deleted = 0;
205
361
  for (const primaryKey of primaryKeys) {
206
362
  const item = this.primary.get(primaryKey);
@@ -212,7 +368,11 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
212
368
  return deleted;
213
369
  }
214
370
 
215
- #setItem(item: A, oldItem: Readonly<A> | undefined, key: PI["keyType"]): A {
371
+ #setItem(
372
+ item: Document,
373
+ oldItem: Readonly<Document> | undefined,
374
+ key: PrimaryIndex["keyType"],
375
+ ): Document {
216
376
  freeze(item, true);
217
377
 
218
378
  // make sure the primary key matches the value
@@ -252,11 +412,11 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
252
412
  // emit an update event and update signals
253
413
  this.#withSignal(this.primaryIndex.extractKey(item), (sig) => sig.set(item));
254
414
  this.emit({ type: "update", item });
255
- this.changed.update((i) => i + 1);
415
+ this.#changed.set(null);
256
416
  return item;
257
417
  }
258
418
 
259
- #deleteItem(item: A, primaryKey: PI["keyType"]) {
419
+ #deleteItem(item: Document, primaryKey: PrimaryIndex["keyType"]) {
260
420
  // remove the item from indices
261
421
  this.#deleteFromIndices(item);
262
422
  // remove the item from the primary index
@@ -264,12 +424,12 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
264
424
  // emit a delete event and update signals
265
425
  this.#withSignal(primaryKey, (sig) => sig.set(undefined));
266
426
  this.emit({ type: "delete", key: primaryKey });
267
- this.changed.update((i) => i + 1);
427
+ this.#changed.set(null);
268
428
  }
269
429
 
270
430
  #withSignal(
271
- primaryKey: PI["keyType"],
272
- fn: (signal: Signal.State<Readonly<A> | undefined>) => void,
431
+ primaryKey: PrimaryIndex["keyType"],
432
+ fn: (signal: Signal.State<Readonly<Document> | undefined>) => void,
273
433
  ) {
274
434
  const sigRef = this.#signals.get(primaryKey);
275
435
  const sig = sigRef?.deref();
@@ -282,7 +442,7 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
282
442
  }
283
443
  }
284
444
 
285
- #deleteFromIndices(item: A) {
445
+ #deleteFromIndices(item: Document) {
286
446
  for (const property of Object.keys(this.indices)) {
287
447
  const index = (this.indices as any)[property];
288
448
  const table = (this.indexTables as any)[property];
@@ -303,7 +463,7 @@ export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
303
463
  }
304
464
  }
305
465
 
306
- #signalCleanup(primaryKey: PI["keyType"]) {
466
+ #signalCleanup(primaryKey: PrimaryIndex["keyType"]) {
307
467
  this.#signals.delete(primaryKey);
308
468
  }
309
469
  }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type IndexablePrimitive = string | number | null | undefined | bigint;
1
+ export type IndexablePrimitive = string | number | bigint;
2
2
  export type IndexableType =
3
3
  | IndexablePrimitive
4
4
  | Array<IndexablePrimitive>