@bodil/bdb 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.
- package/LICENCE.md +288 -0
- package/dist/backend.d.ts +34 -0
- package/dist/backend.js +97 -0
- package/dist/backend.js.map +1 -0
- package/dist/broadcast.d.ts +9 -0
- package/dist/broadcast.js +20 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.js +141 -0
- package/dist/index.test.js.map +1 -0
- package/dist/indices.d.ts +56 -0
- package/dist/indices.js +56 -0
- package/dist/indices.js.map +1 -0
- package/dist/query.d.ts +49 -0
- package/dist/query.js +148 -0
- package/dist/query.js.map +1 -0
- package/dist/serial.d.ts +6 -0
- package/dist/serial.js +92 -0
- package/dist/serial.js.map +1 -0
- package/dist/storage.d.ts +39 -0
- package/dist/storage.js +69 -0
- package/dist/storage.js.map +1 -0
- package/dist/table.d.ts +75 -0
- package/dist/table.js +223 -0
- package/dist/table.js.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +86 -0
- package/src/backend.ts +137 -0
- package/src/broadcast.ts +27 -0
- package/src/index.test.ts +200 -0
- package/src/index.ts +76 -0
- package/src/indices.ts +108 -0
- package/src/query.ts +233 -0
- package/src/serial.ts +92 -0
- package/src/storage.ts +83 -0
- package/src/table.ts +302 -0
- package/src/types.ts +16 -0
package/src/serial.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const primitives = new Set(["string", "number", "boolean", "undefined"]);
|
|
2
|
+
|
|
3
|
+
export type Serialised = { type?: string; value?: unknown } | Array<Serialised>;
|
|
4
|
+
|
|
5
|
+
export function serialise(value: unknown): Serialised {
|
|
6
|
+
if (primitives.has(typeof value)) {
|
|
7
|
+
return { value: value as any };
|
|
8
|
+
} else if (typeof value === "symbol") {
|
|
9
|
+
throw new TypeError(`don't know how to serialise a symbol: ${value.toString()}`);
|
|
10
|
+
} else if (typeof value === "bigint") {
|
|
11
|
+
return { type: "bigint", value: value.toString() };
|
|
12
|
+
} else if (value === null) {
|
|
13
|
+
return { type: "null" };
|
|
14
|
+
} else if (Array.isArray(value)) {
|
|
15
|
+
return value.map(serialise);
|
|
16
|
+
} else if (value instanceof Set) {
|
|
17
|
+
return { type: "set", value: value.keys().map(serialise).toArray() };
|
|
18
|
+
} else if (value instanceof Map) {
|
|
19
|
+
return {
|
|
20
|
+
type: "map",
|
|
21
|
+
value: value
|
|
22
|
+
.entries()
|
|
23
|
+
.map(([k, v]) => [serialise(k), serialise(v)])
|
|
24
|
+
.toArray(),
|
|
25
|
+
};
|
|
26
|
+
} else if (value instanceof Temporal.Instant) {
|
|
27
|
+
return { type: "instant", value: value.toJSON() };
|
|
28
|
+
} else if (value instanceof Temporal.Duration) {
|
|
29
|
+
return { type: "duration", value: value.toJSON() };
|
|
30
|
+
} else if (value instanceof Temporal.PlainDate) {
|
|
31
|
+
return { type: "plaindate", value: value.toJSON() };
|
|
32
|
+
} else if (value instanceof Temporal.ZonedDateTime) {
|
|
33
|
+
return { type: "zdt", value: value.toJSON() };
|
|
34
|
+
} else if (value instanceof Date) {
|
|
35
|
+
throw new TypeError(
|
|
36
|
+
"serialise: Date objects are unsupported, please use Temporal.Instant instead",
|
|
37
|
+
);
|
|
38
|
+
} else if (typeof value === "object") {
|
|
39
|
+
return {
|
|
40
|
+
type: "object",
|
|
41
|
+
value: Object.entries(value)
|
|
42
|
+
.filter(([k]) => typeof k !== "symbol")
|
|
43
|
+
.map(([k, v]) => [k, serialise(v)]),
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
throw new TypeError(
|
|
47
|
+
`serialise: don't know how to serialise this ${typeof value}: ${JSON.stringify(value)}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function deserialise(record: Serialised): unknown {
|
|
53
|
+
if (Array.isArray(record)) {
|
|
54
|
+
return record.map(deserialise);
|
|
55
|
+
}
|
|
56
|
+
switch (record.type) {
|
|
57
|
+
case undefined:
|
|
58
|
+
return record.value;
|
|
59
|
+
case "null":
|
|
60
|
+
return null;
|
|
61
|
+
case "bigint":
|
|
62
|
+
return BigInt(record.value as string);
|
|
63
|
+
case "instant":
|
|
64
|
+
return Temporal.Instant.from(record.value as string);
|
|
65
|
+
case "duration":
|
|
66
|
+
return Temporal.Duration.from(record.value as string);
|
|
67
|
+
case "plaindate":
|
|
68
|
+
return Temporal.PlainDate.from(record.value as string);
|
|
69
|
+
case "zdt":
|
|
70
|
+
return Temporal.ZonedDateTime.from(record.value as string);
|
|
71
|
+
case "set":
|
|
72
|
+
return new Set((record.value as Array<Serialised>).map(deserialise));
|
|
73
|
+
case "map":
|
|
74
|
+
return new Map(
|
|
75
|
+
(record.value as Array<[Serialised, Serialised]>).map(([k, v]) => [
|
|
76
|
+
deserialise(k),
|
|
77
|
+
deserialise(v),
|
|
78
|
+
]),
|
|
79
|
+
);
|
|
80
|
+
case "object":
|
|
81
|
+
return Object.fromEntries(
|
|
82
|
+
(record.value as Array<[string | number | symbol, Serialised]>).map(([k, v]) => [
|
|
83
|
+
k,
|
|
84
|
+
deserialise(v),
|
|
85
|
+
]),
|
|
86
|
+
);
|
|
87
|
+
default:
|
|
88
|
+
throw new TypeError(
|
|
89
|
+
`deserialise: can't deserialise type ${JSON.stringify(record.type)} value ${JSON.stringify(record.value)}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isNullish } from "@bodil/core/assert";
|
|
2
|
+
import { Emitter } from "@bodil/core/async";
|
|
3
|
+
import { DisposableContext } from "@bodil/core/disposable";
|
|
4
|
+
|
|
5
|
+
import type { DatabaseBroadcast, StorageBackend } from "./backend";
|
|
6
|
+
import type { UnitIndex } from "./indices";
|
|
7
|
+
import type { Table, TableEvent } from "./table";
|
|
8
|
+
|
|
9
|
+
export class TableStorage<A extends object, PI extends UnitIndex<A>>
|
|
10
|
+
extends Emitter<TableEvent<A, PI["keyType"]>>
|
|
11
|
+
implements Disposable
|
|
12
|
+
{
|
|
13
|
+
readonly #ready = Promise.withResolvers<void>();
|
|
14
|
+
context = new DisposableContext();
|
|
15
|
+
|
|
16
|
+
get ready(): Promise<void> {
|
|
17
|
+
return this.#ready.promise;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
public store: StorageBackend,
|
|
22
|
+
public name: string,
|
|
23
|
+
public primaryKey: PI,
|
|
24
|
+
) {
|
|
25
|
+
super();
|
|
26
|
+
this.context.use(this.store.onBroadcast((message) => this.onBroadcastMessage(message)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override [Symbol.dispose]() {
|
|
30
|
+
super[Symbol.dispose]();
|
|
31
|
+
this.context.dispose();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private onBroadcastMessage(message: DatabaseBroadcast): void {
|
|
35
|
+
if (message.table !== this.name) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
switch (message.event) {
|
|
39
|
+
case "update":
|
|
40
|
+
void this.store.get<A>(this.name, message.key).then((row) => {
|
|
41
|
+
if (!isNullish(row)) {
|
|
42
|
+
this.emit({ type: "update", item: row });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
break;
|
|
46
|
+
case "delete":
|
|
47
|
+
this.emit({ type: "delete", key: message.key });
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private readonly onTableEvent = (event: TableEvent<A, PI["keyType"]>) => {
|
|
53
|
+
switch (event.type) {
|
|
54
|
+
case "update":
|
|
55
|
+
this.update(event.item);
|
|
56
|
+
break;
|
|
57
|
+
case "delete":
|
|
58
|
+
this.delete(event.key);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
async link<Ix extends object>(table: Table<A, PI, Ix>) {
|
|
64
|
+
let rows: Array<A>;
|
|
65
|
+
try {
|
|
66
|
+
rows = await this.store.getAll<A>(this.name);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
this.#ready.reject(e);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
table.add(...rows);
|
|
72
|
+
this.context.use(table.on((event) => this.onTableEvent(event)));
|
|
73
|
+
this.#ready.resolve();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private update(item: A) {
|
|
77
|
+
void this.store.update(this.name, this.primaryKey.extractKey(item), item);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private delete(key: PI["keyType"]) {
|
|
81
|
+
void this.store.delete(this.name, key);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/table.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { assert, isIterable } from "@bodil/core/assert";
|
|
2
|
+
import { Emitter } from "@bodil/core/async";
|
|
3
|
+
import { DisposableContext } from "@bodil/core/disposable";
|
|
4
|
+
import { Signal } from "@bodil/signal";
|
|
5
|
+
import { freeze, produce, type Draft } from "immer";
|
|
6
|
+
import BTree from "sorted-btree";
|
|
7
|
+
|
|
8
|
+
import type { StorageBackend } from "./backend";
|
|
9
|
+
import { CustomIndex, type Index, type UnitIndex } from "./indices";
|
|
10
|
+
import { IndexQuery, IteratorDirection } from "./query";
|
|
11
|
+
import { TableStorage } from "./storage";
|
|
12
|
+
|
|
13
|
+
export type TableObserveOptions = { includeCurrent: boolean };
|
|
14
|
+
|
|
15
|
+
export type TableEvent<A extends object, PK> =
|
|
16
|
+
| { type: "update"; item: A }
|
|
17
|
+
| { type: "delete"; key: PK };
|
|
18
|
+
|
|
19
|
+
export class Table<A extends object, PI extends UnitIndex<A>, Ix extends object>
|
|
20
|
+
extends Emitter<TableEvent<A, PI["keyType"]>>
|
|
21
|
+
implements Disposable, Iterable<Readonly<A>>
|
|
22
|
+
{
|
|
23
|
+
ready: Promise<void> = Promise.resolve();
|
|
24
|
+
|
|
25
|
+
readonly primaryIndex: PI;
|
|
26
|
+
readonly primary = new BTree<PI["keyType"], Readonly<A>>();
|
|
27
|
+
readonly indices: { [K in Exclude<keyof Ix & string, PI["name"]>]: Index<A> } = {} as any;
|
|
28
|
+
readonly indexTables: {
|
|
29
|
+
[K in Exclude<keyof Ix & string, PI["name"]>]: BTree<Ix[K], Array<A>>;
|
|
30
|
+
} = {} as any;
|
|
31
|
+
|
|
32
|
+
readonly changed = Signal(0);
|
|
33
|
+
|
|
34
|
+
#storage?: TableStorage<A, PI>;
|
|
35
|
+
readonly #signals = new BTree<PI["keyType"], WeakRef<Signal.State<Readonly<A> | undefined>>>();
|
|
36
|
+
readonly #signalCleanupRegistry = new FinalizationRegistry(this.#signalCleanup.bind(this));
|
|
37
|
+
readonly #context = new DisposableContext();
|
|
38
|
+
|
|
39
|
+
constructor(primaryIndex: PI) {
|
|
40
|
+
super();
|
|
41
|
+
this.primaryIndex = primaryIndex;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
override [Symbol.dispose]() {
|
|
45
|
+
super[Symbol.dispose]();
|
|
46
|
+
this.#context.dispose();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
withIndex<I extends Index<A>>(
|
|
50
|
+
index: I,
|
|
51
|
+
): Table<A, PI, { [K in keyof (Ix & I["record"])]: (Ix & I["record"])[K] }> {
|
|
52
|
+
assert(
|
|
53
|
+
(this.indices as any)[index.name] === undefined,
|
|
54
|
+
`duplicate index definition: "${index.name}"`,
|
|
55
|
+
);
|
|
56
|
+
(this.indices as any)[index.name] = index;
|
|
57
|
+
(this.indexTables as any)[index.name] =
|
|
58
|
+
index instanceof CustomIndex ? index.makeIndex() : new BTree();
|
|
59
|
+
return this as Table<A, PI, { [K in keyof (Ix & I["record"])]: (Ix & I["record"])[K] }>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
withStorage(backend: StorageBackend, name: string): this {
|
|
63
|
+
this.#storage = new TableStorage(backend, name, this.primaryIndex);
|
|
64
|
+
this.ready = this.#storage.ready;
|
|
65
|
+
this.#context.use(
|
|
66
|
+
this.#storage.on((event: TableEvent<A, PI["keyType"]>) => {
|
|
67
|
+
switch (event.type) {
|
|
68
|
+
case "update":
|
|
69
|
+
this.add(event.item);
|
|
70
|
+
break;
|
|
71
|
+
case "delete":
|
|
72
|
+
this.delete(event.key);
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
void this.#storage.link(this);
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
signal(primaryKey: PI["keyType"]): Signal.Computed<Readonly<A> | undefined> {
|
|
82
|
+
const active = this.#signals.get(primaryKey)?.deref();
|
|
83
|
+
if (active !== undefined) {
|
|
84
|
+
return active.readOnly();
|
|
85
|
+
}
|
|
86
|
+
const sig = Signal(this.get(primaryKey), {
|
|
87
|
+
equals: () => false,
|
|
88
|
+
});
|
|
89
|
+
this.#signalCleanupRegistry.register(sig, primaryKey);
|
|
90
|
+
this.#signals.set(primaryKey, new WeakRef(sig));
|
|
91
|
+
return sig.readOnly();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get(primaryKey: PI["keyType"]): Readonly<A> | undefined {
|
|
95
|
+
return this.primary.get(primaryKey);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Given a primary key, apply an update function to the value stored under
|
|
100
|
+
* that key. If there's no value, call the create function to create the
|
|
101
|
+
* value before passing it to the update function.
|
|
102
|
+
*/
|
|
103
|
+
createAndUpdate(
|
|
104
|
+
primaryKey: PI["keyType"],
|
|
105
|
+
create: () => A,
|
|
106
|
+
update: (item: Draft<A>) => Draft<A> | void,
|
|
107
|
+
): Readonly<A> {
|
|
108
|
+
const oldItem = this.primary.get(primaryKey) ?? create();
|
|
109
|
+
const item = produce(oldItem, update);
|
|
110
|
+
return this.#setItem(item, oldItem, primaryKey);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Given a primary key, apply an update function to the value stored under
|
|
115
|
+
* that key. If there's no value, call the create function to create a new
|
|
116
|
+
* value. In this case, the update function is not called.
|
|
117
|
+
*/
|
|
118
|
+
createOrUpdate(
|
|
119
|
+
primaryKey: PI["keyType"],
|
|
120
|
+
create: () => A,
|
|
121
|
+
update: (item: Draft<A>) => Draft<A> | void,
|
|
122
|
+
): Readonly<A> {
|
|
123
|
+
const oldItem = this.primary.get(primaryKey);
|
|
124
|
+
const item = oldItem === undefined ? create() : produce(oldItem, update);
|
|
125
|
+
return this.#setItem(item, oldItem, primaryKey);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Apply an update function to the value stored under the given primary key.
|
|
130
|
+
* If no value exists, the update function is not called and `undefined` is
|
|
131
|
+
* returned. Otherwise, the updated value is returned.
|
|
132
|
+
*/
|
|
133
|
+
update(
|
|
134
|
+
primaryKey: PI["keyType"],
|
|
135
|
+
update: (item: Draft<A>) => void | Draft<A>,
|
|
136
|
+
): Readonly<A> | undefined {
|
|
137
|
+
const oldItem = this.primary.get(primaryKey);
|
|
138
|
+
if (oldItem === undefined) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const item = produce(oldItem, update);
|
|
142
|
+
return this.#setItem(item, oldItem, primaryKey);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Add items to the table, overwriting any previous values under their
|
|
147
|
+
* primary keys.
|
|
148
|
+
*/
|
|
149
|
+
add(...items: Array<A>): void;
|
|
150
|
+
add(items: Iterable<A>): void;
|
|
151
|
+
add(...items: Array<A | Iterable<A>>) {
|
|
152
|
+
return this.#addFrom(
|
|
153
|
+
items.length === 1 && isIterable(items[0]) ? items[0] : (items as Array<A>),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Delete items from the table by their primary keys.
|
|
159
|
+
*/
|
|
160
|
+
delete(...primaryKeys: Array<PI["keyType"]>): number;
|
|
161
|
+
delete(primaryKeys: Iterable<PI["keyType"]>): number;
|
|
162
|
+
delete(...items: Array<PI["keyType"] | Iterable<PI["keyType"]>>): number {
|
|
163
|
+
return this.#deleteFrom(
|
|
164
|
+
items.length === 1 && isIterable(items[0]) ? items[0] : (items as Array<A>),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Delete all data from the table.
|
|
170
|
+
*/
|
|
171
|
+
clear() {
|
|
172
|
+
this.delete(...this.primary.keys());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
[Symbol.iterator](): IterableIterator<Readonly<A>> {
|
|
176
|
+
return this.primary.values();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
where<K extends keyof Ix & string, I extends Index<A> & Ix[K]>(
|
|
180
|
+
index: K,
|
|
181
|
+
): IndexQuery<A, PI, I, Ix> {
|
|
182
|
+
return new IndexQuery(this, index, IteratorDirection.Ascending);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
orderBy<K extends keyof Ix & string, I extends Index<A> & Ix[K]>(
|
|
186
|
+
index: K,
|
|
187
|
+
): IndexQuery<A, PI, I, Ix> {
|
|
188
|
+
return this.where(index);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
size(): number {
|
|
192
|
+
return this.primary.size;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#addFrom(items: Iterable<A>) {
|
|
196
|
+
for (const item of items) {
|
|
197
|
+
const key = this.primaryIndex.extractKey(item);
|
|
198
|
+
this.#setItem(item, this.primary.get(key), key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#deleteFrom(primaryKeys: Iterable<PI["keyType"]>): number {
|
|
203
|
+
let deleted = 0;
|
|
204
|
+
for (const primaryKey of primaryKeys) {
|
|
205
|
+
const item = this.primary.get(primaryKey);
|
|
206
|
+
if (item !== undefined) {
|
|
207
|
+
this.#deleteItem(item, primaryKey);
|
|
208
|
+
deleted++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return deleted;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#setItem(item: A, oldItem: Readonly<A> | undefined, key: PI["keyType"]): A {
|
|
215
|
+
freeze(item, true);
|
|
216
|
+
|
|
217
|
+
// stop now if the new value is the same as the old one
|
|
218
|
+
if (Object.is(oldItem, item)) {
|
|
219
|
+
return item;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// remove the old item from indices
|
|
223
|
+
if (oldItem !== undefined) {
|
|
224
|
+
this.#deleteFromIndices(oldItem);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// store the new item under its primary key, overwriting the old value
|
|
228
|
+
this.primary.set(key, item, true);
|
|
229
|
+
|
|
230
|
+
// add new item to indices
|
|
231
|
+
for (const property of Object.keys(this.indices)) {
|
|
232
|
+
const index = (this.indices as any)[property];
|
|
233
|
+
const table = (this.indexTables as any)[property];
|
|
234
|
+
const keys = index.extractKeys(item);
|
|
235
|
+
for (const key of keys) {
|
|
236
|
+
const record = table.get(key);
|
|
237
|
+
if (record === undefined) {
|
|
238
|
+
table.set(key, [item]);
|
|
239
|
+
} else {
|
|
240
|
+
record.push(item);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// emit an update event and update signals
|
|
246
|
+
this.#withSignal(this.primaryIndex.extractKey(item), (sig) => sig.set(item));
|
|
247
|
+
this.emit({ type: "update", item });
|
|
248
|
+
this.changed.update((i) => i + 1);
|
|
249
|
+
return item;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#deleteItem(item: A, primaryKey: PI["keyType"]) {
|
|
253
|
+
// remove the item from indices
|
|
254
|
+
this.#deleteFromIndices(item);
|
|
255
|
+
// remove the item from the primary index
|
|
256
|
+
this.primary.delete(primaryKey);
|
|
257
|
+
// emit a delete event and update signals
|
|
258
|
+
this.#withSignal(primaryKey, (sig) => sig.set(undefined));
|
|
259
|
+
this.emit({ type: "delete", key: primaryKey });
|
|
260
|
+
this.changed.update((i) => i + 1);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#withSignal(
|
|
264
|
+
primaryKey: PI["keyType"],
|
|
265
|
+
fn: (signal: Signal.State<Readonly<A> | undefined>) => void,
|
|
266
|
+
) {
|
|
267
|
+
const sigRef = this.#signals.get(primaryKey);
|
|
268
|
+
const sig = sigRef?.deref();
|
|
269
|
+
if (sig !== undefined) {
|
|
270
|
+
// Only run the function if we have a live signal
|
|
271
|
+
return fn(sig);
|
|
272
|
+
} else if (sigRef !== undefined) {
|
|
273
|
+
// Remove dangling reference from signal map
|
|
274
|
+
this.#signals.delete(primaryKey);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#deleteFromIndices(item: A) {
|
|
279
|
+
for (const property of Object.keys(this.indices)) {
|
|
280
|
+
const index = (this.indices as any)[property];
|
|
281
|
+
const table = (this.indexTables as any)[property];
|
|
282
|
+
const keys = index.extractKeys(item);
|
|
283
|
+
for (const key of keys) {
|
|
284
|
+
const record = table.get(key);
|
|
285
|
+
if (record !== undefined) {
|
|
286
|
+
const recordIndex = record.indexOf(item);
|
|
287
|
+
if (recordIndex >= 0) {
|
|
288
|
+
if (record.length === 1) {
|
|
289
|
+
table.delete(key);
|
|
290
|
+
} else {
|
|
291
|
+
record.splice(recordIndex, 1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#signalCleanup(primaryKey: PI["keyType"]) {
|
|
300
|
+
this.#signals.delete(primaryKey);
|
|
301
|
+
}
|
|
302
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type IndexablePrimitive = string | number | null | undefined | bigint;
|
|
2
|
+
export type IndexableType =
|
|
3
|
+
| IndexablePrimitive
|
|
4
|
+
| Array<IndexablePrimitive>
|
|
5
|
+
| ReadonlyArray<IndexablePrimitive>;
|
|
6
|
+
|
|
7
|
+
export type CustomIndexablesOf<A, T> = string &
|
|
8
|
+
keyof A &
|
|
9
|
+
{ [Key in keyof A]: A[Key] extends T ? Key : never }[keyof A];
|
|
10
|
+
|
|
11
|
+
export type IndexablesOf<A> = CustomIndexablesOf<A, IndexableType>;
|
|
12
|
+
export type PrimitiveIndexablesOf<A> = CustomIndexablesOf<A, IndexablePrimitive>;
|
|
13
|
+
export type ArrayIndexablesOf<A> = CustomIndexablesOf<
|
|
14
|
+
A,
|
|
15
|
+
Array<IndexablePrimitive> | ReadonlyArray<IndexablePrimitive>
|
|
16
|
+
>;
|