@hozon/db 0.2.3 → 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 +71 -5
- 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
|
@@ -3,6 +3,7 @@ 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){
|
|
@@ -63,8 +67,7 @@ export class HozonDB {
|
|
|
63
67
|
if (store == null) {
|
|
64
68
|
throw new Error(`Store "${name}" is not registered`);
|
|
65
69
|
}
|
|
66
|
-
|
|
67
|
-
this.#storeAPIs.set(name, store.createAPI(storeDB, this.#adapter));
|
|
70
|
+
this.#storeAPIs.set(name, this.#createStoreAPI(store, this.#db, this.#storeDB, this.#loaders));
|
|
68
71
|
}
|
|
69
72
|
return this.#storeAPIs.get(name);
|
|
70
73
|
}
|
|
@@ -76,6 +79,41 @@ export class HozonDB {
|
|
|
76
79
|
}
|
|
77
80
|
// Each standalone op auto-commits, so there is nothing to roll back — no-op.
|
|
78
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
|
+
}
|
|
79
117
|
/**
|
|
80
118
|
* Runs `fn` inside a DB transaction and provides a scoped `StoreProvider`.
|
|
81
119
|
* Concurrent calls on the same `HozonDB` instance are supported — the
|
|
@@ -85,6 +123,9 @@ export class HozonDB {
|
|
|
85
123
|
* `db.withTransaction` on the same instance inside the callback opens a
|
|
86
124
|
* new independent transaction (no atomic nesting) and is a bug.
|
|
87
125
|
*/ async withTransaction(fn) {
|
|
126
|
+
return await this.#withTransaction(fn);
|
|
127
|
+
}
|
|
128
|
+
async #withTransaction(fn, beforeHooks) {
|
|
88
129
|
// Migrate every registered store before opening the transaction. Stores
|
|
89
130
|
// are internal and known up front, so a broken migration should fail fast
|
|
90
131
|
// at startup rather than leave a runtime partition where one store's
|
|
@@ -93,6 +134,9 @@ export class HozonDB {
|
|
|
93
134
|
this.#assertOpen();
|
|
94
135
|
const commitHooks = [];
|
|
95
136
|
const rollbackHooks = [];
|
|
137
|
+
const registry = new LoaderRegistry({
|
|
138
|
+
remember: true
|
|
139
|
+
});
|
|
96
140
|
let result;
|
|
97
141
|
let savepointID = 0;
|
|
98
142
|
try {
|
|
@@ -113,8 +157,7 @@ export class HozonDB {
|
|
|
113
157
|
if (store == null) {
|
|
114
158
|
throw new Error(`Store "${name}" is not registered`);
|
|
115
159
|
}
|
|
116
|
-
|
|
117
|
-
txStoreAPIs.set(name, store.createAPI(storeDB, this.#adapter));
|
|
160
|
+
txStoreAPIs.set(name, this.#createStoreAPI(store, txKysely, txStoreDB, registry));
|
|
118
161
|
}
|
|
119
162
|
return txStoreAPIs.get(name);
|
|
120
163
|
},
|
|
@@ -125,6 +168,9 @@ export class HozonDB {
|
|
|
125
168
|
onRollback: (hook)=>{
|
|
126
169
|
scopedRollbackHooks.push(hook);
|
|
127
170
|
},
|
|
171
|
+
withScope: async (nestedFn)=>{
|
|
172
|
+
return await nestedFn(provider);
|
|
173
|
+
},
|
|
128
174
|
withTransaction: async (nestedFn)=>{
|
|
129
175
|
// Already inside a transaction — Kysely does not nest.
|
|
130
176
|
// Run `nestedFn` inline with the same provider so its hooks and
|
|
@@ -139,16 +185,21 @@ export class HozonDB {
|
|
|
139
185
|
const savepointCommitHooks = [];
|
|
140
186
|
const savepointRollbackHooks = [];
|
|
141
187
|
const scopedProvider = createScopedProvider(savepointCommitHooks, savepointRollbackHooks);
|
|
188
|
+
registry.dispatchAll();
|
|
142
189
|
await sql.raw(`SAVEPOINT ${name}`).execute(trx);
|
|
143
190
|
try {
|
|
144
191
|
const value = await nestedFn(scopedProvider);
|
|
192
|
+
registry.dispatchAll();
|
|
145
193
|
await sql.raw(`RELEASE SAVEPOINT ${name}`).execute(trx);
|
|
146
194
|
scopedCommitHooks.push(...savepointCommitHooks);
|
|
147
195
|
scopedRollbackHooks.push(...savepointRollbackHooks);
|
|
148
196
|
return value;
|
|
149
197
|
} catch (error) {
|
|
198
|
+
registry.dispatchAll();
|
|
150
199
|
await sql.raw(`ROLLBACK TO SAVEPOINT ${name}`).execute(trx);
|
|
200
|
+
registry.dispatchAll();
|
|
151
201
|
await sql.raw(`RELEASE SAVEPOINT ${name}`).execute(trx);
|
|
202
|
+
registry.clearAll();
|
|
152
203
|
this.#runHooks(savepointRollbackHooks, 'rollback');
|
|
153
204
|
throw error;
|
|
154
205
|
}
|
|
@@ -159,9 +210,14 @@ export class HozonDB {
|
|
|
159
210
|
};
|
|
160
211
|
return provider;
|
|
161
212
|
};
|
|
162
|
-
|
|
213
|
+
try {
|
|
214
|
+
return await fn(createScopedProvider(commitHooks, rollbackHooks));
|
|
215
|
+
} finally{
|
|
216
|
+
registry.dispatchAll();
|
|
217
|
+
}
|
|
163
218
|
});
|
|
164
219
|
} catch (err) {
|
|
220
|
+
registry.close();
|
|
165
221
|
// Kysely issues ROLLBACK and rejects `execute()` when the callback
|
|
166
222
|
// throws, so the DB is already reverted here. Run the rollback hooks to
|
|
167
223
|
// drop any in-memory state that was eagerly advanced expecting a commit
|
|
@@ -171,6 +227,8 @@ export class HozonDB {
|
|
|
171
227
|
this.#runHooks(rollbackHooks, 'rollback');
|
|
172
228
|
throw err;
|
|
173
229
|
}
|
|
230
|
+
registry.close();
|
|
231
|
+
beforeHooks?.();
|
|
174
232
|
// The transaction has committed; post-commit hooks are best-effort side
|
|
175
233
|
// effects (event emission, cache invalidation). A throwing hook must not
|
|
176
234
|
// skip the remaining hooks or make the caller observe a durable write as a
|
|
@@ -236,6 +294,14 @@ export class HozonDB {
|
|
|
236
294
|
}
|
|
237
295
|
return migrations;
|
|
238
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
|
+
}
|
|
239
305
|
#getStorePlugin(store) {
|
|
240
306
|
if (store.unprefixedTables == null) return this.#defaultPlugin;
|
|
241
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"
|