shelving 1.271.4 → 1.273.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/cloudflare/CloudflareKVProvider.d.ts +9 -9
- package/cloudflare/CloudflareKVProvider.js +17 -17
- package/db/cache/DBCache.d.ts +1 -1
- package/db/cache/DBCache.js +3 -2
- package/db/index.d.ts +1 -0
- package/db/index.js +1 -0
- package/db/migrate/DBMigrator.d.ts +1 -1
- package/db/migrate/SQLMigrator.js +6 -6
- package/db/migrate/SQLiteMigrator.js +3 -3
- package/db/provider/MemoryDBProvider.d.ts +15 -2
- package/db/provider/MemoryDBProvider.js +33 -47
- package/db/provider/SQLProvider.d.ts +9 -9
- package/db/provider/SQLProvider.js +16 -16
- package/db/provider/StorageDBProvider.d.ts +79 -0
- package/db/provider/StorageDBProvider.js +163 -0
- package/error/{UnimplementedError.d.ts → UnsupportedError.d.ts} +4 -4
- package/error/UnsupportedError.js +14 -0
- package/error/index.d.ts +1 -1
- package/error/index.js +1 -1
- package/firestore/lite/FirestoreLiteProvider.d.ts +3 -3
- package/firestore/lite/FirestoreLiteProvider.js +6 -6
- package/package.json +1 -1
- package/error/UnimplementedError.js +0 -14
|
@@ -16,9 +16,9 @@ import type { KVNamespace } from "./types.js";
|
|
|
16
16
|
* - ID generation: `addItem()` generates a UUID v4 identifier automatically.
|
|
17
17
|
*
|
|
18
18
|
* ### Not supported
|
|
19
|
-
* - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `
|
|
19
|
+
* - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `UnsupportedError`.
|
|
20
20
|
* KV has no change feed or push notification mechanism.
|
|
21
|
-
* - **Updates:** `updateItem()` and `updateQuery()` throw `
|
|
21
|
+
* - **Updates:** `updateItem()` and `updateQuery()` throw `UnsupportedError`.
|
|
22
22
|
* - **Collection queries:** `getQuery()`, `setQuery()`, `deleteQuery()`, and `countQuery()` are not supported.
|
|
23
23
|
* KV does not expose efficient filtering, sorting, or collection scans, so this provider avoids the old "read everything and filter in memory" behavior.
|
|
24
24
|
*
|
|
@@ -33,22 +33,22 @@ export declare class CloudflareKVProvider<I extends string = string, T extends D
|
|
|
33
33
|
private readonly _kv;
|
|
34
34
|
constructor(kv: KVNamespace);
|
|
35
35
|
getItem<II extends I, TT extends T>({ name }: Collection<string, II, TT>, id: II): Promise<OptionalItem<II, TT>>;
|
|
36
|
-
/** Not supported — KV has no change feed, so this throws `
|
|
36
|
+
/** Not supported — KV has no change feed, so this throws `UnsupportedError`. */
|
|
37
37
|
getItemSequence<II extends I, TT extends T>(_collection: Collection<string, II, TT>, _id: II): OptionalItemSequence<II, TT>;
|
|
38
38
|
/** Generates a UUID v4 identifier for the new item. */
|
|
39
39
|
addItem<II extends I, TT extends T>({ name }: Collection<string, II, TT>, data: TT): Promise<II>;
|
|
40
40
|
setItem<II extends I, TT extends T>({ name }: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
|
|
41
|
-
/** Not supported — KV cannot apply partial updates, so this throws `
|
|
41
|
+
/** Not supported — KV cannot apply partial updates, so this throws `UnsupportedError`. */
|
|
42
42
|
updateItem<II extends I, TT extends T>(_collection: Collection<string, II, TT>, _id: II, _updates: Updates<Item<II, TT>>): Promise<void>;
|
|
43
43
|
deleteItem<II extends I, TT extends T>({ name }: Collection<string, II, TT>, id: II): Promise<void>;
|
|
44
|
-
/** Not supported — KV cannot filter, sort, or scan collections, so this throws `
|
|
44
|
+
/** Not supported — KV cannot filter, sort, or scan collections, so this throws `UnsupportedError`. */
|
|
45
45
|
getQuery<II extends I, TT extends T>(_collection: Collection<string, II, TT>, _query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
|
|
46
|
-
/** Not supported — KV has no change feed, so this throws `
|
|
46
|
+
/** Not supported — KV has no change feed, so this throws `UnsupportedError`. */
|
|
47
47
|
getQuerySequence<II extends I, TT extends T>(_collection: Collection<string, II, TT>, _query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
|
|
48
|
-
/** Not supported — KV cannot run collection queries, so this throws `
|
|
48
|
+
/** Not supported — KV cannot run collection queries, so this throws `UnsupportedError`. */
|
|
49
49
|
setQuery<II extends I, TT extends T>(_collection: Collection<string, II, TT>, _query: Query<Item<II, TT>>, _data: TT): Promise<void>;
|
|
50
|
-
/** Not supported — KV supports neither updates nor collection queries, so this throws `
|
|
50
|
+
/** Not supported — KV supports neither updates nor collection queries, so this throws `UnsupportedError`. */
|
|
51
51
|
updateQuery<II extends I, TT extends T>(_collection: Collection<string, II, TT>, _query: Query<Item<II, TT>>, _updates: Updates<TT>): Promise<void>;
|
|
52
|
-
/** Not supported — KV cannot run collection queries, so this throws `
|
|
52
|
+
/** Not supported — KV cannot run collection queries, so this throws `UnsupportedError`. */
|
|
53
53
|
deleteQuery<II extends I, TT extends T>(_collection: Collection<string, II, TT>, _query: Query<Item<II, TT>>): Promise<void>;
|
|
54
54
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DBProvider } from "../db/provider/DBProvider.js";
|
|
2
|
-
import {
|
|
2
|
+
import { UnsupportedError } from "../error/UnsupportedError.js";
|
|
3
3
|
import { getItem } from "../util/item.js";
|
|
4
4
|
import { randomUUID } from "../util/uuid.js";
|
|
5
5
|
/**
|
|
@@ -13,9 +13,9 @@ import { randomUUID } from "../util/uuid.js";
|
|
|
13
13
|
* - ID generation: `addItem()` generates a UUID v4 identifier automatically.
|
|
14
14
|
*
|
|
15
15
|
* ### Not supported
|
|
16
|
-
* - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `
|
|
16
|
+
* - **Realtime subscriptions:** `getItemSequence()` and `getQuerySequence()` throw `UnsupportedError`.
|
|
17
17
|
* KV has no change feed or push notification mechanism.
|
|
18
|
-
* - **Updates:** `updateItem()` and `updateQuery()` throw `
|
|
18
|
+
* - **Updates:** `updateItem()` and `updateQuery()` throw `UnsupportedError`.
|
|
19
19
|
* - **Collection queries:** `getQuery()`, `setQuery()`, `deleteQuery()`, and `countQuery()` are not supported.
|
|
20
20
|
* KV does not expose efficient filtering, sorting, or collection scans, so this provider avoids the old "read everything and filter in memory" behavior.
|
|
21
21
|
*
|
|
@@ -37,9 +37,9 @@ export class CloudflareKVProvider extends DBProvider {
|
|
|
37
37
|
if (data)
|
|
38
38
|
return getItem(id, data);
|
|
39
39
|
}
|
|
40
|
-
/** Not supported — KV has no change feed, so this throws `
|
|
40
|
+
/** Not supported — KV has no change feed, so this throws `UnsupportedError`. */
|
|
41
41
|
getItemSequence(_collection, _id) {
|
|
42
|
-
throw new
|
|
42
|
+
throw new UnsupportedError("CloudflareKVProvider does not support realtime subscriptions");
|
|
43
43
|
}
|
|
44
44
|
/** Generates a UUID v4 identifier for the new item. */
|
|
45
45
|
async addItem({ name }, data) {
|
|
@@ -50,32 +50,32 @@ export class CloudflareKVProvider extends DBProvider {
|
|
|
50
50
|
async setItem({ name }, id, data) {
|
|
51
51
|
await this._kv.put(_getKey(name, id), JSON.stringify(data));
|
|
52
52
|
}
|
|
53
|
-
/** Not supported — KV cannot apply partial updates, so this throws `
|
|
53
|
+
/** Not supported — KV cannot apply partial updates, so this throws `UnsupportedError`. */
|
|
54
54
|
async updateItem(_collection, _id, _updates) {
|
|
55
|
-
throw new
|
|
55
|
+
throw new UnsupportedError("CloudflareKVProvider does not support updates to items");
|
|
56
56
|
}
|
|
57
57
|
async deleteItem({ name }, id) {
|
|
58
58
|
await this._kv.delete(_getKey(name, id));
|
|
59
59
|
}
|
|
60
|
-
/** Not supported — KV cannot filter, sort, or scan collections, so this throws `
|
|
60
|
+
/** Not supported — KV cannot filter, sort, or scan collections, so this throws `UnsupportedError`. */
|
|
61
61
|
async getQuery(_collection, _query) {
|
|
62
|
-
throw new
|
|
62
|
+
throw new UnsupportedError("CloudflareKVProvider does not support querying items");
|
|
63
63
|
}
|
|
64
|
-
/** Not supported — KV has no change feed, so this throws `
|
|
64
|
+
/** Not supported — KV has no change feed, so this throws `UnsupportedError`. */
|
|
65
65
|
getQuerySequence(_collection, _query) {
|
|
66
|
-
throw new
|
|
66
|
+
throw new UnsupportedError("CloudflareKVProvider does not support realtime subscriptions");
|
|
67
67
|
}
|
|
68
|
-
/** Not supported — KV cannot run collection queries, so this throws `
|
|
68
|
+
/** Not supported — KV cannot run collection queries, so this throws `UnsupportedError`. */
|
|
69
69
|
async setQuery(_collection, _query, _data) {
|
|
70
|
-
throw new
|
|
70
|
+
throw new UnsupportedError("CloudflareKVProvider does not support querying items");
|
|
71
71
|
}
|
|
72
|
-
/** Not supported — KV supports neither updates nor collection queries, so this throws `
|
|
72
|
+
/** Not supported — KV supports neither updates nor collection queries, so this throws `UnsupportedError`. */
|
|
73
73
|
async updateQuery(_collection, _query, _updates) {
|
|
74
|
-
throw new
|
|
74
|
+
throw new UnsupportedError("CloudflareKVProvider does not support updates to items");
|
|
75
75
|
}
|
|
76
|
-
/** Not supported — KV cannot run collection queries, so this throws `
|
|
76
|
+
/** Not supported — KV cannot run collection queries, so this throws `UnsupportedError`. */
|
|
77
77
|
async deleteQuery(_collection, _query) {
|
|
78
|
-
throw new
|
|
78
|
+
throw new UnsupportedError("CloudflareKVProvider does not support querying items");
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
function _getKey(collection, id) {
|
package/db/cache/DBCache.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Identifier, Item } from "../../util/item.js";
|
|
|
3
3
|
import type { Query } from "../../util/query.js";
|
|
4
4
|
import type { Collection } from "../collection/Collection.js";
|
|
5
5
|
import type { DBProvider } from "../provider/DBProvider.js";
|
|
6
|
-
import
|
|
6
|
+
import { MemoryDBProvider } from "../provider/MemoryDBProvider.js";
|
|
7
7
|
import type { ItemStore } from "../store/ItemStore.js";
|
|
8
8
|
import type { QueryStore } from "../store/QueryStore.js";
|
|
9
9
|
import { CollectionCache } from "./CollectionCache.js";
|
package/db/cache/DBCache.js
CHANGED
|
@@ -2,6 +2,7 @@ import { awaitDispose } from "../../util/dispose.js";
|
|
|
2
2
|
import { setMapItem } from "../../util/map.js";
|
|
3
3
|
import { getSource } from "../../util/source.js";
|
|
4
4
|
import { CacheDBProvider } from "../provider/CacheDBProvider.js";
|
|
5
|
+
import { MemoryDBProvider } from "../provider/MemoryDBProvider.js";
|
|
5
6
|
import { CollectionCache } from "./CollectionCache.js";
|
|
6
7
|
/**
|
|
7
8
|
* Cache of `CollectionCache` objects for multiple collections.
|
|
@@ -28,8 +29,8 @@ export class DBCache {
|
|
|
28
29
|
memory;
|
|
29
30
|
constructor(provider) {
|
|
30
31
|
this.provider = provider;
|
|
31
|
-
// If the provider chain contains a `CacheDBProvider`, reuse its memory so we can seed stores synchronously.
|
|
32
|
-
this.memory = getSource(CacheDBProvider, provider)?.memory;
|
|
32
|
+
// If the provider is itself in-memory (e.g. `MemoryDBProvider` or `StorageDBProvider`), or the chain contains a `CacheDBProvider`, reuse its memory so we can seed stores synchronously.
|
|
33
|
+
this.memory = provider instanceof MemoryDBProvider ? provider : getSource(CacheDBProvider, provider)?.memory;
|
|
33
34
|
}
|
|
34
35
|
_get(collection) {
|
|
35
36
|
return this._collections.get(collection);
|
package/db/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./provider/MockDBProvider.js";
|
|
|
14
14
|
export * from "./provider/PostgreSQLProvider.js";
|
|
15
15
|
export * from "./provider/SQLiteProvider.js";
|
|
16
16
|
export * from "./provider/SQLProvider.js";
|
|
17
|
+
export * from "./provider/StorageDBProvider.js";
|
|
17
18
|
export * from "./provider/ThroughDBProvider.js";
|
|
18
19
|
export * from "./provider/ValidationDBProvider.js";
|
|
19
20
|
export * from "./store/ItemStore.js";
|
package/db/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export * from "./provider/MockDBProvider.js";
|
|
|
14
14
|
export * from "./provider/PostgreSQLProvider.js";
|
|
15
15
|
export * from "./provider/SQLiteProvider.js";
|
|
16
16
|
export * from "./provider/SQLProvider.js";
|
|
17
|
+
export * from "./provider/StorageDBProvider.js";
|
|
17
18
|
export * from "./provider/ThroughDBProvider.js";
|
|
18
19
|
export * from "./provider/ValidationDBProvider.js";
|
|
19
20
|
export * from "./store/ItemStore.js";
|
|
@@ -18,7 +18,7 @@ export declare abstract class DBMigrator<T extends DBProvider = DBProvider> {
|
|
|
18
18
|
* Bring the provider's storage into line with the given collection schemas.
|
|
19
19
|
*
|
|
20
20
|
* @param collections The collections whose schemas the storage should match.
|
|
21
|
-
* @throws `
|
|
21
|
+
* @throws `UnsupportedError` if a schema feature can't be represented by the backend.
|
|
22
22
|
* @example await migrator.migrate(users, posts)
|
|
23
23
|
* @see https://shelving.cc/db/DBMigrator/migrate
|
|
24
24
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UnsupportedError } from "../../error/UnsupportedError.js";
|
|
2
2
|
import { ArraySchema } from "../../schema/ArraySchema.js";
|
|
3
3
|
import { BooleanSchema } from "../../schema/BooleanSchema.js";
|
|
4
4
|
import { ChoiceSchema } from "../../schema/ChoiceSchema.js";
|
|
@@ -97,12 +97,12 @@ export class SQLMigrator extends DBMigrator {
|
|
|
97
97
|
}
|
|
98
98
|
getAddColumnQuery(tableName, column) {
|
|
99
99
|
if (column.name === "id")
|
|
100
|
-
throw new
|
|
100
|
+
throw new UnsupportedError(`Cannot add primary key column to existing table "${tableName}"`);
|
|
101
101
|
return `ALTER TABLE ${this.quoteIdentifier(tableName)} ADD COLUMN ${this.getTableColumnDefinition(column)};`;
|
|
102
102
|
}
|
|
103
103
|
getDropColumnQuery(tableName, columnName) {
|
|
104
104
|
if (columnName === "id")
|
|
105
|
-
throw new
|
|
105
|
+
throw new UnsupportedError(`Cannot drop primary key column from existing table "${tableName}"`);
|
|
106
106
|
return `ALTER TABLE ${this.quoteIdentifier(tableName)} DROP COLUMN ${this.quoteIdentifier(columnName)};`;
|
|
107
107
|
}
|
|
108
108
|
getTableMigrations(collection, table) {
|
|
@@ -129,7 +129,7 @@ export class SQLMigrator extends DBMigrator {
|
|
|
129
129
|
const schema = _getColumnSchema(collection, key);
|
|
130
130
|
const definition = this.definition(schema);
|
|
131
131
|
if (!definition)
|
|
132
|
-
throw new
|
|
132
|
+
throw new UnsupportedError(`Cannot generate SQL column for "${key}"`, { received: schema });
|
|
133
133
|
return { name: column, statement: this.getGeneratedColumnDefinition(column, path, definition) };
|
|
134
134
|
}
|
|
135
135
|
isSameColumn(from, to) {
|
|
@@ -180,10 +180,10 @@ function _getColumnSchema(collection, key) {
|
|
|
180
180
|
for (const part of key.split(".")) {
|
|
181
181
|
const current = _unwrapSchema(schema);
|
|
182
182
|
if (!(current instanceof DataSchema))
|
|
183
|
-
throw new
|
|
183
|
+
throw new UnsupportedError(`Cannot resolve schema path "${key}"`);
|
|
184
184
|
const next = current.props[part];
|
|
185
185
|
if (!next)
|
|
186
|
-
throw new
|
|
186
|
+
throw new UnsupportedError(`Cannot resolve schema path "${key}"`);
|
|
187
187
|
schema = next;
|
|
188
188
|
}
|
|
189
189
|
return schema;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { UnsupportedError } from "../../error/UnsupportedError.js";
|
|
2
2
|
import { ChoiceSchema } from "../../schema/ChoiceSchema.js";
|
|
3
3
|
import { DateSchema } from "../../schema/DateSchema.js";
|
|
4
4
|
import { NumberSchema } from "../../schema/NumberSchema.js";
|
|
@@ -47,7 +47,7 @@ export class SQLiteMigrator extends SQLMigrator {
|
|
|
47
47
|
if (id instanceof NumberSchema) {
|
|
48
48
|
if (id.step === 1)
|
|
49
49
|
return "INTEGER PRIMARY KEY";
|
|
50
|
-
throw new
|
|
50
|
+
throw new UnsupportedError("SQLiteMigrator only supports string and integer identifiers", { received: id });
|
|
51
51
|
}
|
|
52
52
|
if (id instanceof ChoiceSchema || id instanceof DateSchema)
|
|
53
53
|
return "TEXT PRIMARY KEY";
|
|
@@ -62,7 +62,7 @@ export class SQLiteMigrator extends SQLMigrator {
|
|
|
62
62
|
}
|
|
63
63
|
getAlterColumnQueries(tableName, from, to) {
|
|
64
64
|
if (from.name === "id" || from.name === "data") {
|
|
65
|
-
throw new
|
|
65
|
+
throw new UnsupportedError(`Cannot alter SQLite column "${from.name}" in existing table "${tableName}"`);
|
|
66
66
|
}
|
|
67
67
|
return super.getAlterColumnQueries(tableName, from, to);
|
|
68
68
|
}
|
|
@@ -16,7 +16,9 @@ import { DBProvider } from "./DBProvider.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export declare class MemoryDBProvider<I extends Identifier = Identifier, T extends Data = Data> extends DBProvider<I, T> {
|
|
18
18
|
/** List of tables in `{ name: MemoryTable }` format. */
|
|
19
|
-
|
|
19
|
+
protected _tables: {
|
|
20
|
+
[K in string]?: MemoryTable<I, T>;
|
|
21
|
+
};
|
|
20
22
|
/**
|
|
21
23
|
* Get (or lazily create) the `MemoryTable` backing a collection.
|
|
22
24
|
*
|
|
@@ -25,6 +27,15 @@ export declare class MemoryDBProvider<I extends Identifier = Identifier, T exten
|
|
|
25
27
|
* @see https://shelving.cc/db/MemoryDBProvider/getTable
|
|
26
28
|
*/
|
|
27
29
|
getTable<II extends I, TT extends T>(collection: Collection<string, II, TT>): MemoryTable<II, TT>;
|
|
30
|
+
/**
|
|
31
|
+
* Create a new `MemoryTable` for a collection (without registering it — use `getTable()` for that).
|
|
32
|
+
* - Override point for subclasses that back collections with a specialised table, e.g. `StorageDBProvider`.
|
|
33
|
+
*
|
|
34
|
+
* @param collection Collection to create a table for.
|
|
35
|
+
* @example provider.createTable(users) // MemoryTable
|
|
36
|
+
* @see https://shelving.cc/db/MemoryDBProvider/createTable
|
|
37
|
+
*/
|
|
38
|
+
createTable<II extends I, TT extends T>(collection: Collection<string, II, TT>): MemoryTable<II, TT>;
|
|
28
39
|
getItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<OptionalItem<II, TT>>;
|
|
29
40
|
getItemSequence<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): OptionalItemSequence<II, TT>;
|
|
30
41
|
addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
|
|
@@ -46,6 +57,7 @@ export declare class MemoryDBProvider<I extends Identifier = Identifier, T exten
|
|
|
46
57
|
* @see https://shelving.cc/db/MemoryDBProvider/setItems
|
|
47
58
|
*/
|
|
48
59
|
setItems<II extends I, TT extends T>(collection: Collection<string, II, TT>, items: Items<II, TT>): void;
|
|
60
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
49
61
|
}
|
|
50
62
|
/**
|
|
51
63
|
* In-memory table holding the items of a single collection for a `MemoryDBProvider`.
|
|
@@ -59,7 +71,7 @@ export declare class MemoryDBProvider<I extends Identifier = Identifier, T exten
|
|
|
59
71
|
*
|
|
60
72
|
* @see https://shelving.cc/db/MemoryTable
|
|
61
73
|
*/
|
|
62
|
-
export declare class MemoryTable<I extends Identifier, T extends Data> {
|
|
74
|
+
export declare class MemoryTable<I extends Identifier, T extends Data> implements AsyncDisposable {
|
|
63
75
|
/** Actual data in this table. */
|
|
64
76
|
protected readonly _data: Map<I, Item<I, T>>;
|
|
65
77
|
/**
|
|
@@ -203,4 +215,5 @@ export declare class MemoryTable<I extends Identifier, T extends Data> {
|
|
|
203
215
|
deleteQuery(query: Query<Item<I, T>>): void;
|
|
204
216
|
setItems(items: Items<I, T>): void;
|
|
205
217
|
setItemsSequence(sequence: AsyncIterable<Items<I, T>>): AsyncIterable<Items<I, T>>;
|
|
218
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
206
219
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { StringSchema } from "../../schema/StringSchema.js";
|
|
2
2
|
import { DeferredSequence } from "../../sequence/DeferredSequence.js";
|
|
3
3
|
import { requireArray } from "../../util/array.js";
|
|
4
|
+
import { awaitDispose } from "../../util/dispose.js";
|
|
4
5
|
import { isArrayEqual } from "../../util/equal.js";
|
|
5
6
|
import { getItem } from "../../util/item.js";
|
|
6
7
|
import { countItems } from "../../util/iterate.js";
|
|
@@ -28,7 +29,18 @@ export class MemoryDBProvider extends DBProvider {
|
|
|
28
29
|
* @see https://shelving.cc/db/MemoryDBProvider/getTable
|
|
29
30
|
*/
|
|
30
31
|
getTable(collection) {
|
|
31
|
-
return (this._tables[collection.name] ||=
|
|
32
|
+
return (this._tables[collection.name] ||= this.createTable(collection));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create a new `MemoryTable` for a collection (without registering it — use `getTable()` for that).
|
|
36
|
+
* - Override point for subclasses that back collections with a specialised table, e.g. `StorageDBProvider`.
|
|
37
|
+
*
|
|
38
|
+
* @param collection Collection to create a table for.
|
|
39
|
+
* @example provider.createTable(users) // MemoryTable
|
|
40
|
+
* @see https://shelving.cc/db/MemoryDBProvider/createTable
|
|
41
|
+
*/
|
|
42
|
+
createTable(collection) {
|
|
43
|
+
return new MemoryTable(collection);
|
|
32
44
|
}
|
|
33
45
|
async getItem(collection, id) {
|
|
34
46
|
return this.getTable(collection).getItem(id);
|
|
@@ -77,6 +89,11 @@ export class MemoryDBProvider extends DBProvider {
|
|
|
77
89
|
setItems(collection, items) {
|
|
78
90
|
this.getTable(collection).setItems(items);
|
|
79
91
|
}
|
|
92
|
+
// Implement `AsyncDisposable`
|
|
93
|
+
async [Symbol.asyncDispose]() {
|
|
94
|
+
await awaitDispose(...Object.values(this._tables), // Dispose all tables.
|
|
95
|
+
super[Symbol.asyncDispose]());
|
|
96
|
+
}
|
|
80
97
|
}
|
|
81
98
|
/**
|
|
82
99
|
* In-memory table holding the items of a single collection for a `MemoryDBProvider`.
|
|
@@ -214,11 +231,7 @@ export class MemoryTable {
|
|
|
214
231
|
const oldItem = this._data.get(id);
|
|
215
232
|
if (!oldItem)
|
|
216
233
|
return;
|
|
217
|
-
|
|
218
|
-
if (this._data.get(id) !== nextItem) {
|
|
219
|
-
this._data.set(id, nextItem);
|
|
220
|
-
this.next.resolve();
|
|
221
|
-
}
|
|
234
|
+
this.setItem(id, updateData(oldItem, updates));
|
|
222
235
|
}
|
|
223
236
|
/**
|
|
224
237
|
* Delete an item by its id.
|
|
@@ -287,16 +300,8 @@ export class MemoryTable {
|
|
|
287
300
|
* @see https://shelving.cc/db/MemoryTable/setQuery
|
|
288
301
|
*/
|
|
289
302
|
setQuery(query, data) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const item = getItem(id, data);
|
|
293
|
-
if (this._data.get(id) !== item) {
|
|
294
|
-
this._data.set(id, item);
|
|
295
|
-
changed = true;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
if (changed)
|
|
299
|
-
this.next.resolve();
|
|
303
|
+
for (const { id } of queryWritableItems(this._data.values(), query))
|
|
304
|
+
this.setItem(id, data);
|
|
300
305
|
}
|
|
301
306
|
/**
|
|
302
307
|
* Apply partial updates to every item matching a query.
|
|
@@ -307,41 +312,16 @@ export class MemoryTable {
|
|
|
307
312
|
* @see https://shelving.cc/db/MemoryTable/updateQuery
|
|
308
313
|
*/
|
|
309
314
|
updateQuery(query, updates) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const oldItem = this._data.get(id);
|
|
313
|
-
if (!oldItem)
|
|
314
|
-
continue;
|
|
315
|
-
const nextItem = updateData(oldItem, updates);
|
|
316
|
-
if (this._data.get(id) !== nextItem) {
|
|
317
|
-
this._data.set(id, nextItem);
|
|
318
|
-
changed = true;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
if (changed)
|
|
322
|
-
this.next.resolve();
|
|
315
|
+
for (const { id } of queryWritableItems(this._data.values(), query))
|
|
316
|
+
this.updateItem(id, updates);
|
|
323
317
|
}
|
|
324
318
|
deleteQuery(query) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (this._data.has(id)) {
|
|
328
|
-
this._data.delete(id);
|
|
329
|
-
changed = true;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
if (changed)
|
|
333
|
-
this.next.resolve();
|
|
319
|
+
for (const { id } of queryWritableItems(this._data.values(), query))
|
|
320
|
+
this.deleteItem(id);
|
|
334
321
|
}
|
|
335
322
|
setItems(items) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (this._data.get(item.id) !== item) {
|
|
339
|
-
this._data.set(item.id, item);
|
|
340
|
-
changed = true;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
if (changed)
|
|
344
|
-
this.next.resolve();
|
|
323
|
+
for (const item of items)
|
|
324
|
+
this.setItem(item.id, item);
|
|
345
325
|
}
|
|
346
326
|
async *setItemsSequence(sequence) {
|
|
347
327
|
for await (const items of sequence) {
|
|
@@ -349,4 +329,10 @@ export class MemoryTable {
|
|
|
349
329
|
yield items;
|
|
350
330
|
}
|
|
351
331
|
}
|
|
332
|
+
// Implement `AsyncDisposable`
|
|
333
|
+
async [Symbol.asyncDispose]() {
|
|
334
|
+
await awaitDispose(
|
|
335
|
+
// Empty by default.
|
|
336
|
+
);
|
|
337
|
+
}
|
|
352
338
|
}
|
|
@@ -19,7 +19,7 @@ export interface SQLFragment {
|
|
|
19
19
|
*
|
|
20
20
|
* - Subclasses implement `exec()` to run a query against a concrete database (e.g. SQLite, PostgreSQL).
|
|
21
21
|
* - The `sql*` helpers build composable `SQLFragment` objects so dialect differences can be overridden.
|
|
22
|
-
* - Realtime subscriptions are unsupported and throw `
|
|
22
|
+
* - Realtime subscriptions are unsupported and throw `UnsupportedError`.
|
|
23
23
|
*
|
|
24
24
|
* @see https://shelving.cc/db/SQLProvider
|
|
25
25
|
*/
|
|
@@ -34,7 +34,7 @@ export declare abstract class SQLProvider<I extends Identifier = Identifier, T e
|
|
|
34
34
|
*/
|
|
35
35
|
abstract exec<X extends Data>(strings: TemplateStringsArray, ...values: ImmutableArray<unknown>): Promise<ImmutableArray<X>>;
|
|
36
36
|
getItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<OptionalItem<II, TT>>;
|
|
37
|
-
/** Unsupported by SQL providers — always throws `
|
|
37
|
+
/** Unsupported by SQL providers — always throws `UnsupportedError`. */
|
|
38
38
|
getItemSequence<II extends I, TT extends T>(_collection: Collection<string, II, TT>, _id: II): OptionalItemSequence<II, TT>;
|
|
39
39
|
/** Insert via `INSERT ... RETURNING`; throws `RequiredError` if no id comes back. */
|
|
40
40
|
addItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, data: TT): Promise<II>;
|
|
@@ -43,7 +43,7 @@ export declare abstract class SQLProvider<I extends Identifier = Identifier, T e
|
|
|
43
43
|
deleteItem<II extends I, TT extends T>(collection: Collection<string, II, TT>, id: II): Promise<void>;
|
|
44
44
|
countQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<number>;
|
|
45
45
|
getQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
|
|
46
|
-
/** Unsupported by SQL providers — always throws `
|
|
46
|
+
/** Unsupported by SQL providers — always throws `UnsupportedError`. */
|
|
47
47
|
getQuerySequence<II extends I, TT extends T>(_collection: Collection<string, II, TT>, _query?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
|
|
48
48
|
setQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, data: TT): Promise<void>;
|
|
49
49
|
updateQuery<II extends I, TT extends T>(collection: Collection<string, II, TT>, query: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
|
|
@@ -70,7 +70,7 @@ export declare abstract class SQLProvider<I extends Identifier = Identifier, T e
|
|
|
70
70
|
* - Base implementation only supports flat (single-segment) keys; subclasses override for nested JSON paths.
|
|
71
71
|
*
|
|
72
72
|
* @param key The key segments identifying the column (and any nested path).
|
|
73
|
-
* @throws `
|
|
73
|
+
* @throws `UnsupportedError` if the key is nested (multi-segment).
|
|
74
74
|
* @example this.sqlExtract(["name"]); // "name"
|
|
75
75
|
* @see https://shelving.cc/db/SQLProvider/sqlExtract
|
|
76
76
|
*/
|
|
@@ -105,11 +105,11 @@ export declare abstract class SQLProvider<I extends Identifier = Identifier, T e
|
|
|
105
105
|
/**
|
|
106
106
|
* Define an SQL fragment for a single update action.
|
|
107
107
|
* - Handles flat `set` and `sum` only (single-segment key).
|
|
108
|
-
* - Nested keys (multi-segment) and `with`/`omit` actions throw `
|
|
108
|
+
* - Nested keys (multi-segment) and `with`/`omit` actions throw `UnsupportedError`.
|
|
109
109
|
* - Subclasses should override to support nested keys and array mutation actions.
|
|
110
110
|
*
|
|
111
111
|
* @param update The update action (`action`, `key`, `value`) to convert.
|
|
112
|
-
* @throws `
|
|
112
|
+
* @throws `UnsupportedError` if the key is nested or the action is unsupported.
|
|
113
113
|
* @example this.sqlUpdate({ action: "set", key: ["a"], value: 1 }); // "a" = 1
|
|
114
114
|
* @see https://shelving.cc/db/SQLProvider/sqlUpdate
|
|
115
115
|
*/
|
|
@@ -144,7 +144,7 @@ export declare abstract class SQLProvider<I extends Identifier = Identifier, T e
|
|
|
144
144
|
* Define an SQL fragment for a single filter clause on a column, e.g. `x = 1` or `x IN (1, 2)`.
|
|
145
145
|
*
|
|
146
146
|
* @param filter The filter (`key`, `operator`, `value`) to translate.
|
|
147
|
-
* @throws `
|
|
147
|
+
* @throws `UnsupportedError` if the operator is unsupported.
|
|
148
148
|
* @example this.sqlFilter({ key: ["x"], operator: "is", value: 1 }); // "x" = 1
|
|
149
149
|
* @see https://shelving.cc/db/SQLProvider/sqlFilter
|
|
150
150
|
*/
|
|
@@ -152,10 +152,10 @@ export declare abstract class SQLProvider<I extends Identifier = Identifier, T e
|
|
|
152
152
|
/**
|
|
153
153
|
* Define an SQL fragment for an `ORDER BY` clause, e.g. ` ORDER BY "a" ASC, "b" DESC`.
|
|
154
154
|
* - Returns an empty fragment when the query has no orders.
|
|
155
|
-
* - Nested keys (multi-segment) throw `
|
|
155
|
+
* - Nested keys (multi-segment) throw `UnsupportedError`.
|
|
156
156
|
*
|
|
157
157
|
* @param query The query whose orders become the `ORDER BY` clause.
|
|
158
|
-
* @throws `
|
|
158
|
+
* @throws `UnsupportedError` if an order key is nested (multi-segment).
|
|
159
159
|
* @example this.sqlOrder(query); // ORDER BY "a" ASC
|
|
160
160
|
* @see https://shelving.cc/db/SQLProvider/sqlOrder
|
|
161
161
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RequiredError } from "../../error/RequiredError.js";
|
|
2
|
-
import {
|
|
2
|
+
import { UnsupportedError } from "../../error/UnsupportedError.js";
|
|
3
3
|
import { getQueryFilters, getQueryLimit, getQueryOrders } from "../../util/query.js";
|
|
4
4
|
import { getUpdates } from "../../util/update.js";
|
|
5
5
|
import { DBProvider } from "./DBProvider.js";
|
|
@@ -8,7 +8,7 @@ import { DBProvider } from "./DBProvider.js";
|
|
|
8
8
|
*
|
|
9
9
|
* - Subclasses implement `exec()` to run a query against a concrete database (e.g. SQLite, PostgreSQL).
|
|
10
10
|
* - The `sql*` helpers build composable `SQLFragment` objects so dialect differences can be overridden.
|
|
11
|
-
* - Realtime subscriptions are unsupported and throw `
|
|
11
|
+
* - Realtime subscriptions are unsupported and throw `UnsupportedError`.
|
|
12
12
|
*
|
|
13
13
|
* @see https://shelving.cc/db/SQLProvider
|
|
14
14
|
*/
|
|
@@ -21,9 +21,9 @@ export class SQLProvider extends DBProvider {
|
|
|
21
21
|
`;
|
|
22
22
|
return rows[0];
|
|
23
23
|
}
|
|
24
|
-
/** Unsupported by SQL providers — always throws `
|
|
24
|
+
/** Unsupported by SQL providers — always throws `UnsupportedError`. */
|
|
25
25
|
getItemSequence(_collection, _id) {
|
|
26
|
-
throw new
|
|
26
|
+
throw new UnsupportedError(`SQLProvider does not support realtime subscriptions`);
|
|
27
27
|
}
|
|
28
28
|
/** Insert via `INSERT ... RETURNING`; throws `RequiredError` if no id comes back. */
|
|
29
29
|
async addItem(collection, data) {
|
|
@@ -65,9 +65,9 @@ export class SQLProvider extends DBProvider {
|
|
|
65
65
|
${query ? this.sqlClauses(query) : this.sql ``}
|
|
66
66
|
`;
|
|
67
67
|
}
|
|
68
|
-
/** Unsupported by SQL providers — always throws `
|
|
68
|
+
/** Unsupported by SQL providers — always throws `UnsupportedError`. */
|
|
69
69
|
getQuerySequence(_collection, _query) {
|
|
70
|
-
throw new
|
|
70
|
+
throw new UnsupportedError(`SQLProvider does not support realtime subscriptions`);
|
|
71
71
|
}
|
|
72
72
|
async setQuery(collection, query, data) {
|
|
73
73
|
await this.exec `UPDATE ${this.sqlIdentifier(collection.name)} SET ${this.sqlSetters(data)}${this.sqlClauses(query)}`;
|
|
@@ -104,13 +104,13 @@ export class SQLProvider extends DBProvider {
|
|
|
104
104
|
* - Base implementation only supports flat (single-segment) keys; subclasses override for nested JSON paths.
|
|
105
105
|
*
|
|
106
106
|
* @param key The key segments identifying the column (and any nested path).
|
|
107
|
-
* @throws `
|
|
107
|
+
* @throws `UnsupportedError` if the key is nested (multi-segment).
|
|
108
108
|
* @example this.sqlExtract(["name"]); // "name"
|
|
109
109
|
* @see https://shelving.cc/db/SQLProvider/sqlExtract
|
|
110
110
|
*/
|
|
111
111
|
sqlExtract(key) {
|
|
112
112
|
if (key.length > 1)
|
|
113
|
-
throw new
|
|
113
|
+
throw new UnsupportedError("SQLProvider does not support nested filter keys");
|
|
114
114
|
return this.sqlIdentifier(key[0]);
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
@@ -151,23 +151,23 @@ export class SQLProvider extends DBProvider {
|
|
|
151
151
|
/**
|
|
152
152
|
* Define an SQL fragment for a single update action.
|
|
153
153
|
* - Handles flat `set` and `sum` only (single-segment key).
|
|
154
|
-
* - Nested keys (multi-segment) and `with`/`omit` actions throw `
|
|
154
|
+
* - Nested keys (multi-segment) and `with`/`omit` actions throw `UnsupportedError`.
|
|
155
155
|
* - Subclasses should override to support nested keys and array mutation actions.
|
|
156
156
|
*
|
|
157
157
|
* @param update The update action (`action`, `key`, `value`) to convert.
|
|
158
|
-
* @throws `
|
|
158
|
+
* @throws `UnsupportedError` if the key is nested or the action is unsupported.
|
|
159
159
|
* @example this.sqlUpdate({ action: "set", key: ["a"], value: 1 }); // "a" = 1
|
|
160
160
|
* @see https://shelving.cc/db/SQLProvider/sqlUpdate
|
|
161
161
|
*/
|
|
162
162
|
sqlUpdate({ action, key, value }) {
|
|
163
163
|
if (key.length > 1)
|
|
164
|
-
throw new
|
|
164
|
+
throw new UnsupportedError("SQLProvider does not support nested update keys");
|
|
165
165
|
const column = this.sqlIdentifier(key[0]);
|
|
166
166
|
if (action === "set")
|
|
167
167
|
return this.sql `${column} = ${value}`;
|
|
168
168
|
if (action === "sum")
|
|
169
169
|
return this.sql `${column} = ${column} + ${value}`;
|
|
170
|
-
throw new
|
|
170
|
+
throw new UnsupportedError(`SQLProvider does not support "${action}" updates`);
|
|
171
171
|
}
|
|
172
172
|
/**
|
|
173
173
|
* Define an SQL fragment for `VALUES` syntax, e.g. `("a", "b") VALUES (1, 2)`.
|
|
@@ -211,7 +211,7 @@ export class SQLProvider extends DBProvider {
|
|
|
211
211
|
* Define an SQL fragment for a single filter clause on a column, e.g. `x = 1` or `x IN (1, 2)`.
|
|
212
212
|
*
|
|
213
213
|
* @param filter The filter (`key`, `operator`, `value`) to translate.
|
|
214
|
-
* @throws `
|
|
214
|
+
* @throws `UnsupportedError` if the operator is unsupported.
|
|
215
215
|
* @example this.sqlFilter({ key: ["x"], operator: "is", value: 1 }); // "x" = 1
|
|
216
216
|
* @see https://shelving.cc/db/SQLProvider/sqlFilter
|
|
217
217
|
*/
|
|
@@ -239,15 +239,15 @@ export class SQLProvider extends DBProvider {
|
|
|
239
239
|
return this.sql `${path} > ${value}`;
|
|
240
240
|
if (operator === "gte")
|
|
241
241
|
return this.sql `${path} >= ${value}`;
|
|
242
|
-
throw new
|
|
242
|
+
throw new UnsupportedError(`SQLProvider does not support "${operator}" filters`);
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
245
|
* Define an SQL fragment for an `ORDER BY` clause, e.g. ` ORDER BY "a" ASC, "b" DESC`.
|
|
246
246
|
* - Returns an empty fragment when the query has no orders.
|
|
247
|
-
* - Nested keys (multi-segment) throw `
|
|
247
|
+
* - Nested keys (multi-segment) throw `UnsupportedError`.
|
|
248
248
|
*
|
|
249
249
|
* @param query The query whose orders become the `ORDER BY` clause.
|
|
250
|
-
* @throws `
|
|
250
|
+
* @throws `UnsupportedError` if an order key is nested (multi-segment).
|
|
251
251
|
* @example this.sqlOrder(query); // ORDER BY "a" ASC
|
|
252
252
|
* @see https://shelving.cc/db/SQLProvider/sqlOrder
|
|
253
253
|
*/
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Data } from "../../util/data.js";
|
|
2
|
+
import type { Identifier, Item } from "../../util/item.js";
|
|
3
|
+
import type { Collection } from "../collection/Collection.js";
|
|
4
|
+
import { MemoryDBProvider, MemoryTable } from "./MemoryDBProvider.js";
|
|
5
|
+
/**
|
|
6
|
+
* In-memory database provider that persists every collection to a `Storage` — `localStorage`, `sessionStorage`, or anything else with the same interface.
|
|
7
|
+
*
|
|
8
|
+
* - Extends `MemoryDBProvider`, so all reads, queries, and realtime sequences are served synchronously from memory, and it can seed `ItemStore` / `QueryStore` and act as the cache inside `CacheDBProvider`. The only difference is that collections are backed by `StorageTable` instead of `MemoryTable`.
|
|
9
|
+
* - The storage is a required argument (there is no default), so server code that constructs this provider must reference `localStorage` / `sessionStorage` itself — surfacing the mistake at the callsite instead of deep inside this class.
|
|
10
|
+
* - Treat the persisted data as best-effort: quota is shared across the origin (typically ~5MB) and users can clear it at any time. Data read back from storage is unvalidated — wrap this provider in `ValidationDBProvider` if it may have been written by an older version of your app.
|
|
11
|
+
* - If the storage is unusable (writes blocked by browser settings, or private browsing with zero quota), the provider degrades to memory-only operation — check the `persistent` flag to warn users their changes won't be saved.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const provider = new StorageDBProvider(localStorage);
|
|
15
|
+
* if (!provider.persistent) console.warn("Changes won't be saved on this device.");
|
|
16
|
+
* const id = await provider.addItem(users, { name: "Dave" });
|
|
17
|
+
*
|
|
18
|
+
* @see https://shelving.cc/db/StorageDBProvider
|
|
19
|
+
*/
|
|
20
|
+
export declare class StorageDBProvider<I extends Identifier = Identifier, T extends Data = Data> extends MemoryDBProvider<I, T> {
|
|
21
|
+
/**
|
|
22
|
+
* Prefix for every storage key written by this provider.
|
|
23
|
+
*
|
|
24
|
+
* @see https://shelving.cc/db/StorageDBProvider/prefix
|
|
25
|
+
*/
|
|
26
|
+
readonly prefix: string;
|
|
27
|
+
/**
|
|
28
|
+
* Whether this provider is actually persisting to storage.
|
|
29
|
+
* - `false` when storage exists but is unusable (e.g. blocked by browser settings, or private browsing with zero quota) — the provider still works, but data only lives in memory and is lost when the page closes.
|
|
30
|
+
*
|
|
31
|
+
* @see https://shelving.cc/db/StorageDBProvider/persistent
|
|
32
|
+
*/
|
|
33
|
+
readonly persistent: boolean;
|
|
34
|
+
/** Storage being persisted to, or `undefined` when operating memory-only. */
|
|
35
|
+
private readonly _storage;
|
|
36
|
+
/**
|
|
37
|
+
* @param storage `Storage` instance to persist to, e.g. `localStorage` or `sessionStorage` (required so environments without one, e.g. the server, fail at the callsite).
|
|
38
|
+
* @param prefix Prefix for every storage key written by this provider, so it can share an origin's storage with other code (defaults to `"shelving:"`).
|
|
39
|
+
*/
|
|
40
|
+
constructor(storage: Storage, prefix?: string);
|
|
41
|
+
/** Back collections with a `StorageTable`, or a plain `MemoryTable` when operating memory-only. */
|
|
42
|
+
createTable<II extends I, TT extends T>(collection: Collection<string, II, TT>): MemoryTable<II, TT>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `MemoryTable` that persists its items to a `Storage` instance — self-contained, so it can also be used independently of `StorageDBProvider`.
|
|
46
|
+
*
|
|
47
|
+
* - Hydrates itself from storage on construction, then persists each write *before* applying it to memory, so a failed write (e.g. `QuotaExceededError` when the origin's quota is full) throws and leaves memory and storage consistent.
|
|
48
|
+
* - Items are stored as JSON under keys formatted as `{prefix}{collection}:{id}`, so values must be JSON-serializable. The id in the key is authoritative — an `id` stored inside the JSON is overridden.
|
|
49
|
+
* - Listens for `storage` events, so changes made in other tabs/windows update memory and notify realtime sequences. Dispose the table (or its provider) to remove the listener.
|
|
50
|
+
*
|
|
51
|
+
* @see https://shelving.cc/db/StorageTable
|
|
52
|
+
*/
|
|
53
|
+
export declare class StorageTable<I extends Identifier, T extends Data> extends MemoryTable<I, T> {
|
|
54
|
+
/** Prefix for every storage key belonging to this table, e.g. `"shelving:users:"`. */
|
|
55
|
+
private readonly _prefix;
|
|
56
|
+
/** Storage this table persists to. */
|
|
57
|
+
private readonly _storage;
|
|
58
|
+
/** Attached `storage` event listener (so it can be detached on dispose), or `undefined` if none was attached. */
|
|
59
|
+
private readonly _listener;
|
|
60
|
+
constructor(collection: Collection<string, I, T>, storage: Storage, prefix?: string);
|
|
61
|
+
/** Load every persisted item of this table's collection into memory (directly, skipping the persist-back in `setItem()`). */
|
|
62
|
+
private _hydrate;
|
|
63
|
+
/** Storage key for an item of this table. */
|
|
64
|
+
private _key;
|
|
65
|
+
/** Identifier encoded in a storage key of this table. */
|
|
66
|
+
private _decodeKey;
|
|
67
|
+
/** Parse a stored JSON value into an item with the given id, or `undefined` if it's malformed. */
|
|
68
|
+
private _parse;
|
|
69
|
+
/**
|
|
70
|
+
* Apply a `storage` event (a change made in another tab/window) to this table.
|
|
71
|
+
* - Reuses `setItem()` / `deleteItem()` — re-persisting the value already in storage is a no-op, and same-tab writes don't fire `storage` events, so this can't loop.
|
|
72
|
+
*/
|
|
73
|
+
private _syncStorage;
|
|
74
|
+
/** Persist to storage first — a failure (e.g. `QuotaExceededError`) throws before memory changes, keeping the two consistent. */
|
|
75
|
+
setItem(id: I, data: Item<I, T> | T): void;
|
|
76
|
+
/** Remove from storage first, keeping storage and memory consistent. */
|
|
77
|
+
deleteItem(id: I): void;
|
|
78
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
79
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { UnsupportedError } from "../../error/UnsupportedError.js";
|
|
2
|
+
import { StringSchema } from "../../schema/StringSchema.js";
|
|
3
|
+
import { isData } from "../../util/data.js";
|
|
4
|
+
import { awaitDispose } from "../../util/dispose.js";
|
|
5
|
+
import { getItem } from "../../util/item.js";
|
|
6
|
+
import { MemoryDBProvider, MemoryTable } from "./MemoryDBProvider.js";
|
|
7
|
+
/**
|
|
8
|
+
* In-memory database provider that persists every collection to a `Storage` — `localStorage`, `sessionStorage`, or anything else with the same interface.
|
|
9
|
+
*
|
|
10
|
+
* - Extends `MemoryDBProvider`, so all reads, queries, and realtime sequences are served synchronously from memory, and it can seed `ItemStore` / `QueryStore` and act as the cache inside `CacheDBProvider`. The only difference is that collections are backed by `StorageTable` instead of `MemoryTable`.
|
|
11
|
+
* - The storage is a required argument (there is no default), so server code that constructs this provider must reference `localStorage` / `sessionStorage` itself — surfacing the mistake at the callsite instead of deep inside this class.
|
|
12
|
+
* - Treat the persisted data as best-effort: quota is shared across the origin (typically ~5MB) and users can clear it at any time. Data read back from storage is unvalidated — wrap this provider in `ValidationDBProvider` if it may have been written by an older version of your app.
|
|
13
|
+
* - If the storage is unusable (writes blocked by browser settings, or private browsing with zero quota), the provider degrades to memory-only operation — check the `persistent` flag to warn users their changes won't be saved.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const provider = new StorageDBProvider(localStorage);
|
|
17
|
+
* if (!provider.persistent) console.warn("Changes won't be saved on this device.");
|
|
18
|
+
* const id = await provider.addItem(users, { name: "Dave" });
|
|
19
|
+
*
|
|
20
|
+
* @see https://shelving.cc/db/StorageDBProvider
|
|
21
|
+
*/
|
|
22
|
+
export class StorageDBProvider extends MemoryDBProvider {
|
|
23
|
+
/**
|
|
24
|
+
* Prefix for every storage key written by this provider.
|
|
25
|
+
*
|
|
26
|
+
* @see https://shelving.cc/db/StorageDBProvider/prefix
|
|
27
|
+
*/
|
|
28
|
+
prefix;
|
|
29
|
+
/**
|
|
30
|
+
* Whether this provider is actually persisting to storage.
|
|
31
|
+
* - `false` when storage exists but is unusable (e.g. blocked by browser settings, or private browsing with zero quota) — the provider still works, but data only lives in memory and is lost when the page closes.
|
|
32
|
+
*
|
|
33
|
+
* @see https://shelving.cc/db/StorageDBProvider/persistent
|
|
34
|
+
*/
|
|
35
|
+
persistent;
|
|
36
|
+
/** Storage being persisted to, or `undefined` when operating memory-only. */
|
|
37
|
+
_storage;
|
|
38
|
+
/**
|
|
39
|
+
* @param storage `Storage` instance to persist to, e.g. `localStorage` or `sessionStorage` (required so environments without one, e.g. the server, fail at the callsite).
|
|
40
|
+
* @param prefix Prefix for every storage key written by this provider, so it can share an origin's storage with other code (defaults to `"shelving:"`).
|
|
41
|
+
*/
|
|
42
|
+
constructor(storage, prefix = "shelving:") {
|
|
43
|
+
super();
|
|
44
|
+
this.prefix = prefix;
|
|
45
|
+
if (!storage)
|
|
46
|
+
throw new UnsupportedError("StorageDBProvider requires a Storage, e.g. localStorage or sessionStorage", {
|
|
47
|
+
caller: StorageDBProvider,
|
|
48
|
+
});
|
|
49
|
+
// Probe with a write+remove: private browsing can expose a `Storage` whose every write throws.
|
|
50
|
+
try {
|
|
51
|
+
const probe = `${prefix}?`;
|
|
52
|
+
storage.setItem(probe, "?");
|
|
53
|
+
storage.removeItem(probe);
|
|
54
|
+
this.persistent = true;
|
|
55
|
+
this._storage = storage;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Unusable storage — degrade to memory-only.
|
|
59
|
+
this.persistent = false;
|
|
60
|
+
this._storage = undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Back collections with a `StorageTable`, or a plain `MemoryTable` when operating memory-only. */
|
|
64
|
+
createTable(collection) {
|
|
65
|
+
return this._storage ? new StorageTable(collection, this._storage, this.prefix) : super.createTable(collection);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `MemoryTable` that persists its items to a `Storage` instance — self-contained, so it can also be used independently of `StorageDBProvider`.
|
|
70
|
+
*
|
|
71
|
+
* - Hydrates itself from storage on construction, then persists each write *before* applying it to memory, so a failed write (e.g. `QuotaExceededError` when the origin's quota is full) throws and leaves memory and storage consistent.
|
|
72
|
+
* - Items are stored as JSON under keys formatted as `{prefix}{collection}:{id}`, so values must be JSON-serializable. The id in the key is authoritative — an `id` stored inside the JSON is overridden.
|
|
73
|
+
* - Listens for `storage` events, so changes made in other tabs/windows update memory and notify realtime sequences. Dispose the table (or its provider) to remove the listener.
|
|
74
|
+
*
|
|
75
|
+
* @see https://shelving.cc/db/StorageTable
|
|
76
|
+
*/
|
|
77
|
+
export class StorageTable extends MemoryTable {
|
|
78
|
+
/** Prefix for every storage key belonging to this table, e.g. `"shelving:users:"`. */
|
|
79
|
+
_prefix;
|
|
80
|
+
/** Storage this table persists to. */
|
|
81
|
+
_storage;
|
|
82
|
+
/** Attached `storage` event listener (so it can be detached on dispose), or `undefined` if none was attached. */
|
|
83
|
+
_listener;
|
|
84
|
+
constructor(collection, storage, prefix = "shelving:") {
|
|
85
|
+
super(collection);
|
|
86
|
+
this._storage = storage;
|
|
87
|
+
this._prefix = `${prefix}${collection.name}:`;
|
|
88
|
+
this._hydrate();
|
|
89
|
+
// Changes made in other tabs/windows arrive as `storage` events on the global scope.
|
|
90
|
+
if (typeof globalThis.addEventListener === "function") {
|
|
91
|
+
this._listener = event => this._syncStorage(event);
|
|
92
|
+
globalThis.addEventListener("storage", this._listener);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Load every persisted item of this table's collection into memory (directly, skipping the persist-back in `setItem()`). */
|
|
96
|
+
_hydrate() {
|
|
97
|
+
for (let i = 0; i < this._storage.length; i++) {
|
|
98
|
+
const key = this._storage.key(i);
|
|
99
|
+
if (key?.startsWith(this._prefix)) {
|
|
100
|
+
const id = this._decodeKey(key);
|
|
101
|
+
const item = this._parse(id, this._storage.getItem(key));
|
|
102
|
+
if (item)
|
|
103
|
+
this._data.set(id, item);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Storage key for an item of this table. */
|
|
108
|
+
_key(id) {
|
|
109
|
+
return `${this._prefix}${id}`;
|
|
110
|
+
}
|
|
111
|
+
/** Identifier encoded in a storage key of this table. */
|
|
112
|
+
_decodeKey(key) {
|
|
113
|
+
const raw = key.slice(this._prefix.length);
|
|
114
|
+
return (this.collection.id instanceof StringSchema ? raw : Number(raw)); // `as I` needed: TypeScript can't narrow I from the schema check.
|
|
115
|
+
}
|
|
116
|
+
/** Parse a stored JSON value into an item with the given id, or `undefined` if it's malformed. */
|
|
117
|
+
_parse(id, value) {
|
|
118
|
+
if (value) {
|
|
119
|
+
try {
|
|
120
|
+
const data = JSON.parse(value);
|
|
121
|
+
if (isData(data))
|
|
122
|
+
return getItem(id, data); // `as T` needed: stored values are unvalidated — wrap the provider in `ValidationDBProvider` to validate them.
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Malformed values are skipped, not deleted — they may belong to other code sharing the prefix.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Apply a `storage` event (a change made in another tab/window) to this table.
|
|
131
|
+
* - Reuses `setItem()` / `deleteItem()` — re-persisting the value already in storage is a no-op, and same-tab writes don't fire `storage` events, so this can't loop.
|
|
132
|
+
*/
|
|
133
|
+
_syncStorage({ storageArea, key, newValue }) {
|
|
134
|
+
if (storageArea !== this._storage)
|
|
135
|
+
return;
|
|
136
|
+
if (key === null) {
|
|
137
|
+
this.deleteQuery({}); // A `null` key means the other tab called `storage.clear()`.
|
|
138
|
+
}
|
|
139
|
+
else if (key.startsWith(this._prefix)) {
|
|
140
|
+
const id = this._decodeKey(key);
|
|
141
|
+
const item = newValue !== null ? this._parse(id, newValue) : undefined;
|
|
142
|
+
item ? this.setItem(id, item) : this.deleteItem(id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Persist to storage first — a failure (e.g. `QuotaExceededError`) throws before memory changes, keeping the two consistent. */
|
|
146
|
+
setItem(id, data) {
|
|
147
|
+
const item = getItem(id, data);
|
|
148
|
+
if (this._data.get(id) !== item)
|
|
149
|
+
this._storage.setItem(this._key(id), JSON.stringify(item));
|
|
150
|
+
super.setItem(id, item);
|
|
151
|
+
}
|
|
152
|
+
/** Remove from storage first, keeping storage and memory consistent. */
|
|
153
|
+
deleteItem(id) {
|
|
154
|
+
if (this._data.has(id))
|
|
155
|
+
this._storage.removeItem(this._key(id));
|
|
156
|
+
super.deleteItem(id);
|
|
157
|
+
}
|
|
158
|
+
// Implement `AsyncDisposable`
|
|
159
|
+
async [Symbol.asyncDispose]() {
|
|
160
|
+
await awaitDispose(() => this._listener && globalThis.removeEventListener("storage", this._listener), // Stop listening for `storage` events.
|
|
161
|
+
super[Symbol.asyncDispose]());
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type BaseError, type BaseErrorOptions } from "./BaseError.js";
|
|
2
2
|
/**
|
|
3
|
-
* Error thrown when functionality is called but is not
|
|
3
|
+
* Error thrown when functionality is called but is not supported — by a backend, an environment, or an implementation.
|
|
4
4
|
*
|
|
5
|
-
* @see https://shelving.cc/error/
|
|
5
|
+
* @see https://shelving.cc/error/UnsupportedError
|
|
6
6
|
*/
|
|
7
|
-
export declare class
|
|
7
|
+
export declare class UnsupportedError extends Error implements BaseError {
|
|
8
8
|
/** Provide additional named contextual data that is relevant to the `Error` instance. */
|
|
9
9
|
readonly [key: string]: unknown;
|
|
10
|
-
/** @param message Optional human-readable description of the
|
|
10
|
+
/** @param message Optional human-readable description of the unsupported functionality. */
|
|
11
11
|
constructor(message?: string, options?: BaseErrorOptions);
|
|
12
12
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { setBaseErrorOptions } from "./BaseError.js";
|
|
2
|
+
/**
|
|
3
|
+
* Error thrown when functionality is called but is not supported — by a backend, an environment, or an implementation.
|
|
4
|
+
*
|
|
5
|
+
* @see https://shelving.cc/error/UnsupportedError
|
|
6
|
+
*/
|
|
7
|
+
export class UnsupportedError extends Error {
|
|
8
|
+
/** @param message Optional human-readable description of the unsupported functionality. */
|
|
9
|
+
constructor(message, options = {}) {
|
|
10
|
+
super(message, options);
|
|
11
|
+
setBaseErrorOptions(UnsupportedError, this, options);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
UnsupportedError.prototype.name = "UnsupportedError";
|
package/error/index.d.ts
CHANGED
package/error/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import type { Updates } from "../../util/update.js";
|
|
|
10
10
|
*
|
|
11
11
|
* - Works with the Firebase JS SDK via `firebase/firestore/lite`, which keeps bundle size small.
|
|
12
12
|
* - Does not support offline mode.
|
|
13
|
-
* - Does not support realtime subscriptions: `getItemSequence()` and `getQuerySequence()` throw `
|
|
13
|
+
* - Does not support realtime subscriptions: `getItemSequence()` and `getQuerySequence()` throw `UnsupportedError`.
|
|
14
14
|
*
|
|
15
15
|
* @see https://shelving.cc/firestore/lite/FirestoreLiteProvider
|
|
16
16
|
*/
|
|
@@ -24,7 +24,7 @@ export declare class FirestoreLiteProvider<I extends string = string, T extends
|
|
|
24
24
|
/** Get a Firestore QueryReference for a given query. */
|
|
25
25
|
private _query;
|
|
26
26
|
getItem<II extends I, TT extends T>(c: Collection<string, II, TT>, id: II): Promise<OptionalItem<II, TT>>;
|
|
27
|
-
/** Not supported — the Firebase Lite SDK has no realtime listeners, so this throws `
|
|
27
|
+
/** Not supported — the Firebase Lite SDK has no realtime listeners, so this throws `UnsupportedError`. */
|
|
28
28
|
getItemSequence<II extends I, TT extends T>(_c: Collection<string, II, TT>, _id: II): OptionalItemSequence<II, TT>;
|
|
29
29
|
addItem<II extends I, TT extends T>(c: Collection<string, II, TT>, data: TT): Promise<II>;
|
|
30
30
|
setItem<II extends I, TT extends T>(c: Collection<string, II, TT>, id: II, data: TT): Promise<void>;
|
|
@@ -32,7 +32,7 @@ export declare class FirestoreLiteProvider<I extends string = string, T extends
|
|
|
32
32
|
deleteItem<II extends I, TT extends T>(c: Collection<string, II, TT>, id: II): Promise<void>;
|
|
33
33
|
countQuery<II extends I, TT extends T>(c: Collection<string, II, TT>, q?: Query<Item<II, TT>>): Promise<number>;
|
|
34
34
|
getQuery<II extends I, TT extends T>(c: Collection<string, II, TT>, q?: Query<Item<II, TT>>): Promise<Items<II, TT>>;
|
|
35
|
-
/** Not supported — the Firebase Lite SDK has no realtime listeners, so this throws `
|
|
35
|
+
/** Not supported — the Firebase Lite SDK has no realtime listeners, so this throws `UnsupportedError`. */
|
|
36
36
|
getQuerySequence<II extends I, TT extends T>(_c: Collection<string, II, TT>, _q?: Query<Item<II, TT>>): ItemsSequence<II, TT>;
|
|
37
37
|
setQuery<II extends I, TT extends T>(c: Collection<string, II, TT>, q: Query<Item<II, TT>>, data: TT): Promise<void>;
|
|
38
38
|
updateQuery<II extends I, TT extends T>(c: Collection<string, II, TT>, q: Query<Item<II, TT>>, updates: Updates<TT>): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { addDoc, arrayRemove, arrayUnion, collection, deleteDoc, doc, documentId, getCount, getDoc, getDocs, increment, limit, orderBy, query, setDoc, updateDoc, where, } from "firebase/firestore/lite";
|
|
2
2
|
import { DBProvider } from "../../db/provider/DBProvider.js";
|
|
3
|
-
import {
|
|
3
|
+
import { UnsupportedError } from "../../error/UnsupportedError.js";
|
|
4
4
|
import { joinDataPath } from "../../util/data.js";
|
|
5
5
|
import { getItem } from "../../util/item.js";
|
|
6
6
|
import { getObject } from "../../util/object.js";
|
|
@@ -66,7 +66,7 @@ function _getFieldValue({ key, action, value }) {
|
|
|
66
66
|
*
|
|
67
67
|
* - Works with the Firebase JS SDK via `firebase/firestore/lite`, which keeps bundle size small.
|
|
68
68
|
* - Does not support offline mode.
|
|
69
|
-
* - Does not support realtime subscriptions: `getItemSequence()` and `getQuerySequence()` throw `
|
|
69
|
+
* - Does not support realtime subscriptions: `getItemSequence()` and `getQuerySequence()` throw `UnsupportedError`.
|
|
70
70
|
*
|
|
71
71
|
* @see https://shelving.cc/firestore/lite/FirestoreLiteProvider
|
|
72
72
|
*/
|
|
@@ -92,9 +92,9 @@ export class FirestoreLiteProvider extends DBProvider {
|
|
|
92
92
|
const snapshot = await getDoc(this._doc(c, id));
|
|
93
93
|
return _getOptionalItem(snapshot);
|
|
94
94
|
}
|
|
95
|
-
/** Not supported — the Firebase Lite SDK has no realtime listeners, so this throws `
|
|
95
|
+
/** Not supported — the Firebase Lite SDK has no realtime listeners, so this throws `UnsupportedError`. */
|
|
96
96
|
getItemSequence(_c, _id) {
|
|
97
|
-
throw new
|
|
97
|
+
throw new UnsupportedError("FirestoreLiteProvider does not support realtime subscriptions");
|
|
98
98
|
}
|
|
99
99
|
async addItem(c, data) {
|
|
100
100
|
const reference = await addDoc(this._collection(c), data);
|
|
@@ -116,9 +116,9 @@ export class FirestoreLiteProvider extends DBProvider {
|
|
|
116
116
|
async getQuery(c, q) {
|
|
117
117
|
return _getItems(await getDocs(this._query(c, q)));
|
|
118
118
|
}
|
|
119
|
-
/** Not supported — the Firebase Lite SDK has no realtime listeners, so this throws `
|
|
119
|
+
/** Not supported — the Firebase Lite SDK has no realtime listeners, so this throws `UnsupportedError`. */
|
|
120
120
|
getQuerySequence(_c, _q) {
|
|
121
|
-
throw new
|
|
121
|
+
throw new UnsupportedError("FirestoreLiteProvider does not support realtime subscriptions");
|
|
122
122
|
}
|
|
123
123
|
async setQuery(c, q, data) {
|
|
124
124
|
const snapshot = await getDocs(this._query(c, q));
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { setBaseErrorOptions } from "./BaseError.js";
|
|
2
|
-
/**
|
|
3
|
-
* Error thrown when functionality is called but is not implemented by an interface.
|
|
4
|
-
*
|
|
5
|
-
* @see https://shelving.cc/error/UnimplementedError
|
|
6
|
-
*/
|
|
7
|
-
export class UnimplementedError extends Error {
|
|
8
|
-
/** @param message Optional human-readable description of the missing implementation. */
|
|
9
|
-
constructor(message, options = {}) {
|
|
10
|
-
super(message, options);
|
|
11
|
-
setBaseErrorOptions(UnimplementedError, this, options);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
UnimplementedError.prototype.name = "UnimplementedError";
|