@hozon/db 0.2.2 → 0.3.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/lib/batch-loader.d.ts +19 -0
- package/lib/batch-loader.js +84 -0
- package/lib/batch.d.ts +17 -0
- package/lib/batch.js +27 -0
- package/lib/db.d.ts +7 -1
- package/lib/db.js +73 -10
- package/lib/errors.d.ts +3 -0
- package/lib/errors.js +6 -0
- package/lib/index.d.ts +4 -2
- package/lib/index.js +3 -1
- package/lib/loader-registry.d.ts +11 -0
- package/lib/loader-registry.js +26 -0
- package/package.json +6 -5
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type BatchLoaderParams<TKey, TValue> = {
|
|
2
|
+
load: (keys: Array<TKey>) => Promise<Map<TKey, TValue | null>>;
|
|
3
|
+
maxBatchSize: number;
|
|
4
|
+
};
|
|
5
|
+
export type BatchLoader<TKey, TValue> = {
|
|
6
|
+
load(key: TKey): Promise<TValue | null>;
|
|
7
|
+
clear(key: TKey): void;
|
|
8
|
+
clearAll(): void;
|
|
9
|
+
dispatch(): void;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Coalesces same-tick `load` calls into batched `params.load` calls of at most `maxBatchSize`
|
|
13
|
+
* keys. `remember` is read at settle time so a scope can switch the loader to batch-only.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createBatchLoader<TKey, TValue>(params: BatchLoaderParams<TKey, TValue> & {
|
|
16
|
+
remember: () => boolean;
|
|
17
|
+
}): BatchLoader<TKey, TValue>;
|
|
18
|
+
/** Test probe: number of per-key generation entries a loader currently tracks. */
|
|
19
|
+
export declare function trackedGenerationCount(loader: object): number;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defer } from '@sozai/async';
|
|
2
|
+
import { chunk } from './keep-set.js';
|
|
3
|
+
// Runs after the microtask queue drains, so loads chained behind a few awaits still coalesce.
|
|
4
|
+
function scheduleAfterMicrotasks(fn) {
|
|
5
|
+
if (typeof setImmediate === 'function') setImmediate(fn);
|
|
6
|
+
else setTimeout(fn, 0);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Coalesces same-tick `load` calls into batched `params.load` calls of at most `maxBatchSize`
|
|
10
|
+
* keys. `remember` is read at settle time so a scope can switch the loader to batch-only.
|
|
11
|
+
*/ export function createBatchLoader(params) {
|
|
12
|
+
const { load, maxBatchSize, remember } = params;
|
|
13
|
+
let pending = new Map();
|
|
14
|
+
let scheduled = false;
|
|
15
|
+
const remembered = new Map();
|
|
16
|
+
// Both counters only grow, so their sum changes whenever either is bumped.
|
|
17
|
+
const keyGenerations = new Map();
|
|
18
|
+
let globalGeneration = 0;
|
|
19
|
+
// Per-key bumps only matter to in-flight chunks; dropping them when idle bounds the map.
|
|
20
|
+
let inFlight = 0;
|
|
21
|
+
const generationOf = (key)=>globalGeneration + (keyGenerations.get(key) ?? 0);
|
|
22
|
+
async function dispatchChunk(entries) {
|
|
23
|
+
const generations = entries.map(([key])=>generationOf(key));
|
|
24
|
+
inFlight++;
|
|
25
|
+
try {
|
|
26
|
+
const results = await load(entries.map(([key])=>key));
|
|
27
|
+
const shouldRemember = remember();
|
|
28
|
+
entries.forEach(([key, deferred], index)=>{
|
|
29
|
+
const value = results.get(key) ?? null;
|
|
30
|
+
if (shouldRemember && generationOf(key) === generations[index]) {
|
|
31
|
+
remembered.set(key, value);
|
|
32
|
+
}
|
|
33
|
+
deferred.resolve(value);
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
for (const [, deferred] of entries)deferred.reject(error);
|
|
37
|
+
} finally{
|
|
38
|
+
inFlight--;
|
|
39
|
+
if (inFlight === 0) keyGenerations.clear();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function dispatch() {
|
|
43
|
+
if (pending.size === 0) return;
|
|
44
|
+
const entries = [
|
|
45
|
+
...pending
|
|
46
|
+
];
|
|
47
|
+
pending = new Map();
|
|
48
|
+
for (const part of chunk(entries, maxBatchSize))void dispatchChunk(part);
|
|
49
|
+
}
|
|
50
|
+
const loader = {
|
|
51
|
+
load (key) {
|
|
52
|
+
if (remember() && remembered.has(key)) {
|
|
53
|
+
return Promise.resolve(remembered.get(key));
|
|
54
|
+
}
|
|
55
|
+
const existing = pending.get(key);
|
|
56
|
+
if (existing != null) return existing.promise;
|
|
57
|
+
const deferred = defer();
|
|
58
|
+
pending.set(key, deferred);
|
|
59
|
+
if (!scheduled) {
|
|
60
|
+
scheduled = true;
|
|
61
|
+
scheduleAfterMicrotasks(()=>{
|
|
62
|
+
scheduled = false;
|
|
63
|
+
dispatch();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return deferred.promise;
|
|
67
|
+
},
|
|
68
|
+
clear (key) {
|
|
69
|
+
if (inFlight > 0) keyGenerations.set(key, (keyGenerations.get(key) ?? 0) + 1);
|
|
70
|
+
remembered.delete(key);
|
|
71
|
+
},
|
|
72
|
+
clearAll () {
|
|
73
|
+
globalGeneration++;
|
|
74
|
+
remembered.clear();
|
|
75
|
+
},
|
|
76
|
+
dispatch
|
|
77
|
+
};
|
|
78
|
+
generationProbes.set(loader, ()=>keyGenerations.size);
|
|
79
|
+
return loader;
|
|
80
|
+
}
|
|
81
|
+
const generationProbes = new WeakMap();
|
|
82
|
+
/** Test probe: number of per-key generation entries a loader currently tracks. */ export function trackedGenerationCount(loader) {
|
|
83
|
+
return generationProbes.get(loader)?.() ?? 0;
|
|
84
|
+
}
|
package/lib/batch.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type BatchReadOptions = {
|
|
2
|
+
batchLimit?: number;
|
|
3
|
+
};
|
|
4
|
+
export type IterateBatchesOptions = {
|
|
5
|
+
batchSize: number;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
};
|
|
8
|
+
/** Throws if `size` exceeds `batchLimit ?? max`, or if `batchLimit` is invalid or above `max`. */
|
|
9
|
+
export declare function assertBatchSize(size: number, max: number, batchLimit?: number): void;
|
|
10
|
+
/**
|
|
11
|
+
* Dedupes `keys`, loads them in sequential chunks of `batchSize` and yields each chunk with
|
|
12
|
+
* its results. Checks `signal` before each chunk.
|
|
13
|
+
*/
|
|
14
|
+
export declare function iterateBatches<TKey, TValue>(keys: Iterable<TKey>, load: (chunk: Array<TKey>) => Promise<Map<TKey, TValue | null>>, options: IterateBatchesOptions): AsyncGenerator<{
|
|
15
|
+
keys: Array<TKey>;
|
|
16
|
+
results: Map<TKey, TValue | null>;
|
|
17
|
+
}>;
|
package/lib/batch.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BatchLimitError } from './errors.js';
|
|
2
|
+
import { chunk } from './keep-set.js';
|
|
3
|
+
/** Throws if `size` exceeds `batchLimit ?? max`, or if `batchLimit` is invalid or above `max`. */ export function assertBatchSize(size, max, batchLimit) {
|
|
4
|
+
if (batchLimit != null && (!Number.isInteger(batchLimit) || batchLimit <= 0 || batchLimit > max)) {
|
|
5
|
+
throw new RangeError(`batchLimit must be a positive integer no greater than ${max}`);
|
|
6
|
+
}
|
|
7
|
+
const limit = batchLimit ?? max;
|
|
8
|
+
if (size > limit) throw new BatchLimitError(size, limit);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Dedupes `keys`, loads them in sequential chunks of `batchSize` and yields each chunk with
|
|
12
|
+
* its results. Checks `signal` before each chunk.
|
|
13
|
+
*/ export async function* iterateBatches(keys, load, options) {
|
|
14
|
+
const { batchSize, signal } = options;
|
|
15
|
+
if (!Number.isInteger(batchSize) || batchSize <= 0) {
|
|
16
|
+
throw new RangeError('batchSize must be a positive integer');
|
|
17
|
+
}
|
|
18
|
+
for (const chunkKeys of chunk([
|
|
19
|
+
...new Set(keys)
|
|
20
|
+
], batchSize)){
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
yield {
|
|
23
|
+
keys: chunkKeys,
|
|
24
|
+
results: await load(chunkKeys)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
package/lib/db.d.ts
CHANGED
|
@@ -2,12 +2,16 @@ import type { Adapter, ColumnTypes, Functions } from '@hozon/adapter';
|
|
|
2
2
|
import { type Logger } from '@sozai/log';
|
|
3
3
|
import { Kysely } from 'kysely';
|
|
4
4
|
import { type Migration } from 'kysely/migration';
|
|
5
|
+
import type { BatchLoader, BatchLoaderParams } from './batch-loader.js';
|
|
5
6
|
export type MigrationContext = {
|
|
6
7
|
tablePrefix: string;
|
|
7
8
|
kind: Adapter['kind'];
|
|
8
9
|
types: ColumnTypes;
|
|
9
10
|
functions: Functions;
|
|
10
11
|
};
|
|
12
|
+
export type StoreContext = {
|
|
13
|
+
createLoader<TKey, TValue>(params: BatchLoaderParams<TKey, TValue>): BatchLoader<TKey, TValue>;
|
|
14
|
+
};
|
|
11
15
|
export type StoreDefinition<Tables, API> = {
|
|
12
16
|
name: string;
|
|
13
17
|
migrations: Record<string, Migration> | ((ctx: MigrationContext) => Record<string, Migration>);
|
|
@@ -18,7 +22,7 @@ export type StoreDefinition<Tables, API> = {
|
|
|
18
22
|
* tables are not namespaced: the store must keep them collision-free.
|
|
19
23
|
*/
|
|
20
24
|
unprefixedTables?: (name: string) => boolean;
|
|
21
|
-
createAPI: (db: Kysely<Tables>, adapter: Adapter) => API;
|
|
25
|
+
createAPI: (db: Kysely<Tables>, adapter: Adapter, context: StoreContext) => API;
|
|
22
26
|
};
|
|
23
27
|
export type StoreProvider<Stores extends Record<string, unknown> = Record<string, unknown>> = {
|
|
24
28
|
getStore<S extends keyof Stores & string>(name: S): Promise<Stores[S]>;
|
|
@@ -53,6 +57,7 @@ export type StoreProvider<Stores extends Record<string, unknown> = Record<string
|
|
|
53
57
|
* transaction or explicit savepoint.
|
|
54
58
|
*/
|
|
55
59
|
withTransaction<NestedStores extends Record<string, unknown>, R>(fn: (tx: StoreProvider<NestedStores>) => Promise<R>): Promise<R>;
|
|
60
|
+
withScope?<NestedStores extends Record<string, unknown>, R>(fn: (scope: StoreProvider<NestedStores>) => Promise<R>): Promise<R>;
|
|
56
61
|
/** Roll back only this unit of work when called inside a transaction. */
|
|
57
62
|
withSavepoint?<NestedStores extends Record<string, unknown>, R>(fn: (tx: StoreProvider<NestedStores>) => Promise<R>): Promise<R>;
|
|
58
63
|
};
|
|
@@ -70,6 +75,7 @@ export declare class HozonDB implements StoreProvider {
|
|
|
70
75
|
hasStore(name: string): boolean;
|
|
71
76
|
onCommit(fn: () => void): void;
|
|
72
77
|
onRollback(_fn: () => void): void;
|
|
78
|
+
withScope<Stores extends Record<string, unknown>, R>(fn: (scope: StoreProvider<Stores>) => Promise<R>): Promise<R>;
|
|
73
79
|
/**
|
|
74
80
|
* Runs `fn` inside a DB transaction and provides a scoped `StoreProvider`.
|
|
75
81
|
* Concurrent calls on the same `HozonDB` instance are supported — the
|
package/lib/db.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { getLogger } from '@sozai/log';
|
|
2
|
-
import { Kysely,
|
|
2
|
+
import { Kysely, sql } from 'kysely';
|
|
3
3
|
import { Migrator } from 'kysely/migration';
|
|
4
4
|
import { withTransactionalDDL } from './dialect.js';
|
|
5
5
|
import { HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError } from './errors.js';
|
|
6
|
+
import { LoaderRegistry } from './loader-registry.js';
|
|
6
7
|
import { checkStore } from './preflight.js';
|
|
7
8
|
import { TablePrefixPlugin } from './table-prefix.js';
|
|
8
9
|
export class HozonDB {
|
|
@@ -19,6 +20,9 @@ export class HozonDB {
|
|
|
19
20
|
#storePlugins = new Map();
|
|
20
21
|
#logger;
|
|
21
22
|
#migrated = new Map();
|
|
23
|
+
#loaders = new LoaderRegistry({
|
|
24
|
+
remember: false
|
|
25
|
+
});
|
|
22
26
|
#storeAPIs = new Map();
|
|
23
27
|
#stores = new Map();
|
|
24
28
|
constructor(params){
|
|
@@ -30,10 +34,7 @@ export class HozonDB {
|
|
|
30
34
|
this.#adapter = params.adapter;
|
|
31
35
|
this.#db = new Kysely({
|
|
32
36
|
// SQLite supports transactional DDL, but Kysely's SQLite adapters do not report it.
|
|
33
|
-
dialect: this.#adapter.kind === 'sqlite' ? withTransactionalDDL(this.#adapter.dialect) : this.#adapter.dialect
|
|
34
|
-
plugins: [
|
|
35
|
-
new ParseJSONResultsPlugin()
|
|
36
|
-
]
|
|
37
|
+
dialect: this.#adapter.kind === 'sqlite' ? withTransactionalDDL(this.#adapter.dialect) : this.#adapter.dialect
|
|
37
38
|
});
|
|
38
39
|
this.#storeDB = this.#db.withPlugin(this.#defaultPlugin);
|
|
39
40
|
this.#logger = params.logger ?? getLogger([
|
|
@@ -66,8 +67,7 @@ export class HozonDB {
|
|
|
66
67
|
if (store == null) {
|
|
67
68
|
throw new Error(`Store "${name}" is not registered`);
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
this.#storeAPIs.set(name, store.createAPI(storeDB, this.#adapter));
|
|
70
|
+
this.#storeAPIs.set(name, this.#createStoreAPI(store, this.#db, this.#storeDB, this.#loaders));
|
|
71
71
|
}
|
|
72
72
|
return this.#storeAPIs.get(name);
|
|
73
73
|
}
|
|
@@ -79,6 +79,41 @@ export class HozonDB {
|
|
|
79
79
|
}
|
|
80
80
|
// Each standalone op auto-commits, so there is nothing to roll back — no-op.
|
|
81
81
|
onRollback(_fn) {}
|
|
82
|
+
async withScope(fn) {
|
|
83
|
+
this.#assertOpen();
|
|
84
|
+
const registry = new LoaderRegistry({
|
|
85
|
+
remember: true
|
|
86
|
+
});
|
|
87
|
+
const storeAPIs = new Map();
|
|
88
|
+
const provider = {
|
|
89
|
+
getStore: async (name)=>{
|
|
90
|
+
this.#assertOpen();
|
|
91
|
+
await this.#ensurePreflight();
|
|
92
|
+
await this.#ensureMigrated(name);
|
|
93
|
+
this.#assertOpen();
|
|
94
|
+
if (!storeAPIs.has(name)) {
|
|
95
|
+
const store = this.#stores.get(name);
|
|
96
|
+
if (store == null) throw new Error(`Store "${name}" is not registered`);
|
|
97
|
+
storeAPIs.set(name, this.#createStoreAPI(store, this.#db, this.#storeDB, registry));
|
|
98
|
+
}
|
|
99
|
+
return storeAPIs.get(name);
|
|
100
|
+
},
|
|
101
|
+
hasStore: (name)=>this.hasStore(name),
|
|
102
|
+
onCommit: (hook)=>hook(),
|
|
103
|
+
onRollback: ()=>{},
|
|
104
|
+
withScope: async (nestedFn)=>{
|
|
105
|
+
return await nestedFn(provider);
|
|
106
|
+
},
|
|
107
|
+
withTransaction: async (nestedFn)=>{
|
|
108
|
+
return await this.#withTransaction(nestedFn, ()=>registry.clearAll());
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
return await fn(provider);
|
|
113
|
+
} finally{
|
|
114
|
+
registry.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
82
117
|
/**
|
|
83
118
|
* Runs `fn` inside a DB transaction and provides a scoped `StoreProvider`.
|
|
84
119
|
* Concurrent calls on the same `HozonDB` instance are supported — the
|
|
@@ -88,6 +123,9 @@ export class HozonDB {
|
|
|
88
123
|
* `db.withTransaction` on the same instance inside the callback opens a
|
|
89
124
|
* new independent transaction (no atomic nesting) and is a bug.
|
|
90
125
|
*/ async withTransaction(fn) {
|
|
126
|
+
return await this.#withTransaction(fn);
|
|
127
|
+
}
|
|
128
|
+
async #withTransaction(fn, beforeHooks) {
|
|
91
129
|
// Migrate every registered store before opening the transaction. Stores
|
|
92
130
|
// are internal and known up front, so a broken migration should fail fast
|
|
93
131
|
// at startup rather than leave a runtime partition where one store's
|
|
@@ -96,6 +134,9 @@ export class HozonDB {
|
|
|
96
134
|
this.#assertOpen();
|
|
97
135
|
const commitHooks = [];
|
|
98
136
|
const rollbackHooks = [];
|
|
137
|
+
const registry = new LoaderRegistry({
|
|
138
|
+
remember: true
|
|
139
|
+
});
|
|
99
140
|
let result;
|
|
100
141
|
let savepointID = 0;
|
|
101
142
|
try {
|
|
@@ -116,8 +157,7 @@ export class HozonDB {
|
|
|
116
157
|
if (store == null) {
|
|
117
158
|
throw new Error(`Store "${name}" is not registered`);
|
|
118
159
|
}
|
|
119
|
-
|
|
120
|
-
txStoreAPIs.set(name, store.createAPI(storeDB, this.#adapter));
|
|
160
|
+
txStoreAPIs.set(name, this.#createStoreAPI(store, txKysely, txStoreDB, registry));
|
|
121
161
|
}
|
|
122
162
|
return txStoreAPIs.get(name);
|
|
123
163
|
},
|
|
@@ -128,6 +168,9 @@ export class HozonDB {
|
|
|
128
168
|
onRollback: (hook)=>{
|
|
129
169
|
scopedRollbackHooks.push(hook);
|
|
130
170
|
},
|
|
171
|
+
withScope: async (nestedFn)=>{
|
|
172
|
+
return await nestedFn(provider);
|
|
173
|
+
},
|
|
131
174
|
withTransaction: async (nestedFn)=>{
|
|
132
175
|
// Already inside a transaction — Kysely does not nest.
|
|
133
176
|
// Run `nestedFn` inline with the same provider so its hooks and
|
|
@@ -142,16 +185,21 @@ export class HozonDB {
|
|
|
142
185
|
const savepointCommitHooks = [];
|
|
143
186
|
const savepointRollbackHooks = [];
|
|
144
187
|
const scopedProvider = createScopedProvider(savepointCommitHooks, savepointRollbackHooks);
|
|
188
|
+
registry.dispatchAll();
|
|
145
189
|
await sql.raw(`SAVEPOINT ${name}`).execute(trx);
|
|
146
190
|
try {
|
|
147
191
|
const value = await nestedFn(scopedProvider);
|
|
192
|
+
registry.dispatchAll();
|
|
148
193
|
await sql.raw(`RELEASE SAVEPOINT ${name}`).execute(trx);
|
|
149
194
|
scopedCommitHooks.push(...savepointCommitHooks);
|
|
150
195
|
scopedRollbackHooks.push(...savepointRollbackHooks);
|
|
151
196
|
return value;
|
|
152
197
|
} catch (error) {
|
|
198
|
+
registry.dispatchAll();
|
|
153
199
|
await sql.raw(`ROLLBACK TO SAVEPOINT ${name}`).execute(trx);
|
|
200
|
+
registry.dispatchAll();
|
|
154
201
|
await sql.raw(`RELEASE SAVEPOINT ${name}`).execute(trx);
|
|
202
|
+
registry.clearAll();
|
|
155
203
|
this.#runHooks(savepointRollbackHooks, 'rollback');
|
|
156
204
|
throw error;
|
|
157
205
|
}
|
|
@@ -162,9 +210,14 @@ export class HozonDB {
|
|
|
162
210
|
};
|
|
163
211
|
return provider;
|
|
164
212
|
};
|
|
165
|
-
|
|
213
|
+
try {
|
|
214
|
+
return await fn(createScopedProvider(commitHooks, rollbackHooks));
|
|
215
|
+
} finally{
|
|
216
|
+
registry.dispatchAll();
|
|
217
|
+
}
|
|
166
218
|
});
|
|
167
219
|
} catch (err) {
|
|
220
|
+
registry.close();
|
|
168
221
|
// Kysely issues ROLLBACK and rejects `execute()` when the callback
|
|
169
222
|
// throws, so the DB is already reverted here. Run the rollback hooks to
|
|
170
223
|
// drop any in-memory state that was eagerly advanced expecting a commit
|
|
@@ -174,6 +227,8 @@ export class HozonDB {
|
|
|
174
227
|
this.#runHooks(rollbackHooks, 'rollback');
|
|
175
228
|
throw err;
|
|
176
229
|
}
|
|
230
|
+
registry.close();
|
|
231
|
+
beforeHooks?.();
|
|
177
232
|
// The transaction has committed; post-commit hooks are best-effort side
|
|
178
233
|
// effects (event emission, cache invalidation). A throwing hook must not
|
|
179
234
|
// skip the remaining hooks or make the caller observe a durable write as a
|
|
@@ -239,6 +294,14 @@ export class HozonDB {
|
|
|
239
294
|
}
|
|
240
295
|
return migrations;
|
|
241
296
|
}
|
|
297
|
+
// Hands stores only createLoader, so they cannot reach close/dispatchAll/clearAll.
|
|
298
|
+
// Stores on the default plugin must share one Kysely instance: keep-set locks key on it.
|
|
299
|
+
#createStoreAPI(store, kysely, defaultDB, registry) {
|
|
300
|
+
const storeDB = store.unprefixedTables == null ? defaultDB : kysely.withPlugin(this.#getStorePlugin(store));
|
|
301
|
+
return store.createAPI(storeDB, this.#adapter, {
|
|
302
|
+
createLoader: (params)=>registry.createLoader(params)
|
|
303
|
+
});
|
|
304
|
+
}
|
|
242
305
|
#getStorePlugin(store) {
|
|
243
306
|
if (store.unprefixedTables == null) return this.#defaultPlugin;
|
|
244
307
|
let plugin = this.#storePlugins.get(store.name);
|
package/lib/errors.d.ts
CHANGED
package/lib/errors.js
CHANGED
|
@@ -22,3 +22,9 @@ export class HozonDBClosedError extends Error {
|
|
|
22
22
|
this.name = 'HozonDBClosedError';
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
+
export class BatchLimitError extends Error {
|
|
26
|
+
constructor(size, limit){
|
|
27
|
+
super(`Batch of ${size} keys exceeds limit ${limit}`);
|
|
28
|
+
this.name = 'BatchLimitError';
|
|
29
|
+
}
|
|
30
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export type { ColumnType, Expression, Generated, Insertable, RawBuilder, Selectable, SelectQueryBuilder, Transaction, Updateable, } from 'kysely';
|
|
2
2
|
export { Kysely, sql } from 'kysely';
|
|
3
3
|
export type { Migration } from 'kysely/migration';
|
|
4
|
-
export
|
|
4
|
+
export { assertBatchSize, type BatchReadOptions, type IterateBatchesOptions, iterateBatches, } from './batch.js';
|
|
5
|
+
export { type BatchLoader, type BatchLoaderParams, createBatchLoader, } from './batch-loader.js';
|
|
6
|
+
export type { HozonDBParams, MigrationContext, StoreContext, StoreDefinition, StoreProvider, } from './db.js';
|
|
5
7
|
export { HozonDB } from './db.js';
|
|
6
|
-
export { HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError, SchemaVersionError, } from './errors.js';
|
|
8
|
+
export { BatchLimitError, HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError, SchemaVersionError, } from './errors.js';
|
|
7
9
|
export { chunk, withKeepSet } from './keep-set.js';
|
|
8
10
|
export { withStoreTransaction } from './store-transaction.js';
|
|
9
11
|
export { type TablePrefixOptions, TablePrefixPlugin } from './table-prefix.js';
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { Kysely, sql } from 'kysely';
|
|
2
|
+
export { assertBatchSize, iterateBatches } from './batch.js';
|
|
3
|
+
export { createBatchLoader } from './batch-loader.js';
|
|
2
4
|
export { HozonDB } from './db.js';
|
|
3
|
-
export { HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError, SchemaVersionError } from './errors.js';
|
|
5
|
+
export { BatchLimitError, HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError, SchemaVersionError } from './errors.js';
|
|
4
6
|
export { chunk, withKeepSet } from './keep-set.js';
|
|
5
7
|
export { withStoreTransaction } from './store-transaction.js';
|
|
6
8
|
export { TablePrefixPlugin } from './table-prefix.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type BatchLoader, type BatchLoaderParams } from './batch-loader.js';
|
|
2
|
+
export declare class LoaderRegistry {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(params: {
|
|
5
|
+
remember: boolean;
|
|
6
|
+
});
|
|
7
|
+
createLoader<TKey, TValue>(params: BatchLoaderParams<TKey, TValue>): BatchLoader<TKey, TValue>;
|
|
8
|
+
dispatchAll(): void;
|
|
9
|
+
clearAll(): void;
|
|
10
|
+
close(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createBatchLoader } from './batch-loader.js';
|
|
2
|
+
export class LoaderRegistry {
|
|
3
|
+
#remember;
|
|
4
|
+
#loaders = new Set();
|
|
5
|
+
constructor(params){
|
|
6
|
+
this.#remember = params.remember;
|
|
7
|
+
}
|
|
8
|
+
createLoader(params) {
|
|
9
|
+
const loader = createBatchLoader({
|
|
10
|
+
...params,
|
|
11
|
+
remember: ()=>this.#remember
|
|
12
|
+
});
|
|
13
|
+
this.#loaders.add(loader);
|
|
14
|
+
return loader;
|
|
15
|
+
}
|
|
16
|
+
dispatchAll() {
|
|
17
|
+
for (const loader of this.#loaders)loader.dispatch();
|
|
18
|
+
}
|
|
19
|
+
clearAll() {
|
|
20
|
+
for (const loader of this.#loaders)loader.clearAll();
|
|
21
|
+
}
|
|
22
|
+
close() {
|
|
23
|
+
this.#remember = false;
|
|
24
|
+
this.clearAll();
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hozon/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Database registry, migrations and transactions for Hozon",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,14 +18,15 @@
|
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@hozon/adapter": "^0.1.0",
|
|
21
|
+
"@sozai/async": "^0.4.1",
|
|
21
22
|
"@sozai/log": "^0.4.0",
|
|
22
|
-
"kysely": "^0.29.
|
|
23
|
+
"kysely": "^0.29.7"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@hozon/node-sqlite": "^0.1.0",
|
|
26
|
-
"@types/node": "^26.6.
|
|
27
|
-
"del-cli": "^
|
|
28
|
-
"vitest": "5.0.
|
|
27
|
+
"@types/node": "^26.6.5",
|
|
28
|
+
"del-cli": "^8.0.1",
|
|
29
|
+
"vitest": "5.0.5"
|
|
29
30
|
},
|
|
30
31
|
"engines": {
|
|
31
32
|
"node": ">=24"
|