@workglow/storage 0.0.52
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/LICENSE +201 -0
- package/README.md +1015 -0
- package/dist/browser.js +2635 -0
- package/dist/browser.js.map +27 -0
- package/dist/bun.js +3880 -0
- package/dist/bun.js.map +35 -0
- package/dist/common-server.d.ts +23 -0
- package/dist/common-server.d.ts.map +1 -0
- package/dist/common.d.ts +16 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/kv/FsFolderJsonKvRepository.d.ts +27 -0
- package/dist/kv/FsFolderJsonKvRepository.d.ts.map +1 -0
- package/dist/kv/FsFolderKvRepository.d.ts +74 -0
- package/dist/kv/FsFolderKvRepository.d.ts.map +1 -0
- package/dist/kv/IKvRepository.d.ts +65 -0
- package/dist/kv/IKvRepository.d.ts.map +1 -0
- package/dist/kv/InMemoryKvRepository.d.ts +26 -0
- package/dist/kv/InMemoryKvRepository.d.ts.map +1 -0
- package/dist/kv/IndexedDbKvRepository.d.ts +27 -0
- package/dist/kv/IndexedDbKvRepository.d.ts.map +1 -0
- package/dist/kv/KvRepository.d.ts +109 -0
- package/dist/kv/KvRepository.d.ts.map +1 -0
- package/dist/kv/KvViaTabularRepository.d.ts +64 -0
- package/dist/kv/KvViaTabularRepository.d.ts.map +1 -0
- package/dist/kv/PostgresKvRepository.d.ts +28 -0
- package/dist/kv/PostgresKvRepository.d.ts.map +1 -0
- package/dist/kv/SqliteKvRepository.d.ts +28 -0
- package/dist/kv/SqliteKvRepository.d.ts.map +1 -0
- package/dist/kv/SupabaseKvRepository.d.ts +34 -0
- package/dist/kv/SupabaseKvRepository.d.ts.map +1 -0
- package/dist/node.js +3879 -0
- package/dist/node.js.map +35 -0
- package/dist/queue/IQueueStorage.d.ts +125 -0
- package/dist/queue/IQueueStorage.d.ts.map +1 -0
- package/dist/queue/InMemoryQueueStorage.d.ts +109 -0
- package/dist/queue/InMemoryQueueStorage.d.ts.map +1 -0
- package/dist/queue/IndexedDbQueueStorage.d.ts +89 -0
- package/dist/queue/IndexedDbQueueStorage.d.ts.map +1 -0
- package/dist/queue/PostgresQueueStorage.d.ts +92 -0
- package/dist/queue/PostgresQueueStorage.d.ts.map +1 -0
- package/dist/queue/SqliteQueueStorage.d.ts +116 -0
- package/dist/queue/SqliteQueueStorage.d.ts.map +1 -0
- package/dist/queue/SupabaseQueueStorage.d.ts +93 -0
- package/dist/queue/SupabaseQueueStorage.d.ts.map +1 -0
- package/dist/tabular/BaseSqlTabularRepository.d.ts +94 -0
- package/dist/tabular/BaseSqlTabularRepository.d.ts.map +1 -0
- package/dist/tabular/CachedTabularRepository.d.ts +110 -0
- package/dist/tabular/CachedTabularRepository.d.ts.map +1 -0
- package/dist/tabular/FsFolderTabularRepository.d.ts +92 -0
- package/dist/tabular/FsFolderTabularRepository.d.ts.map +1 -0
- package/dist/tabular/ITabularRepository.d.ts +52 -0
- package/dist/tabular/ITabularRepository.d.ts.map +1 -0
- package/dist/tabular/InMemoryTabularRepository.d.ts +93 -0
- package/dist/tabular/InMemoryTabularRepository.d.ts.map +1 -0
- package/dist/tabular/IndexedDbTabularRepository.d.ts +100 -0
- package/dist/tabular/IndexedDbTabularRepository.d.ts.map +1 -0
- package/dist/tabular/PostgresTabularRepository.d.ts +133 -0
- package/dist/tabular/PostgresTabularRepository.d.ts.map +1 -0
- package/dist/tabular/SharedInMemoryTabularRepository.d.ts +126 -0
- package/dist/tabular/SharedInMemoryTabularRepository.d.ts.map +1 -0
- package/dist/tabular/SqliteTabularRepository.d.ts +110 -0
- package/dist/tabular/SqliteTabularRepository.d.ts.map +1 -0
- package/dist/tabular/SupabaseTabularRepository.d.ts +132 -0
- package/dist/tabular/SupabaseTabularRepository.d.ts.map +1 -0
- package/dist/tabular/TabularRepository.d.ts +123 -0
- package/dist/tabular/TabularRepository.d.ts.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/util/IndexedDbTable.d.ts +40 -0
- package/dist/util/IndexedDbTable.d.ts.map +1 -0
- package/package.json +60 -0
- package/src/kv/README.md +159 -0
- package/src/queue/README.md +41 -0
- package/src/tabular/README.md +298 -0
package/dist/browser.js
ADDED
|
@@ -0,0 +1,2635 @@
|
|
|
1
|
+
// src/tabular/CachedTabularRepository.ts
|
|
2
|
+
import { createServiceToken as createServiceToken3 } from "@workglow/util";
|
|
3
|
+
|
|
4
|
+
// src/tabular/InMemoryTabularRepository.ts
|
|
5
|
+
import {
|
|
6
|
+
createServiceToken as createServiceToken2,
|
|
7
|
+
makeFingerprint as makeFingerprint2
|
|
8
|
+
} from "@workglow/util";
|
|
9
|
+
|
|
10
|
+
// src/tabular/TabularRepository.ts
|
|
11
|
+
import {
|
|
12
|
+
createServiceToken,
|
|
13
|
+
EventEmitter,
|
|
14
|
+
makeFingerprint
|
|
15
|
+
} from "@workglow/util";
|
|
16
|
+
var TABULAR_REPOSITORY = createServiceToken("storage.tabularRepository");
|
|
17
|
+
|
|
18
|
+
class TabularRepository {
|
|
19
|
+
schema;
|
|
20
|
+
primaryKeyNames;
|
|
21
|
+
events = new EventEmitter;
|
|
22
|
+
indexes;
|
|
23
|
+
primaryKeySchema;
|
|
24
|
+
valueSchema;
|
|
25
|
+
constructor(schema, primaryKeyNames, indexes = []) {
|
|
26
|
+
this.schema = schema;
|
|
27
|
+
this.primaryKeyNames = primaryKeyNames;
|
|
28
|
+
const primaryKeyProps = {};
|
|
29
|
+
const valueProps = {};
|
|
30
|
+
const primaryKeySet = new Set(primaryKeyNames);
|
|
31
|
+
for (const [key, typeDef] of Object.entries(schema.properties)) {
|
|
32
|
+
if (primaryKeySet.has(key)) {
|
|
33
|
+
primaryKeyProps[key] = Object.assign({}, typeDef);
|
|
34
|
+
} else {
|
|
35
|
+
valueProps[key] = Object.assign({}, typeDef);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const primaryKeyRequired = schema.required?.filter((key) => primaryKeySet.has(key)) ?? [];
|
|
39
|
+
const valueRequired = schema.required?.filter((key) => !primaryKeySet.has(key)) ?? [];
|
|
40
|
+
this.primaryKeySchema = {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: primaryKeyProps,
|
|
43
|
+
required: primaryKeyRequired,
|
|
44
|
+
additionalProperties: false
|
|
45
|
+
};
|
|
46
|
+
this.valueSchema = {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: valueProps,
|
|
49
|
+
required: valueRequired,
|
|
50
|
+
additionalProperties: false
|
|
51
|
+
};
|
|
52
|
+
const combinedColumns = [...this.primaryKeyColumns(), ...this.valueColumns()];
|
|
53
|
+
for (const column of combinedColumns) {
|
|
54
|
+
if (typeof column !== "string") {
|
|
55
|
+
throw new Error("Column names must be strings");
|
|
56
|
+
}
|
|
57
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(column)) {
|
|
58
|
+
throw new Error("Column names must start with a letter and contain only letters, digits, and underscores");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
this.indexes = indexes.map((spec) => Array.isArray(spec) ? spec : [spec]);
|
|
62
|
+
this.indexes = this.filterCompoundKeys(this.primaryKeyColumns(), this.indexes);
|
|
63
|
+
for (const compoundIndex of this.indexes) {
|
|
64
|
+
for (const column of compoundIndex) {
|
|
65
|
+
if (!(column in this.primaryKeySchema.properties) && !(column in this.valueSchema.properties)) {
|
|
66
|
+
throw new Error(`Searchable column ${String(column)} is not in the primary key schema or value schema`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
filterCompoundKeys(primaryKey, potentialKeys) {
|
|
72
|
+
const isPrefix = (prefix, arr) => {
|
|
73
|
+
if (prefix.length > arr.length)
|
|
74
|
+
return false;
|
|
75
|
+
return prefix.every((val, index) => val === arr[index]);
|
|
76
|
+
};
|
|
77
|
+
potentialKeys.sort((a, b) => a.length - b.length);
|
|
78
|
+
let filteredKeys = [];
|
|
79
|
+
for (let i = 0;i < potentialKeys.length; i++) {
|
|
80
|
+
let key = potentialKeys[i];
|
|
81
|
+
if (isPrefix(key, primaryKey))
|
|
82
|
+
continue;
|
|
83
|
+
if (key.length === 1) {
|
|
84
|
+
filteredKeys.push(key);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
let isRedundant = potentialKeys.some((otherKey, j) => j > i && isPrefix(key, otherKey));
|
|
88
|
+
if (!isRedundant) {
|
|
89
|
+
filteredKeys.push(key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return filteredKeys;
|
|
93
|
+
}
|
|
94
|
+
on(name, fn) {
|
|
95
|
+
this.events.on(name, fn);
|
|
96
|
+
}
|
|
97
|
+
off(name, fn) {
|
|
98
|
+
this.events.off(name, fn);
|
|
99
|
+
}
|
|
100
|
+
once(name, fn) {
|
|
101
|
+
this.events.once(name, fn);
|
|
102
|
+
}
|
|
103
|
+
emit(name, ...args) {
|
|
104
|
+
this.events.emit(name, ...args);
|
|
105
|
+
}
|
|
106
|
+
waitOn(name) {
|
|
107
|
+
return this.events.waitOn(name);
|
|
108
|
+
}
|
|
109
|
+
primaryKeyColumns() {
|
|
110
|
+
const columns = [];
|
|
111
|
+
for (const key of Object.keys(this.primaryKeySchema.properties)) {
|
|
112
|
+
columns.push(key);
|
|
113
|
+
}
|
|
114
|
+
return columns;
|
|
115
|
+
}
|
|
116
|
+
valueColumns() {
|
|
117
|
+
const columns = [];
|
|
118
|
+
for (const key of Object.keys(this.valueSchema.properties)) {
|
|
119
|
+
columns.push(key);
|
|
120
|
+
}
|
|
121
|
+
return columns;
|
|
122
|
+
}
|
|
123
|
+
separateKeyValueFromCombined(obj) {
|
|
124
|
+
if (obj === null) {
|
|
125
|
+
console.warn("Key is null");
|
|
126
|
+
return { value: {}, key: {} };
|
|
127
|
+
}
|
|
128
|
+
if (typeof obj !== "object") {
|
|
129
|
+
console.warn("Object is not an object");
|
|
130
|
+
return { value: {}, key: {} };
|
|
131
|
+
}
|
|
132
|
+
const primaryKeyNames = this.primaryKeyColumns();
|
|
133
|
+
const valueNames = this.valueColumns();
|
|
134
|
+
const value = {};
|
|
135
|
+
const key = {};
|
|
136
|
+
for (const k of primaryKeyNames) {
|
|
137
|
+
key[k] = obj[k];
|
|
138
|
+
}
|
|
139
|
+
for (const k of valueNames) {
|
|
140
|
+
value[k] = obj[k];
|
|
141
|
+
}
|
|
142
|
+
return { value, key };
|
|
143
|
+
}
|
|
144
|
+
async getKeyAsIdString(key) {
|
|
145
|
+
return await makeFingerprint(key);
|
|
146
|
+
}
|
|
147
|
+
getPrimaryKeyAsOrderedArray(key) {
|
|
148
|
+
const orderedParams = [];
|
|
149
|
+
const keyObj = key;
|
|
150
|
+
for (const k in this.primaryKeySchema.properties) {
|
|
151
|
+
if (k in keyObj) {
|
|
152
|
+
orderedParams.push(keyObj[k]);
|
|
153
|
+
} else {
|
|
154
|
+
throw new Error(`Missing required primary key field: ${k}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return orderedParams;
|
|
158
|
+
}
|
|
159
|
+
findBestMatchingIndex(unorderedSearchKey) {
|
|
160
|
+
if (!unorderedSearchKey.length)
|
|
161
|
+
return;
|
|
162
|
+
const allKeys = [
|
|
163
|
+
this.primaryKeyColumns(),
|
|
164
|
+
...this.indexes
|
|
165
|
+
];
|
|
166
|
+
const searchKeySet = new Set(unorderedSearchKey);
|
|
167
|
+
const hasMatchingPrefix = (index) => {
|
|
168
|
+
return index.length > 0 && searchKeySet.has(index[0]);
|
|
169
|
+
};
|
|
170
|
+
let bestMatch;
|
|
171
|
+
let bestMatchScore = 0;
|
|
172
|
+
for (const index of allKeys) {
|
|
173
|
+
if (hasMatchingPrefix(index)) {
|
|
174
|
+
let score = 0;
|
|
175
|
+
for (const col of index) {
|
|
176
|
+
if (!searchKeySet.has(col))
|
|
177
|
+
break;
|
|
178
|
+
score++;
|
|
179
|
+
}
|
|
180
|
+
if (score > bestMatchScore) {
|
|
181
|
+
bestMatch = index;
|
|
182
|
+
bestMatchScore = score;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return bestMatch;
|
|
187
|
+
}
|
|
188
|
+
destroy() {}
|
|
189
|
+
async[Symbol.asyncDispose]() {
|
|
190
|
+
this.destroy();
|
|
191
|
+
}
|
|
192
|
+
[Symbol.dispose]() {
|
|
193
|
+
this.destroy();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/tabular/InMemoryTabularRepository.ts
|
|
198
|
+
var MEMORY_TABULAR_REPOSITORY = createServiceToken2("storage.tabularRepository.inMemory");
|
|
199
|
+
|
|
200
|
+
class InMemoryTabularRepository extends TabularRepository {
|
|
201
|
+
values = new Map;
|
|
202
|
+
constructor(schema, primaryKeyNames, indexes = []) {
|
|
203
|
+
super(schema, primaryKeyNames, indexes);
|
|
204
|
+
}
|
|
205
|
+
async setupDatabase() {}
|
|
206
|
+
async put(value) {
|
|
207
|
+
const { key } = this.separateKeyValueFromCombined(value);
|
|
208
|
+
const id = await makeFingerprint2(key);
|
|
209
|
+
this.values.set(id, value);
|
|
210
|
+
this.events.emit("put", value);
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
async putBulk(values) {
|
|
214
|
+
return await Promise.all(values.map(async (value) => this.put(value)));
|
|
215
|
+
}
|
|
216
|
+
async get(key) {
|
|
217
|
+
const id = await makeFingerprint2(key);
|
|
218
|
+
const out = this.values.get(id);
|
|
219
|
+
this.events.emit("get", key, out);
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
async search(key) {
|
|
223
|
+
const searchKeys = Object.keys(key);
|
|
224
|
+
if (searchKeys.length === 0) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const bestIndex = this.findBestMatchingIndex(searchKeys);
|
|
228
|
+
if (!bestIndex) {
|
|
229
|
+
throw new Error(`No suitable index found for the search criteria, searching for ['${searchKeys.join("', '")}'] with pk ['${this.primaryKeyNames.join("', '")}'] and indexes ['${this.indexes.join("', '")}']`);
|
|
230
|
+
}
|
|
231
|
+
const results = Array.from(this.values.values()).filter((item) => Object.entries(key).every(([k, v]) => item[k] === v));
|
|
232
|
+
if (results.length > 0) {
|
|
233
|
+
this.events.emit("search", key, results);
|
|
234
|
+
return results;
|
|
235
|
+
} else {
|
|
236
|
+
this.events.emit("search", key, undefined);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async delete(value) {
|
|
241
|
+
const { key } = this.separateKeyValueFromCombined(value);
|
|
242
|
+
const id = await makeFingerprint2(key);
|
|
243
|
+
this.values.delete(id);
|
|
244
|
+
this.events.emit("delete", key);
|
|
245
|
+
}
|
|
246
|
+
async deleteAll() {
|
|
247
|
+
this.values.clear();
|
|
248
|
+
this.events.emit("clearall");
|
|
249
|
+
}
|
|
250
|
+
async getAll() {
|
|
251
|
+
const all = Array.from(this.values.values());
|
|
252
|
+
return all.length > 0 ? all : undefined;
|
|
253
|
+
}
|
|
254
|
+
async size() {
|
|
255
|
+
return this.values.size;
|
|
256
|
+
}
|
|
257
|
+
async deleteSearch(column, value, operator = "=") {
|
|
258
|
+
const entries = this.values.entries();
|
|
259
|
+
const entriesToDelete = entries.filter(([_, entity]) => {
|
|
260
|
+
const columnValue = entity[column];
|
|
261
|
+
switch (operator) {
|
|
262
|
+
case "=":
|
|
263
|
+
return columnValue === value;
|
|
264
|
+
case "<":
|
|
265
|
+
return columnValue !== null && columnValue !== undefined && columnValue < value;
|
|
266
|
+
case "<=":
|
|
267
|
+
return columnValue !== null && columnValue !== undefined && columnValue <= value;
|
|
268
|
+
case ">":
|
|
269
|
+
return columnValue !== null && columnValue !== undefined && columnValue > value;
|
|
270
|
+
case ">=":
|
|
271
|
+
return columnValue !== null && columnValue !== undefined && columnValue >= value;
|
|
272
|
+
default:
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
for (const [id, _] of entriesToDelete) {
|
|
277
|
+
this.values.delete(id);
|
|
278
|
+
}
|
|
279
|
+
if (Array.from(entriesToDelete).length > 0) {
|
|
280
|
+
this.events.emit("delete", column);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
destroy() {
|
|
284
|
+
this.values.clear();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/tabular/CachedTabularRepository.ts
|
|
289
|
+
var CACHED_TABULAR_REPOSITORY = createServiceToken3("storage.tabularRepository.cached");
|
|
290
|
+
|
|
291
|
+
class CachedTabularRepository extends TabularRepository {
|
|
292
|
+
cache;
|
|
293
|
+
durable;
|
|
294
|
+
cacheInitialized = false;
|
|
295
|
+
constructor(durable, cache, schema, primaryKeyNames, indexes) {
|
|
296
|
+
if (!schema || !primaryKeyNames) {
|
|
297
|
+
throw new Error("Schema and primaryKeyNames must be provided when creating CachedTabularRepository");
|
|
298
|
+
}
|
|
299
|
+
super(schema, primaryKeyNames, indexes || []);
|
|
300
|
+
this.durable = durable;
|
|
301
|
+
if (cache) {
|
|
302
|
+
this.cache = cache;
|
|
303
|
+
} else {
|
|
304
|
+
this.cache = new InMemoryTabularRepository(schema, primaryKeyNames, indexes || []);
|
|
305
|
+
}
|
|
306
|
+
this.setupEventForwarding();
|
|
307
|
+
}
|
|
308
|
+
setupEventForwarding() {
|
|
309
|
+
this.cache.on("put", (entity) => {
|
|
310
|
+
this.events.emit("put", entity);
|
|
311
|
+
});
|
|
312
|
+
this.cache.on("get", (key, entity) => {
|
|
313
|
+
this.events.emit("get", key, entity);
|
|
314
|
+
});
|
|
315
|
+
this.cache.on("search", (key, entities) => {
|
|
316
|
+
this.events.emit("search", key, entities);
|
|
317
|
+
});
|
|
318
|
+
this.cache.on("delete", (key) => {
|
|
319
|
+
this.events.emit("delete", key);
|
|
320
|
+
});
|
|
321
|
+
this.cache.on("clearall", () => {
|
|
322
|
+
this.events.emit("clearall");
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
async initializeCache() {
|
|
326
|
+
if (this.cacheInitialized)
|
|
327
|
+
return;
|
|
328
|
+
try {
|
|
329
|
+
const all = await this.durable.getAll();
|
|
330
|
+
if (all && all.length > 0) {
|
|
331
|
+
await this.cache.putBulk(all);
|
|
332
|
+
}
|
|
333
|
+
this.cacheInitialized = true;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.warn("Failed to initialize cache from durable repository:", error);
|
|
336
|
+
this.cacheInitialized = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async put(value) {
|
|
340
|
+
await this.initializeCache();
|
|
341
|
+
const result = await this.durable.put(value);
|
|
342
|
+
await this.cache.put(result);
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
async putBulk(values) {
|
|
346
|
+
await this.initializeCache();
|
|
347
|
+
const results = await this.durable.putBulk(values);
|
|
348
|
+
await this.cache.putBulk(results);
|
|
349
|
+
return results;
|
|
350
|
+
}
|
|
351
|
+
async get(key) {
|
|
352
|
+
await this.initializeCache();
|
|
353
|
+
let result = await this.cache.get(key);
|
|
354
|
+
if (result === undefined) {
|
|
355
|
+
result = await this.durable.get(key);
|
|
356
|
+
if (result) {
|
|
357
|
+
await this.cache.put(result);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
async search(key) {
|
|
363
|
+
await this.initializeCache();
|
|
364
|
+
let results = await this.cache.search(key);
|
|
365
|
+
if (results === undefined) {
|
|
366
|
+
results = await this.durable.search(key);
|
|
367
|
+
if (results && results.length > 0) {
|
|
368
|
+
await this.cache.putBulk(results);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return results;
|
|
372
|
+
}
|
|
373
|
+
async delete(value) {
|
|
374
|
+
await this.initializeCache();
|
|
375
|
+
await this.durable.delete(value);
|
|
376
|
+
await this.cache.delete(value);
|
|
377
|
+
}
|
|
378
|
+
async deleteAll() {
|
|
379
|
+
await this.initializeCache();
|
|
380
|
+
await this.durable.deleteAll();
|
|
381
|
+
await this.cache.deleteAll();
|
|
382
|
+
}
|
|
383
|
+
async getAll() {
|
|
384
|
+
await this.initializeCache();
|
|
385
|
+
let results = await this.cache.getAll();
|
|
386
|
+
if (!results || results.length === 0) {
|
|
387
|
+
results = await this.durable.getAll();
|
|
388
|
+
if (results && results.length > 0) {
|
|
389
|
+
await this.cache.putBulk(results);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return results;
|
|
393
|
+
}
|
|
394
|
+
async size() {
|
|
395
|
+
await this.initializeCache();
|
|
396
|
+
return await this.durable.size();
|
|
397
|
+
}
|
|
398
|
+
async deleteSearch(column, value, operator = "=") {
|
|
399
|
+
await this.initializeCache();
|
|
400
|
+
await this.durable.deleteSearch(column, value, operator);
|
|
401
|
+
await this.cache.deleteSearch(column, value, operator);
|
|
402
|
+
}
|
|
403
|
+
async invalidateCache() {
|
|
404
|
+
await this.cache.deleteAll();
|
|
405
|
+
this.cacheInitialized = false;
|
|
406
|
+
}
|
|
407
|
+
async refreshCache() {
|
|
408
|
+
await this.cache.deleteAll();
|
|
409
|
+
this.cacheInitialized = false;
|
|
410
|
+
await this.initializeCache();
|
|
411
|
+
}
|
|
412
|
+
destroy() {
|
|
413
|
+
this.durable.destroy();
|
|
414
|
+
this.cache.destroy();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// src/kv/IKvRepository.ts
|
|
418
|
+
var DefaultKeyValueSchema = {
|
|
419
|
+
type: "object",
|
|
420
|
+
properties: {
|
|
421
|
+
key: { type: "string" },
|
|
422
|
+
value: {}
|
|
423
|
+
},
|
|
424
|
+
additionalProperties: false
|
|
425
|
+
};
|
|
426
|
+
var DefaultKeyValueKey = ["key"];
|
|
427
|
+
// src/kv/InMemoryKvRepository.ts
|
|
428
|
+
import { createServiceToken as createServiceToken5 } from "@workglow/util";
|
|
429
|
+
|
|
430
|
+
// src/kv/KvRepository.ts
|
|
431
|
+
import { createServiceToken as createServiceToken4, EventEmitter as EventEmitter2, makeFingerprint as makeFingerprint3 } from "@workglow/util";
|
|
432
|
+
var KV_REPOSITORY = createServiceToken4("storage.kvRepository");
|
|
433
|
+
|
|
434
|
+
class KvRepository {
|
|
435
|
+
keySchema;
|
|
436
|
+
valueSchema;
|
|
437
|
+
events = new EventEmitter2;
|
|
438
|
+
constructor(keySchema = { type: "string" }, valueSchema = {}) {
|
|
439
|
+
this.keySchema = keySchema;
|
|
440
|
+
this.valueSchema = valueSchema;
|
|
441
|
+
}
|
|
442
|
+
async getObjectAsIdString(object) {
|
|
443
|
+
return await makeFingerprint3(object);
|
|
444
|
+
}
|
|
445
|
+
on(name, fn) {
|
|
446
|
+
this.events.on(name, fn);
|
|
447
|
+
}
|
|
448
|
+
off(name, fn) {
|
|
449
|
+
this.events.off(name, fn);
|
|
450
|
+
}
|
|
451
|
+
once(name, fn) {
|
|
452
|
+
this.events.once(name, fn);
|
|
453
|
+
}
|
|
454
|
+
emit(name, ...args) {
|
|
455
|
+
this.events.emit(name, ...args);
|
|
456
|
+
}
|
|
457
|
+
waitOn(name) {
|
|
458
|
+
return this.events.waitOn(name);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/kv/KvViaTabularRepository.ts
|
|
463
|
+
class KvViaTabularRepository extends KvRepository {
|
|
464
|
+
async put(key, value) {
|
|
465
|
+
const schemaType = typeof this.valueSchema === "object" && this.valueSchema !== null && "type" in this.valueSchema ? this.valueSchema.type : undefined;
|
|
466
|
+
const shouldStringify = !["number", "boolean", "string", "blob"].includes(schemaType);
|
|
467
|
+
if (shouldStringify) {
|
|
468
|
+
value = JSON.stringify(value);
|
|
469
|
+
}
|
|
470
|
+
await this.tabularRepository.put({ key, value });
|
|
471
|
+
}
|
|
472
|
+
async putBulk(items) {
|
|
473
|
+
const schemaType = typeof this.valueSchema === "object" && this.valueSchema !== null && "type" in this.valueSchema ? this.valueSchema.type : undefined;
|
|
474
|
+
const shouldStringify = !["number", "boolean", "string", "blob"].includes(schemaType);
|
|
475
|
+
const entities = items.map(({ key, value }) => {
|
|
476
|
+
if (shouldStringify) {
|
|
477
|
+
value = JSON.stringify(value);
|
|
478
|
+
}
|
|
479
|
+
return { key, value };
|
|
480
|
+
});
|
|
481
|
+
await this.tabularRepository.putBulk(entities);
|
|
482
|
+
}
|
|
483
|
+
async get(key) {
|
|
484
|
+
const result = await this.tabularRepository.get({ key });
|
|
485
|
+
if (result) {
|
|
486
|
+
const schemaType = typeof this.valueSchema === "object" && this.valueSchema !== null && "type" in this.valueSchema ? this.valueSchema.type : undefined;
|
|
487
|
+
const shouldParse = !["number", "boolean", "string", "blob"].includes(schemaType);
|
|
488
|
+
if (shouldParse) {
|
|
489
|
+
try {
|
|
490
|
+
return JSON.parse(result.value);
|
|
491
|
+
} catch (e) {
|
|
492
|
+
return result.value;
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
return result.value;
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async delete(key) {
|
|
502
|
+
return await this.tabularRepository.delete({ key });
|
|
503
|
+
}
|
|
504
|
+
async getAll() {
|
|
505
|
+
const values = await this.tabularRepository.getAll();
|
|
506
|
+
if (values) {
|
|
507
|
+
return values.map((value) => ({
|
|
508
|
+
key: value.key,
|
|
509
|
+
value: (() => {
|
|
510
|
+
const schemaType = typeof this.valueSchema === "object" && this.valueSchema !== null && "type" in this.valueSchema ? this.valueSchema.type : undefined;
|
|
511
|
+
const shouldParse = !["number", "boolean", "string"].includes(schemaType);
|
|
512
|
+
if (shouldParse && typeof value.value === "string") {
|
|
513
|
+
try {
|
|
514
|
+
return JSON.parse(value.value);
|
|
515
|
+
} catch (e) {
|
|
516
|
+
return value.value;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return value.value;
|
|
520
|
+
})()
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async deleteAll() {
|
|
525
|
+
return await this.tabularRepository.deleteAll();
|
|
526
|
+
}
|
|
527
|
+
async size() {
|
|
528
|
+
return await this.tabularRepository.size();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/kv/InMemoryKvRepository.ts
|
|
533
|
+
var MEMORY_KV_REPOSITORY = createServiceToken5("storage.kvRepository.inMemory");
|
|
534
|
+
|
|
535
|
+
class InMemoryKvRepository extends KvViaTabularRepository {
|
|
536
|
+
tabularRepository;
|
|
537
|
+
constructor(keySchema = { type: "string" }, valueSchema = {}) {
|
|
538
|
+
super(keySchema, valueSchema);
|
|
539
|
+
this.tabularRepository = new InMemoryTabularRepository(DefaultKeyValueSchema, DefaultKeyValueKey);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
// src/queue/InMemoryQueueStorage.ts
|
|
543
|
+
import { createServiceToken as createServiceToken7, makeFingerprint as makeFingerprint4, sleep, uuid4 } from "@workglow/util";
|
|
544
|
+
|
|
545
|
+
// src/queue/IQueueStorage.ts
|
|
546
|
+
import { createServiceToken as createServiceToken6 } from "@workglow/util";
|
|
547
|
+
var QUEUE_STORAGE = createServiceToken6("jobqueue.storage");
|
|
548
|
+
var JobStatus;
|
|
549
|
+
((JobStatus2) => {
|
|
550
|
+
JobStatus2["PENDING"] = "PENDING";
|
|
551
|
+
JobStatus2["PROCESSING"] = "PROCESSING";
|
|
552
|
+
JobStatus2["COMPLETED"] = "COMPLETED";
|
|
553
|
+
JobStatus2["ABORTING"] = "ABORTING";
|
|
554
|
+
JobStatus2["FAILED"] = "FAILED";
|
|
555
|
+
JobStatus2["DISABLED"] = "DISABLED";
|
|
556
|
+
})(JobStatus ||= {});
|
|
557
|
+
|
|
558
|
+
// src/queue/InMemoryQueueStorage.ts
|
|
559
|
+
var IN_MEMORY_QUEUE_STORAGE = createServiceToken7("jobqueue.storage.inMemory");
|
|
560
|
+
|
|
561
|
+
class InMemoryQueueStorage {
|
|
562
|
+
queueName;
|
|
563
|
+
constructor(queueName) {
|
|
564
|
+
this.queueName = queueName;
|
|
565
|
+
this.jobQueue = [];
|
|
566
|
+
}
|
|
567
|
+
jobQueue;
|
|
568
|
+
pendingQueue() {
|
|
569
|
+
const now = new Date().toISOString();
|
|
570
|
+
return this.jobQueue.filter((job) => job.status === "PENDING" /* PENDING */).filter((job) => !job.run_after || job.run_after <= now).sort((a, b) => (a.run_after || "").localeCompare(b.run_after || ""));
|
|
571
|
+
}
|
|
572
|
+
async add(job) {
|
|
573
|
+
await sleep(0);
|
|
574
|
+
const now = new Date().toISOString();
|
|
575
|
+
job.id = job.id ?? uuid4();
|
|
576
|
+
job.job_run_id = job.job_run_id ?? uuid4();
|
|
577
|
+
job.queue = this.queueName;
|
|
578
|
+
job.fingerprint = await makeFingerprint4(job.input);
|
|
579
|
+
job.status = "PENDING" /* PENDING */;
|
|
580
|
+
job.progress = 0;
|
|
581
|
+
job.progress_message = "";
|
|
582
|
+
job.progress_details = null;
|
|
583
|
+
job.created_at = now;
|
|
584
|
+
job.run_after = now;
|
|
585
|
+
this.jobQueue.push(job);
|
|
586
|
+
return job.id;
|
|
587
|
+
}
|
|
588
|
+
async get(id) {
|
|
589
|
+
await sleep(0);
|
|
590
|
+
return this.jobQueue.find((j) => j.id === id);
|
|
591
|
+
}
|
|
592
|
+
async peek(status = "PENDING" /* PENDING */, num = 100) {
|
|
593
|
+
await sleep(0);
|
|
594
|
+
num = Number(num) || 100;
|
|
595
|
+
return this.jobQueue.sort((a, b) => (a.run_after || "").localeCompare(b.run_after || "")).filter((j) => j.status === status).slice(0, num);
|
|
596
|
+
}
|
|
597
|
+
async next() {
|
|
598
|
+
await sleep(0);
|
|
599
|
+
const top = this.pendingQueue();
|
|
600
|
+
const job = top[0];
|
|
601
|
+
if (job) {
|
|
602
|
+
job.status = "PROCESSING" /* PROCESSING */;
|
|
603
|
+
job.last_ran_at = new Date().toISOString();
|
|
604
|
+
return job;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
async size(status = "PENDING" /* PENDING */) {
|
|
608
|
+
await sleep(0);
|
|
609
|
+
return this.jobQueue.filter((j) => j.status === status).length;
|
|
610
|
+
}
|
|
611
|
+
async saveProgress(id, progress, message, details) {
|
|
612
|
+
await sleep(0);
|
|
613
|
+
const job = this.jobQueue.find((j) => j.id === id);
|
|
614
|
+
if (!job) {
|
|
615
|
+
throw new Error(`Job ${id} not found`);
|
|
616
|
+
}
|
|
617
|
+
job.progress = progress;
|
|
618
|
+
job.progress_message = message;
|
|
619
|
+
job.progress_details = details;
|
|
620
|
+
}
|
|
621
|
+
async complete(job) {
|
|
622
|
+
await sleep(0);
|
|
623
|
+
const index = this.jobQueue.findIndex((j) => j.id === job.id);
|
|
624
|
+
if (index !== -1) {
|
|
625
|
+
const existing = this.jobQueue[index];
|
|
626
|
+
const currentAttempts = existing?.run_attempts ?? 0;
|
|
627
|
+
job.run_attempts = currentAttempts + 1;
|
|
628
|
+
this.jobQueue[index] = job;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async abort(id) {
|
|
632
|
+
await sleep(0);
|
|
633
|
+
const job = this.jobQueue.find((j) => j.id === id);
|
|
634
|
+
if (job) {
|
|
635
|
+
job.status = "ABORTING" /* ABORTING */;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async getByRunId(runId) {
|
|
639
|
+
await sleep(0);
|
|
640
|
+
return this.jobQueue.filter((job) => job.job_run_id === runId);
|
|
641
|
+
}
|
|
642
|
+
async deleteAll() {
|
|
643
|
+
await sleep(0);
|
|
644
|
+
this.jobQueue = [];
|
|
645
|
+
}
|
|
646
|
+
async outputForInput(input) {
|
|
647
|
+
await sleep(0);
|
|
648
|
+
const fingerprint = await makeFingerprint4(input);
|
|
649
|
+
return this.jobQueue.find((j) => j.fingerprint === fingerprint && j.status === "COMPLETED" /* COMPLETED */)?.output ?? null;
|
|
650
|
+
}
|
|
651
|
+
async delete(id) {
|
|
652
|
+
await sleep(0);
|
|
653
|
+
this.jobQueue = this.jobQueue.filter((job) => job.id !== id);
|
|
654
|
+
}
|
|
655
|
+
async deleteJobsByStatusAndAge(status, olderThanMs) {
|
|
656
|
+
await sleep(0);
|
|
657
|
+
const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();
|
|
658
|
+
this.jobQueue = this.jobQueue.filter((job) => job.status !== status || !job.completed_at || job.completed_at > cutoffDate);
|
|
659
|
+
}
|
|
660
|
+
async setupDatabase() {}
|
|
661
|
+
}
|
|
662
|
+
// src/tabular/IndexedDbTabularRepository.ts
|
|
663
|
+
import { createServiceToken as createServiceToken8 } from "@workglow/util";
|
|
664
|
+
|
|
665
|
+
// src/util/IndexedDbTable.ts
|
|
666
|
+
var METADATA_STORE_NAME = "__schema_metadata__";
|
|
667
|
+
async function saveSchemaMetadata(db, tableName, snapshot) {
|
|
668
|
+
return new Promise((resolve, reject) => {
|
|
669
|
+
try {
|
|
670
|
+
const transaction = db.transaction(METADATA_STORE_NAME, "readwrite");
|
|
671
|
+
const store = transaction.objectStore(METADATA_STORE_NAME);
|
|
672
|
+
const request = store.put({ ...snapshot, tableName }, tableName);
|
|
673
|
+
request.onsuccess = () => resolve();
|
|
674
|
+
request.onerror = () => reject(request.error);
|
|
675
|
+
transaction.onerror = () => reject(transaction.error);
|
|
676
|
+
} catch (err) {
|
|
677
|
+
resolve();
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
async function loadSchemaMetadata(db, tableName) {
|
|
682
|
+
return new Promise((resolve) => {
|
|
683
|
+
try {
|
|
684
|
+
if (!db.objectStoreNames.contains(METADATA_STORE_NAME)) {
|
|
685
|
+
resolve(null);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const transaction = db.transaction(METADATA_STORE_NAME, "readonly");
|
|
689
|
+
const store = transaction.objectStore(METADATA_STORE_NAME);
|
|
690
|
+
const request = store.get(tableName);
|
|
691
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
692
|
+
request.onerror = () => resolve(null);
|
|
693
|
+
transaction.onerror = () => resolve(null);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
resolve(null);
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
async function openIndexedDbTable(tableName, version, upgradeNeededCallback) {
|
|
700
|
+
return new Promise((resolve, reject) => {
|
|
701
|
+
const openRequest = indexedDB.open(tableName, version);
|
|
702
|
+
openRequest.onsuccess = (event) => {
|
|
703
|
+
const db = event.target.result;
|
|
704
|
+
db.onversionchange = () => {
|
|
705
|
+
db.close();
|
|
706
|
+
};
|
|
707
|
+
resolve(db);
|
|
708
|
+
};
|
|
709
|
+
openRequest.onupgradeneeded = (event) => {
|
|
710
|
+
if (upgradeNeededCallback) {
|
|
711
|
+
upgradeNeededCallback(event);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
openRequest.onerror = () => {
|
|
715
|
+
const error = openRequest.error;
|
|
716
|
+
if (error && error.name === "VersionError") {
|
|
717
|
+
reject(new Error(`Database ${tableName} exists at a higher version. Cannot open at version ${version || "current"}.`));
|
|
718
|
+
} else {
|
|
719
|
+
reject(error);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
openRequest.onblocked = () => {
|
|
723
|
+
reject(new Error(`Database ${tableName} is blocked. Close all other tabs using this database.`));
|
|
724
|
+
};
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
async function deleteIndexedDbTable(tableName) {
|
|
728
|
+
return new Promise((resolve, reject) => {
|
|
729
|
+
const deleteRequest = indexedDB.deleteDatabase(tableName);
|
|
730
|
+
deleteRequest.onsuccess = () => resolve();
|
|
731
|
+
deleteRequest.onerror = () => reject(deleteRequest.error);
|
|
732
|
+
deleteRequest.onblocked = () => {
|
|
733
|
+
reject(new Error(`Cannot delete database ${tableName}. Close all other tabs using this database.`));
|
|
734
|
+
};
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function compareSchemas(store, expectedPrimaryKey, expectedIndexes) {
|
|
738
|
+
const diff = {
|
|
739
|
+
indexesToAdd: [],
|
|
740
|
+
indexesToRemove: [],
|
|
741
|
+
indexesToModify: [],
|
|
742
|
+
primaryKeyChanged: false,
|
|
743
|
+
needsObjectStoreRecreation: false
|
|
744
|
+
};
|
|
745
|
+
const actualKeyPath = store.keyPath;
|
|
746
|
+
const normalizedExpected = Array.isArray(expectedPrimaryKey) ? expectedPrimaryKey : expectedPrimaryKey;
|
|
747
|
+
const normalizedActual = Array.isArray(actualKeyPath) ? actualKeyPath : actualKeyPath;
|
|
748
|
+
if (JSON.stringify(normalizedExpected) !== JSON.stringify(normalizedActual)) {
|
|
749
|
+
diff.primaryKeyChanged = true;
|
|
750
|
+
diff.needsObjectStoreRecreation = true;
|
|
751
|
+
return diff;
|
|
752
|
+
}
|
|
753
|
+
const existingIndexes = new Map;
|
|
754
|
+
for (let i = 0;i < store.indexNames.length; i++) {
|
|
755
|
+
const indexName = store.indexNames[i];
|
|
756
|
+
existingIndexes.set(indexName, store.index(indexName));
|
|
757
|
+
}
|
|
758
|
+
for (const expectedIdx of expectedIndexes) {
|
|
759
|
+
const existingIdx = existingIndexes.get(expectedIdx.name);
|
|
760
|
+
if (!existingIdx) {
|
|
761
|
+
diff.indexesToAdd.push(expectedIdx);
|
|
762
|
+
} else {
|
|
763
|
+
const expectedKeyPath = Array.isArray(expectedIdx.keyPath) ? expectedIdx.keyPath : [expectedIdx.keyPath];
|
|
764
|
+
const actualKeyPath2 = Array.isArray(existingIdx.keyPath) ? existingIdx.keyPath : [existingIdx.keyPath];
|
|
765
|
+
const keyPathChanged = JSON.stringify(expectedKeyPath) !== JSON.stringify(actualKeyPath2);
|
|
766
|
+
const uniqueChanged = existingIdx.unique !== (expectedIdx.options?.unique ?? false);
|
|
767
|
+
const multiEntryChanged = existingIdx.multiEntry !== (expectedIdx.options?.multiEntry ?? false);
|
|
768
|
+
if (keyPathChanged || uniqueChanged || multiEntryChanged) {
|
|
769
|
+
diff.indexesToModify.push(expectedIdx);
|
|
770
|
+
}
|
|
771
|
+
existingIndexes.delete(expectedIdx.name);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
diff.indexesToRemove = Array.from(existingIndexes.keys());
|
|
775
|
+
return diff;
|
|
776
|
+
}
|
|
777
|
+
async function readAllData(store) {
|
|
778
|
+
return new Promise((resolve, reject) => {
|
|
779
|
+
const request = store.getAll();
|
|
780
|
+
request.onsuccess = () => resolve(request.result || []);
|
|
781
|
+
request.onerror = () => reject(request.error);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async function performIncrementalMigration(db, tableName, diff, options = {}) {
|
|
785
|
+
const currentVersion = db.version;
|
|
786
|
+
const newVersion = currentVersion + 1;
|
|
787
|
+
db.close();
|
|
788
|
+
options.onMigrationProgress?.(`Migrating ${tableName} from version ${currentVersion} to ${newVersion}...`, 0);
|
|
789
|
+
return openIndexedDbTable(tableName, newVersion, (event) => {
|
|
790
|
+
const db2 = event.target.result;
|
|
791
|
+
const transaction = event.target.transaction;
|
|
792
|
+
const store = transaction.objectStore(tableName);
|
|
793
|
+
for (const indexName of diff.indexesToRemove) {
|
|
794
|
+
options.onMigrationProgress?.(`Removing index: ${indexName}`, 0.2);
|
|
795
|
+
store.deleteIndex(indexName);
|
|
796
|
+
}
|
|
797
|
+
for (const indexDef of diff.indexesToModify) {
|
|
798
|
+
options.onMigrationProgress?.(`Updating index: ${indexDef.name}`, 0.4);
|
|
799
|
+
if (store.indexNames.contains(indexDef.name)) {
|
|
800
|
+
store.deleteIndex(indexDef.name);
|
|
801
|
+
}
|
|
802
|
+
store.createIndex(indexDef.name, indexDef.keyPath, indexDef.options);
|
|
803
|
+
}
|
|
804
|
+
for (const indexDef of diff.indexesToAdd) {
|
|
805
|
+
options.onMigrationProgress?.(`Adding index: ${indexDef.name}`, 0.6);
|
|
806
|
+
store.createIndex(indexDef.name, indexDef.keyPath, indexDef.options);
|
|
807
|
+
}
|
|
808
|
+
options.onMigrationProgress?.(`Migration complete`, 1);
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
async function performDestructiveMigration(db, tableName, primaryKey, expectedIndexes, options = {}) {
|
|
812
|
+
if (!options.allowDestructiveMigration) {
|
|
813
|
+
throw new Error(`Destructive migration required for ${tableName} but not allowed. ` + `Primary key has changed. Set allowDestructiveMigration=true to proceed with data loss, ` + `or provide a dataTransformer to migrate data.`);
|
|
814
|
+
}
|
|
815
|
+
const currentVersion = db.version;
|
|
816
|
+
const newVersion = currentVersion + 1;
|
|
817
|
+
options.onMigrationProgress?.(`Performing destructive migration of ${tableName}. Reading existing data...`, 0);
|
|
818
|
+
let existingData = [];
|
|
819
|
+
try {
|
|
820
|
+
const transaction = db.transaction(tableName, "readonly");
|
|
821
|
+
const store = transaction.objectStore(tableName);
|
|
822
|
+
existingData = await readAllData(store);
|
|
823
|
+
options.onMigrationProgress?.(`Read ${existingData.length} records`, 0.3);
|
|
824
|
+
} catch (err) {
|
|
825
|
+
options.onMigrationWarning?.(`Failed to read existing data during migration: ${err}`, err);
|
|
826
|
+
}
|
|
827
|
+
db.close();
|
|
828
|
+
if (options.dataTransformer && existingData.length > 0) {
|
|
829
|
+
options.onMigrationProgress?.(`Transforming ${existingData.length} records...`, 0.4);
|
|
830
|
+
try {
|
|
831
|
+
const transformed = [];
|
|
832
|
+
for (let i = 0;i < existingData.length; i++) {
|
|
833
|
+
const record = existingData[i];
|
|
834
|
+
const transformedRecord = await options.dataTransformer(record);
|
|
835
|
+
if (transformedRecord !== undefined && transformedRecord !== null) {
|
|
836
|
+
transformed.push(transformedRecord);
|
|
837
|
+
}
|
|
838
|
+
if (i % 100 === 0) {
|
|
839
|
+
options.onMigrationProgress?.(`Transformed ${i}/${existingData.length} records`, 0.4 + i / existingData.length * 0.3);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
existingData = transformed;
|
|
843
|
+
options.onMigrationProgress?.(`Transformation complete: ${existingData.length} records`, 0.7);
|
|
844
|
+
} catch (err) {
|
|
845
|
+
options.onMigrationWarning?.(`Data transformation failed: ${err}. Some data may be lost.`, err);
|
|
846
|
+
existingData = [];
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
options.onMigrationProgress?.(`Recreating object store...`, 0.75);
|
|
850
|
+
const newDb = await openIndexedDbTable(tableName, newVersion, (event) => {
|
|
851
|
+
const db2 = event.target.result;
|
|
852
|
+
const transaction = event.target.transaction;
|
|
853
|
+
if (db2.objectStoreNames.contains(tableName)) {
|
|
854
|
+
db2.deleteObjectStore(tableName);
|
|
855
|
+
}
|
|
856
|
+
const store = db2.createObjectStore(tableName, { keyPath: primaryKey });
|
|
857
|
+
for (const idx of expectedIndexes) {
|
|
858
|
+
store.createIndex(idx.name, idx.keyPath, idx.options);
|
|
859
|
+
}
|
|
860
|
+
if (existingData.length > 0) {
|
|
861
|
+
options.onMigrationProgress?.(`Restoring ${existingData.length} records...`, 0.8);
|
|
862
|
+
for (const record of existingData) {
|
|
863
|
+
try {
|
|
864
|
+
store.put(record);
|
|
865
|
+
} catch (err) {
|
|
866
|
+
options.onMigrationWarning?.(`Failed to restore record: ${err}`, err);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
options.onMigrationProgress?.(`Destructive migration complete`, 1);
|
|
872
|
+
return newDb;
|
|
873
|
+
}
|
|
874
|
+
async function createNewDatabase(tableName, primaryKey, expectedIndexes, options = {}) {
|
|
875
|
+
options.onMigrationProgress?.(`Creating new database: ${tableName}`, 0);
|
|
876
|
+
try {
|
|
877
|
+
await deleteIndexedDbTable(tableName);
|
|
878
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
879
|
+
} catch (err) {}
|
|
880
|
+
const version = 1;
|
|
881
|
+
const db = await openIndexedDbTable(tableName, version, (event) => {
|
|
882
|
+
const db2 = event.target.result;
|
|
883
|
+
if (!db2.objectStoreNames.contains(METADATA_STORE_NAME)) {
|
|
884
|
+
db2.createObjectStore(METADATA_STORE_NAME, { keyPath: "tableName" });
|
|
885
|
+
}
|
|
886
|
+
const store = db2.createObjectStore(tableName, { keyPath: primaryKey });
|
|
887
|
+
for (const idx of expectedIndexes) {
|
|
888
|
+
store.createIndex(idx.name, idx.keyPath, idx.options);
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
const snapshot = {
|
|
892
|
+
version: db.version,
|
|
893
|
+
primaryKey,
|
|
894
|
+
indexes: expectedIndexes,
|
|
895
|
+
recordCount: 0,
|
|
896
|
+
timestamp: Date.now()
|
|
897
|
+
};
|
|
898
|
+
await saveSchemaMetadata(db, tableName, snapshot);
|
|
899
|
+
options.onMigrationProgress?.(`Database created successfully`, 1);
|
|
900
|
+
return db;
|
|
901
|
+
}
|
|
902
|
+
async function ensureIndexedDbTable(tableName, primaryKey, expectedIndexes = [], options = {}) {
|
|
903
|
+
try {
|
|
904
|
+
let db;
|
|
905
|
+
let wasJustCreated = false;
|
|
906
|
+
try {
|
|
907
|
+
db = await openIndexedDbTable(tableName);
|
|
908
|
+
if (db.version === 1 && !db.objectStoreNames.contains(tableName)) {
|
|
909
|
+
wasJustCreated = true;
|
|
910
|
+
db.close();
|
|
911
|
+
}
|
|
912
|
+
} catch (err) {
|
|
913
|
+
options.onMigrationProgress?.(`Database ${tableName} does not exist or has version conflict, creating...`, 0);
|
|
914
|
+
return await createNewDatabase(tableName, primaryKey, expectedIndexes, options);
|
|
915
|
+
}
|
|
916
|
+
if (wasJustCreated) {
|
|
917
|
+
options.onMigrationProgress?.(`Creating new database: ${tableName}`, 0);
|
|
918
|
+
try {
|
|
919
|
+
await deleteIndexedDbTable(tableName);
|
|
920
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
921
|
+
} catch (err) {}
|
|
922
|
+
db = await openIndexedDbTable(tableName, 1, (event) => {
|
|
923
|
+
const db2 = event.target.result;
|
|
924
|
+
if (!db2.objectStoreNames.contains(METADATA_STORE_NAME)) {
|
|
925
|
+
db2.createObjectStore(METADATA_STORE_NAME, { keyPath: "tableName" });
|
|
926
|
+
}
|
|
927
|
+
const store2 = db2.createObjectStore(tableName, { keyPath: primaryKey });
|
|
928
|
+
for (const idx of expectedIndexes) {
|
|
929
|
+
store2.createIndex(idx.name, idx.keyPath, idx.options);
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
const snapshot2 = {
|
|
933
|
+
version: db.version,
|
|
934
|
+
primaryKey,
|
|
935
|
+
indexes: expectedIndexes,
|
|
936
|
+
recordCount: 0,
|
|
937
|
+
timestamp: Date.now()
|
|
938
|
+
};
|
|
939
|
+
await saveSchemaMetadata(db, tableName, snapshot2);
|
|
940
|
+
options.onMigrationProgress?.(`Database created successfully`, 1);
|
|
941
|
+
return db;
|
|
942
|
+
}
|
|
943
|
+
if (!db.objectStoreNames.contains(METADATA_STORE_NAME)) {
|
|
944
|
+
const currentVersion = db.version;
|
|
945
|
+
db.close();
|
|
946
|
+
db = await openIndexedDbTable(tableName, currentVersion + 1, (event) => {
|
|
947
|
+
const db2 = event.target.result;
|
|
948
|
+
if (!db2.objectStoreNames.contains(METADATA_STORE_NAME)) {
|
|
949
|
+
db2.createObjectStore(METADATA_STORE_NAME, { keyPath: "tableName" });
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
const metadata = await loadSchemaMetadata(db, tableName);
|
|
954
|
+
if (!db.objectStoreNames.contains(tableName)) {
|
|
955
|
+
options.onMigrationProgress?.(`Object store ${tableName} does not exist, creating...`, 0);
|
|
956
|
+
db.close();
|
|
957
|
+
return await createNewDatabase(tableName, primaryKey, expectedIndexes, options);
|
|
958
|
+
}
|
|
959
|
+
const transaction = db.transaction(tableName, "readonly");
|
|
960
|
+
const store = transaction.objectStore(tableName);
|
|
961
|
+
const diff = compareSchemas(store, primaryKey, expectedIndexes);
|
|
962
|
+
await new Promise((resolve) => {
|
|
963
|
+
transaction.oncomplete = () => resolve();
|
|
964
|
+
transaction.onerror = () => resolve();
|
|
965
|
+
});
|
|
966
|
+
const needsMigration = diff.indexesToAdd.length > 0 || diff.indexesToRemove.length > 0 || diff.indexesToModify.length > 0 || diff.needsObjectStoreRecreation;
|
|
967
|
+
if (!needsMigration) {
|
|
968
|
+
options.onMigrationProgress?.(`Schema for ${tableName} is up to date`, 1);
|
|
969
|
+
const snapshot2 = {
|
|
970
|
+
version: db.version,
|
|
971
|
+
primaryKey,
|
|
972
|
+
indexes: expectedIndexes,
|
|
973
|
+
timestamp: Date.now()
|
|
974
|
+
};
|
|
975
|
+
await saveSchemaMetadata(db, tableName, snapshot2);
|
|
976
|
+
return db;
|
|
977
|
+
}
|
|
978
|
+
if (diff.needsObjectStoreRecreation) {
|
|
979
|
+
options.onMigrationProgress?.(`Schema change requires object store recreation for ${tableName}`, 0);
|
|
980
|
+
db = await performDestructiveMigration(db, tableName, primaryKey, expectedIndexes, options);
|
|
981
|
+
} else {
|
|
982
|
+
options.onMigrationProgress?.(`Performing incremental migration for ${tableName}`, 0);
|
|
983
|
+
db = await performIncrementalMigration(db, tableName, diff, options);
|
|
984
|
+
}
|
|
985
|
+
const snapshot = {
|
|
986
|
+
version: db.version,
|
|
987
|
+
primaryKey,
|
|
988
|
+
indexes: expectedIndexes,
|
|
989
|
+
timestamp: Date.now()
|
|
990
|
+
};
|
|
991
|
+
await saveSchemaMetadata(db, tableName, snapshot);
|
|
992
|
+
return db;
|
|
993
|
+
} catch (err) {
|
|
994
|
+
options.onMigrationWarning?.(`Migration failed for ${tableName}: ${err}`, err);
|
|
995
|
+
throw err;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async function dropIndexedDbTable(tableName) {
|
|
999
|
+
return deleteIndexedDbTable(tableName);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// src/tabular/IndexedDbTabularRepository.ts
|
|
1003
|
+
var IDB_TABULAR_REPOSITORY = createServiceToken8("storage.tabularRepository.indexedDb");
|
|
1004
|
+
|
|
1005
|
+
class IndexedDbTabularRepository extends TabularRepository {
|
|
1006
|
+
table;
|
|
1007
|
+
db;
|
|
1008
|
+
migrationOptions;
|
|
1009
|
+
constructor(table = "tabular_store", schema, primaryKeyNames, indexes = [], migrationOptions = {}) {
|
|
1010
|
+
super(schema, primaryKeyNames, indexes);
|
|
1011
|
+
this.table = table;
|
|
1012
|
+
this.migrationOptions = migrationOptions;
|
|
1013
|
+
}
|
|
1014
|
+
async setupDatabase() {
|
|
1015
|
+
if (this.db)
|
|
1016
|
+
return this.db;
|
|
1017
|
+
const pkColumns = super.primaryKeyColumns();
|
|
1018
|
+
const expectedIndexes = [];
|
|
1019
|
+
for (const spec of this.indexes) {
|
|
1020
|
+
const columns = spec;
|
|
1021
|
+
if (columns.length <= pkColumns.length) {
|
|
1022
|
+
const isPkPrefix = columns.every((col, idx) => col === pkColumns[idx]);
|
|
1023
|
+
if (isPkPrefix)
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const columnNames = columns.map((col) => String(col));
|
|
1027
|
+
const indexName = columnNames.join("_");
|
|
1028
|
+
expectedIndexes.push({
|
|
1029
|
+
name: indexName,
|
|
1030
|
+
keyPath: columnNames.length === 1 ? columnNames[0] : columnNames,
|
|
1031
|
+
options: { unique: false }
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
const primaryKey = pkColumns.length === 1 ? pkColumns[0] : pkColumns;
|
|
1035
|
+
this.db = await ensureIndexedDbTable(this.table, primaryKey, expectedIndexes, this.migrationOptions);
|
|
1036
|
+
return this.db;
|
|
1037
|
+
}
|
|
1038
|
+
async put(record) {
|
|
1039
|
+
const db = await this.setupDatabase();
|
|
1040
|
+
const { key } = this.separateKeyValueFromCombined(record);
|
|
1041
|
+
return new Promise((resolve, reject) => {
|
|
1042
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
1043
|
+
const store = transaction.objectStore(this.table);
|
|
1044
|
+
const request = store.put(record);
|
|
1045
|
+
request.onerror = () => {
|
|
1046
|
+
reject(request.error);
|
|
1047
|
+
};
|
|
1048
|
+
request.onsuccess = () => {
|
|
1049
|
+
this.events.emit("put", record);
|
|
1050
|
+
resolve(record);
|
|
1051
|
+
};
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
async putBulk(records) {
|
|
1055
|
+
const db = await this.setupDatabase();
|
|
1056
|
+
return new Promise((resolve, reject) => {
|
|
1057
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
1058
|
+
const store = transaction.objectStore(this.table);
|
|
1059
|
+
let completed = 0;
|
|
1060
|
+
let hasError = false;
|
|
1061
|
+
transaction.onerror = () => {
|
|
1062
|
+
if (!hasError) {
|
|
1063
|
+
hasError = true;
|
|
1064
|
+
reject(transaction.error);
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
transaction.oncomplete = () => {
|
|
1068
|
+
if (!hasError) {
|
|
1069
|
+
resolve(records);
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
for (const record of records) {
|
|
1073
|
+
const request = store.put(record);
|
|
1074
|
+
request.onsuccess = () => {
|
|
1075
|
+
this.events.emit("put", record);
|
|
1076
|
+
completed++;
|
|
1077
|
+
};
|
|
1078
|
+
request.onerror = () => {
|
|
1079
|
+
if (!hasError) {
|
|
1080
|
+
hasError = true;
|
|
1081
|
+
reject(request.error);
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
getPrimaryKeyAsOrderedArray(key) {
|
|
1088
|
+
return super.getPrimaryKeyAsOrderedArray(key).map((value) => typeof value === "bigint" ? value.toString() : value);
|
|
1089
|
+
}
|
|
1090
|
+
getIndexedKey(key) {
|
|
1091
|
+
const keys = super.getPrimaryKeyAsOrderedArray(key).map((value) => typeof value === "bigint" ? value.toString() : value);
|
|
1092
|
+
return keys.length === 1 ? keys[0] : keys;
|
|
1093
|
+
}
|
|
1094
|
+
async get(key) {
|
|
1095
|
+
const db = await this.setupDatabase();
|
|
1096
|
+
return new Promise((resolve, reject) => {
|
|
1097
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
1098
|
+
const store = transaction.objectStore(this.table);
|
|
1099
|
+
const request = store.get(this.getIndexedKey(key));
|
|
1100
|
+
request.onerror = () => reject(request.error);
|
|
1101
|
+
request.onsuccess = () => {
|
|
1102
|
+
if (!request.result) {
|
|
1103
|
+
this.events.emit("get", key, undefined);
|
|
1104
|
+
resolve(undefined);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
this.events.emit("get", key, request.result);
|
|
1108
|
+
resolve(request.result);
|
|
1109
|
+
};
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
async getAll() {
|
|
1113
|
+
const db = await this.setupDatabase();
|
|
1114
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
1115
|
+
const store = transaction.objectStore(this.table);
|
|
1116
|
+
const request = store.getAll();
|
|
1117
|
+
return new Promise((resolve, reject) => {
|
|
1118
|
+
request.onerror = () => reject(request.error);
|
|
1119
|
+
request.onsuccess = () => {
|
|
1120
|
+
const values = request.result;
|
|
1121
|
+
resolve(values.length > 0 ? values : undefined);
|
|
1122
|
+
};
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
async search(key) {
|
|
1126
|
+
const db = await this.setupDatabase();
|
|
1127
|
+
const searchKeys = Object.keys(key);
|
|
1128
|
+
if (searchKeys.length === 0) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const bestIndex = this.findBestMatchingIndex(searchKeys);
|
|
1132
|
+
if (!bestIndex) {
|
|
1133
|
+
throw new Error("No suitable index found for the search criteria");
|
|
1134
|
+
}
|
|
1135
|
+
return new Promise((resolve, reject) => {
|
|
1136
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
1137
|
+
const store = transaction.objectStore(this.table);
|
|
1138
|
+
const indexName = bestIndex.join("_");
|
|
1139
|
+
const primaryKeyName = this.primaryKeyColumns().join("_");
|
|
1140
|
+
const isPrimaryKey = indexName === primaryKeyName;
|
|
1141
|
+
const indexValues = [];
|
|
1142
|
+
for (const col of bestIndex) {
|
|
1143
|
+
const val = key[col];
|
|
1144
|
+
if (val === undefined)
|
|
1145
|
+
break;
|
|
1146
|
+
if (typeof val !== "string" && typeof val !== "number") {
|
|
1147
|
+
throw new Error(`Invalid value type for indexed column ${String(col)}`);
|
|
1148
|
+
}
|
|
1149
|
+
indexValues.push(val);
|
|
1150
|
+
}
|
|
1151
|
+
if (indexValues.length > 0) {
|
|
1152
|
+
const index = isPrimaryKey ? store : store.index(indexName);
|
|
1153
|
+
const isPartialMatch = indexValues.length < bestIndex.length;
|
|
1154
|
+
if (isPartialMatch) {
|
|
1155
|
+
const allColumnsRequired = bestIndex.every((col) => {
|
|
1156
|
+
const colName = String(col);
|
|
1157
|
+
return this.schema.required?.includes(colName);
|
|
1158
|
+
});
|
|
1159
|
+
if (allColumnsRequired) {
|
|
1160
|
+
const results = [];
|
|
1161
|
+
const keyRange = IDBKeyRange.lowerBound(indexValues);
|
|
1162
|
+
const cursorRequest = index.openCursor(keyRange);
|
|
1163
|
+
cursorRequest.onsuccess = () => {
|
|
1164
|
+
const cursor = cursorRequest.result;
|
|
1165
|
+
if (cursor) {
|
|
1166
|
+
const item = cursor.value;
|
|
1167
|
+
const cursorKey = Array.isArray(cursor.key) ? cursor.key : [cursor.key];
|
|
1168
|
+
const prefixMatches = indexValues.every((val, idx) => cursorKey[idx] === val);
|
|
1169
|
+
if (!prefixMatches) {
|
|
1170
|
+
resolve(results.length > 0 ? results : undefined);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const matches = Object.entries(key).every(([k, v]) => item[k] === v);
|
|
1174
|
+
if (matches) {
|
|
1175
|
+
results.push(item);
|
|
1176
|
+
}
|
|
1177
|
+
cursor.continue();
|
|
1178
|
+
} else {
|
|
1179
|
+
resolve(results.length > 0 ? results : undefined);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
cursorRequest.onerror = () => {
|
|
1183
|
+
reject(cursorRequest.error);
|
|
1184
|
+
};
|
|
1185
|
+
} else {
|
|
1186
|
+
const getAllRequest = store.getAll();
|
|
1187
|
+
getAllRequest.onsuccess = () => {
|
|
1188
|
+
const allRecords = getAllRequest.result;
|
|
1189
|
+
const results = allRecords.filter((item) => Object.entries(key).every(([k, v]) => item[k] === v));
|
|
1190
|
+
resolve(results.length > 0 ? results : undefined);
|
|
1191
|
+
};
|
|
1192
|
+
getAllRequest.onerror = () => {
|
|
1193
|
+
reject(getAllRequest.error);
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
} else {
|
|
1197
|
+
const request = index.getAll(indexValues.length === 1 ? indexValues[0] : indexValues);
|
|
1198
|
+
request.onsuccess = () => {
|
|
1199
|
+
const results = request.result.filter((item) => Object.entries(key).every(([k, v]) => item[k] === v));
|
|
1200
|
+
resolve(results.length > 0 ? results : undefined);
|
|
1201
|
+
};
|
|
1202
|
+
request.onerror = () => {
|
|
1203
|
+
console.error("Search error:", request.error);
|
|
1204
|
+
reject(request.error);
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
} else {
|
|
1208
|
+
throw new Error(`No valid values provided for indexed columns: ${bestIndex.join(", ")}`);
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
async delete(key) {
|
|
1213
|
+
const db = await this.setupDatabase();
|
|
1214
|
+
return new Promise((resolve, reject) => {
|
|
1215
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
1216
|
+
const store = transaction.objectStore(this.table);
|
|
1217
|
+
const request = store.delete(this.getIndexedKey(key));
|
|
1218
|
+
request.onerror = () => reject(request.error);
|
|
1219
|
+
request.onsuccess = () => {
|
|
1220
|
+
this.events.emit("delete", key);
|
|
1221
|
+
resolve();
|
|
1222
|
+
};
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
async deleteAll() {
|
|
1226
|
+
const db = await this.setupDatabase();
|
|
1227
|
+
return new Promise((resolve, reject) => {
|
|
1228
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
1229
|
+
const store = transaction.objectStore(this.table);
|
|
1230
|
+
const request = store.clear();
|
|
1231
|
+
request.onerror = () => reject(request.error);
|
|
1232
|
+
request.onsuccess = () => {
|
|
1233
|
+
this.events.emit("clearall");
|
|
1234
|
+
resolve();
|
|
1235
|
+
};
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
async size() {
|
|
1239
|
+
const db = await this.setupDatabase();
|
|
1240
|
+
return new Promise((resolve, reject) => {
|
|
1241
|
+
const transaction = db.transaction(this.table, "readonly");
|
|
1242
|
+
const store = transaction.objectStore(this.table);
|
|
1243
|
+
const request = store.count();
|
|
1244
|
+
request.onerror = () => reject(request.error);
|
|
1245
|
+
request.onsuccess = () => resolve(request.result);
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
async deleteSearch(column, value, operator = "=") {
|
|
1249
|
+
const db = await this.setupDatabase();
|
|
1250
|
+
return new Promise(async (resolve, reject) => {
|
|
1251
|
+
try {
|
|
1252
|
+
if (operator === "=") {
|
|
1253
|
+
const searchKey = { [column]: value };
|
|
1254
|
+
const recordsToDelete = await this.search(searchKey);
|
|
1255
|
+
if (!recordsToDelete || recordsToDelete.length === 0) {
|
|
1256
|
+
this.events.emit("delete", column);
|
|
1257
|
+
resolve();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
1261
|
+
const store = transaction.objectStore(this.table);
|
|
1262
|
+
transaction.oncomplete = () => {
|
|
1263
|
+
this.events.emit("delete", column);
|
|
1264
|
+
resolve();
|
|
1265
|
+
};
|
|
1266
|
+
transaction.onerror = () => {
|
|
1267
|
+
reject(transaction.error);
|
|
1268
|
+
};
|
|
1269
|
+
for (const record of recordsToDelete) {
|
|
1270
|
+
const primaryKey = this.primaryKeyColumns().reduce((key, column2) => {
|
|
1271
|
+
key[column2] = record[column2];
|
|
1272
|
+
return key;
|
|
1273
|
+
}, {});
|
|
1274
|
+
const request = store.delete(this.getIndexedKey(primaryKey));
|
|
1275
|
+
request.onerror = () => {
|
|
1276
|
+
console.error("Error deleting record:", request.error);
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
} else {
|
|
1280
|
+
const transaction = db.transaction(this.table, "readwrite");
|
|
1281
|
+
const store = transaction.objectStore(this.table);
|
|
1282
|
+
transaction.oncomplete = () => {
|
|
1283
|
+
this.events.emit("delete", column);
|
|
1284
|
+
resolve();
|
|
1285
|
+
};
|
|
1286
|
+
transaction.onerror = () => {
|
|
1287
|
+
reject(transaction.error);
|
|
1288
|
+
};
|
|
1289
|
+
const getAllRequest = store.getAll();
|
|
1290
|
+
getAllRequest.onsuccess = () => {
|
|
1291
|
+
const allRecords = getAllRequest.result;
|
|
1292
|
+
const recordsToDelete = allRecords.filter((record) => {
|
|
1293
|
+
const recordValue = record[column];
|
|
1294
|
+
if (recordValue === null || recordValue === undefined || value === null || value === undefined) {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
switch (operator) {
|
|
1298
|
+
case "<":
|
|
1299
|
+
return recordValue < value;
|
|
1300
|
+
case "<=":
|
|
1301
|
+
return recordValue <= value;
|
|
1302
|
+
case ">":
|
|
1303
|
+
return recordValue > value;
|
|
1304
|
+
case ">=":
|
|
1305
|
+
return recordValue >= value;
|
|
1306
|
+
default:
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
if (recordsToDelete.length === 0) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
for (const record of recordsToDelete) {
|
|
1314
|
+
const primaryKey = this.primaryKeyColumns().reduce((key, column2) => {
|
|
1315
|
+
key[column2] = record[column2];
|
|
1316
|
+
return key;
|
|
1317
|
+
}, {});
|
|
1318
|
+
const request = store.delete(this.getIndexedKey(primaryKey));
|
|
1319
|
+
request.onerror = () => {
|
|
1320
|
+
console.error("Error deleting record:", request.error);
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
getAllRequest.onerror = () => {
|
|
1325
|
+
reject(getAllRequest.error);
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
reject(error);
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
destroy() {
|
|
1334
|
+
this.db?.close();
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
// src/tabular/SharedInMemoryTabularRepository.ts
|
|
1338
|
+
import { createServiceToken as createServiceToken9 } from "@workglow/util";
|
|
1339
|
+
var SHARED_IN_MEMORY_TABULAR_REPOSITORY = createServiceToken9("storage.tabularRepository.sharedInMemory");
|
|
1340
|
+
|
|
1341
|
+
class SharedInMemoryTabularRepository extends TabularRepository {
|
|
1342
|
+
channel = null;
|
|
1343
|
+
channelName;
|
|
1344
|
+
inMemoryRepo;
|
|
1345
|
+
isInitialized = false;
|
|
1346
|
+
syncInProgress = false;
|
|
1347
|
+
constructor(channelName = "tabular_store", schema, primaryKeyNames, indexes = []) {
|
|
1348
|
+
super(schema, primaryKeyNames, indexes);
|
|
1349
|
+
this.channelName = channelName;
|
|
1350
|
+
this.inMemoryRepo = new InMemoryTabularRepository(schema, primaryKeyNames, indexes);
|
|
1351
|
+
this.setupEventForwarding();
|
|
1352
|
+
this.initializeBroadcastChannel();
|
|
1353
|
+
}
|
|
1354
|
+
isBroadcastChannelAvailable() {
|
|
1355
|
+
return typeof BroadcastChannel !== "undefined";
|
|
1356
|
+
}
|
|
1357
|
+
initializeBroadcastChannel() {
|
|
1358
|
+
if (!this.isBroadcastChannelAvailable()) {
|
|
1359
|
+
console.warn("BroadcastChannel is not available. Tab synchronization will not work.");
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
try {
|
|
1363
|
+
this.channel = new BroadcastChannel(this.channelName);
|
|
1364
|
+
this.channel.onmessage = (event) => {
|
|
1365
|
+
this.handleBroadcastMessage(event.data);
|
|
1366
|
+
};
|
|
1367
|
+
this.syncFromOtherTabs();
|
|
1368
|
+
} catch (error) {
|
|
1369
|
+
console.error("Failed to initialize BroadcastChannel:", error);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
setupEventForwarding() {
|
|
1373
|
+
this.inMemoryRepo.on("put", (entity) => {
|
|
1374
|
+
this.events.emit("put", entity);
|
|
1375
|
+
});
|
|
1376
|
+
this.inMemoryRepo.on("get", (key, entity) => {
|
|
1377
|
+
this.events.emit("get", key, entity);
|
|
1378
|
+
});
|
|
1379
|
+
this.inMemoryRepo.on("search", (key, entities) => {
|
|
1380
|
+
this.events.emit("search", key, entities);
|
|
1381
|
+
});
|
|
1382
|
+
this.inMemoryRepo.on("delete", (key) => {
|
|
1383
|
+
this.events.emit("delete", key);
|
|
1384
|
+
});
|
|
1385
|
+
this.inMemoryRepo.on("clearall", () => {
|
|
1386
|
+
this.events.emit("clearall");
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
async handleBroadcastMessage(message) {
|
|
1390
|
+
if (this.syncInProgress && message.type !== "SYNC_RESPONSE") {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
switch (message.type) {
|
|
1394
|
+
case "SYNC_REQUEST":
|
|
1395
|
+
const all = await this.inMemoryRepo.getAll();
|
|
1396
|
+
if (this.channel && all) {
|
|
1397
|
+
this.channel.postMessage({
|
|
1398
|
+
type: "SYNC_RESPONSE",
|
|
1399
|
+
data: all
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
break;
|
|
1403
|
+
case "SYNC_RESPONSE":
|
|
1404
|
+
if (message.data && Array.isArray(message.data)) {
|
|
1405
|
+
await this.copyDataFromArray(message.data);
|
|
1406
|
+
}
|
|
1407
|
+
this.syncInProgress = false;
|
|
1408
|
+
break;
|
|
1409
|
+
case "PUT":
|
|
1410
|
+
await this.inMemoryRepo.put(message.entity);
|
|
1411
|
+
break;
|
|
1412
|
+
case "PUT_BULK":
|
|
1413
|
+
await this.inMemoryRepo.putBulk(message.entities);
|
|
1414
|
+
break;
|
|
1415
|
+
case "DELETE":
|
|
1416
|
+
await this.inMemoryRepo.delete(message.key);
|
|
1417
|
+
break;
|
|
1418
|
+
case "DELETE_ALL":
|
|
1419
|
+
await this.inMemoryRepo.deleteAll();
|
|
1420
|
+
break;
|
|
1421
|
+
case "DELETE_SEARCH":
|
|
1422
|
+
await this.inMemoryRepo.deleteSearch(message.column, message.value, message.operator);
|
|
1423
|
+
break;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
syncFromOtherTabs() {
|
|
1427
|
+
if (!this.channel)
|
|
1428
|
+
return;
|
|
1429
|
+
this.syncInProgress = true;
|
|
1430
|
+
this.channel.postMessage({ type: "SYNC_REQUEST" });
|
|
1431
|
+
setTimeout(() => {
|
|
1432
|
+
this.syncInProgress = false;
|
|
1433
|
+
}, 1000);
|
|
1434
|
+
}
|
|
1435
|
+
async copyDataFromArray(entities) {
|
|
1436
|
+
if (entities.length === 0)
|
|
1437
|
+
return;
|
|
1438
|
+
await this.inMemoryRepo.deleteAll();
|
|
1439
|
+
await this.inMemoryRepo.putBulk(entities);
|
|
1440
|
+
}
|
|
1441
|
+
broadcast(message) {
|
|
1442
|
+
if (this.channel) {
|
|
1443
|
+
this.channel.postMessage(message);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
async setupDatabase() {
|
|
1447
|
+
if (this.isInitialized)
|
|
1448
|
+
return;
|
|
1449
|
+
this.isInitialized = true;
|
|
1450
|
+
await this.syncFromOtherTabs();
|
|
1451
|
+
}
|
|
1452
|
+
async put(value) {
|
|
1453
|
+
await this.setupDatabase();
|
|
1454
|
+
const result = await this.inMemoryRepo.put(value);
|
|
1455
|
+
this.broadcast({ type: "PUT", entity: value });
|
|
1456
|
+
return result;
|
|
1457
|
+
}
|
|
1458
|
+
async putBulk(values) {
|
|
1459
|
+
await this.setupDatabase();
|
|
1460
|
+
const result = await this.inMemoryRepo.putBulk(values);
|
|
1461
|
+
this.broadcast({ type: "PUT_BULK", entities: values });
|
|
1462
|
+
return result;
|
|
1463
|
+
}
|
|
1464
|
+
async get(key) {
|
|
1465
|
+
await this.setupDatabase();
|
|
1466
|
+
return await this.inMemoryRepo.get(key);
|
|
1467
|
+
}
|
|
1468
|
+
async search(key) {
|
|
1469
|
+
await this.setupDatabase();
|
|
1470
|
+
return await this.inMemoryRepo.search(key);
|
|
1471
|
+
}
|
|
1472
|
+
async delete(value) {
|
|
1473
|
+
await this.setupDatabase();
|
|
1474
|
+
await this.inMemoryRepo.delete(value);
|
|
1475
|
+
const { key } = this.separateKeyValueFromCombined(value);
|
|
1476
|
+
this.broadcast({ type: "DELETE", key });
|
|
1477
|
+
}
|
|
1478
|
+
async deleteAll() {
|
|
1479
|
+
await this.setupDatabase();
|
|
1480
|
+
await this.inMemoryRepo.deleteAll();
|
|
1481
|
+
this.broadcast({ type: "DELETE_ALL" });
|
|
1482
|
+
}
|
|
1483
|
+
async getAll() {
|
|
1484
|
+
await this.setupDatabase();
|
|
1485
|
+
return await this.inMemoryRepo.getAll();
|
|
1486
|
+
}
|
|
1487
|
+
async size() {
|
|
1488
|
+
await this.setupDatabase();
|
|
1489
|
+
return await this.inMemoryRepo.size();
|
|
1490
|
+
}
|
|
1491
|
+
async deleteSearch(column, value, operator = "=") {
|
|
1492
|
+
await this.setupDatabase();
|
|
1493
|
+
await this.inMemoryRepo.deleteSearch(column, value, operator);
|
|
1494
|
+
this.broadcast({
|
|
1495
|
+
type: "DELETE_SEARCH",
|
|
1496
|
+
column: String(column),
|
|
1497
|
+
value,
|
|
1498
|
+
operator
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
destroy() {
|
|
1502
|
+
if (this.channel) {
|
|
1503
|
+
this.channel.close();
|
|
1504
|
+
this.channel = null;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
// src/tabular/SupabaseTabularRepository.ts
|
|
1509
|
+
import { createServiceToken as createServiceToken10 } from "@workglow/util";
|
|
1510
|
+
|
|
1511
|
+
// src/tabular/BaseSqlTabularRepository.ts
|
|
1512
|
+
class BaseSqlTabularRepository extends TabularRepository {
|
|
1513
|
+
table;
|
|
1514
|
+
constructor(table = "tabular_store", schema, primaryKeyNames, indexes = []) {
|
|
1515
|
+
super(schema, primaryKeyNames, indexes);
|
|
1516
|
+
this.table = table;
|
|
1517
|
+
this.validateTableAndSchema();
|
|
1518
|
+
}
|
|
1519
|
+
constructPrimaryKeyColumns($delimiter = "") {
|
|
1520
|
+
const cols = Object.entries(this.primaryKeySchema.properties).map(([key, typeDef]) => {
|
|
1521
|
+
const sqlType = this.mapTypeToSQL(typeDef);
|
|
1522
|
+
return `${$delimiter}${key}${$delimiter} ${sqlType} NOT NULL`;
|
|
1523
|
+
}).join(", ");
|
|
1524
|
+
return cols;
|
|
1525
|
+
}
|
|
1526
|
+
constructValueColumns($delimiter = "") {
|
|
1527
|
+
const requiredSet = new Set(this.valueSchema.required ?? []);
|
|
1528
|
+
const cols = Object.entries(this.valueSchema.properties).map(([key, typeDef]) => {
|
|
1529
|
+
const sqlType = this.mapTypeToSQL(typeDef);
|
|
1530
|
+
const isRequired = requiredSet.has(key);
|
|
1531
|
+
const nullable = !isRequired || this.isNullable(typeDef);
|
|
1532
|
+
return `${$delimiter}${key}${$delimiter} ${sqlType}${nullable ? " NULL" : " NOT NULL"}`;
|
|
1533
|
+
}).join(", ");
|
|
1534
|
+
if (cols.length > 0) {
|
|
1535
|
+
return `, ${cols}`;
|
|
1536
|
+
} else {
|
|
1537
|
+
return "";
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
isNullable(typeDef) {
|
|
1541
|
+
if (typeof typeDef === "boolean")
|
|
1542
|
+
return typeDef;
|
|
1543
|
+
if (typeDef.type === "null") {
|
|
1544
|
+
return true;
|
|
1545
|
+
}
|
|
1546
|
+
if (Array.isArray(typeDef.type)) {
|
|
1547
|
+
return typeDef.type.includes("null");
|
|
1548
|
+
}
|
|
1549
|
+
if (typeDef.anyOf && Array.isArray(typeDef.anyOf)) {
|
|
1550
|
+
return typeDef.anyOf.some((type) => type.type === "null");
|
|
1551
|
+
}
|
|
1552
|
+
if (typeDef.oneOf && Array.isArray(typeDef.oneOf)) {
|
|
1553
|
+
return typeDef.oneOf.some((type) => type.type === "null");
|
|
1554
|
+
}
|
|
1555
|
+
return false;
|
|
1556
|
+
}
|
|
1557
|
+
primaryKeyColumnList($delimiter = "") {
|
|
1558
|
+
return $delimiter + this.primaryKeyColumns().join(`${$delimiter}, ${$delimiter}`) + $delimiter;
|
|
1559
|
+
}
|
|
1560
|
+
valueColumnList($delimiter = "") {
|
|
1561
|
+
return $delimiter + this.valueColumns().join(`${$delimiter}, ${$delimiter}`) + $delimiter;
|
|
1562
|
+
}
|
|
1563
|
+
getNonNullType(typeDef) {
|
|
1564
|
+
if (typeof typeDef === "boolean")
|
|
1565
|
+
return typeDef;
|
|
1566
|
+
if (typeDef.anyOf && Array.isArray(typeDef.anyOf)) {
|
|
1567
|
+
const nonNullType = typeDef.anyOf.find((t) => t.type !== "null");
|
|
1568
|
+
if (nonNullType) {
|
|
1569
|
+
return nonNullType;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
if (typeDef.oneOf && Array.isArray(typeDef.oneOf)) {
|
|
1573
|
+
const nonNullType = typeDef.oneOf.find((t) => t.type !== "null");
|
|
1574
|
+
if (nonNullType) {
|
|
1575
|
+
return nonNullType;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
return typeDef;
|
|
1579
|
+
}
|
|
1580
|
+
getValueAsOrderedArray(value) {
|
|
1581
|
+
const orderedParams = [];
|
|
1582
|
+
const valueAsRecord = value;
|
|
1583
|
+
const requiredSet = new Set(this.valueSchema.required ?? []);
|
|
1584
|
+
for (const key in this.valueSchema.properties) {
|
|
1585
|
+
if (Object.prototype.hasOwnProperty.call(valueAsRecord, key)) {
|
|
1586
|
+
const val = valueAsRecord[key];
|
|
1587
|
+
if (val === undefined && !requiredSet.has(key)) {
|
|
1588
|
+
orderedParams.push(null);
|
|
1589
|
+
} else {
|
|
1590
|
+
orderedParams.push(this.jsToSqlValue(key, val));
|
|
1591
|
+
}
|
|
1592
|
+
} else {
|
|
1593
|
+
if (requiredSet.has(key)) {
|
|
1594
|
+
throw new Error(`Missing required value field: ${key}`);
|
|
1595
|
+
}
|
|
1596
|
+
orderedParams.push(null);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return orderedParams;
|
|
1600
|
+
}
|
|
1601
|
+
getPrimaryKeyAsOrderedArray(key) {
|
|
1602
|
+
const orderedParams = [];
|
|
1603
|
+
const keyObj = key;
|
|
1604
|
+
for (const k of Object.keys(this.primaryKeySchema.properties)) {
|
|
1605
|
+
if (k in keyObj) {
|
|
1606
|
+
const value = keyObj[k];
|
|
1607
|
+
if (value === null) {
|
|
1608
|
+
throw new Error(`Primary key field ${k} cannot be null`);
|
|
1609
|
+
}
|
|
1610
|
+
orderedParams.push(this.jsToSqlValue(k, value));
|
|
1611
|
+
} else {
|
|
1612
|
+
throw new Error(`Missing required primary key field: ${k}`);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
return orderedParams;
|
|
1616
|
+
}
|
|
1617
|
+
jsToSqlValue(column, value) {
|
|
1618
|
+
const typeDef = this.schema.properties[column];
|
|
1619
|
+
if (!typeDef) {
|
|
1620
|
+
return value;
|
|
1621
|
+
}
|
|
1622
|
+
if (value === null && this.isNullable(typeDef)) {
|
|
1623
|
+
return null;
|
|
1624
|
+
}
|
|
1625
|
+
const actualType = this.getNonNullType(typeDef);
|
|
1626
|
+
if (typeof actualType === "boolean") {
|
|
1627
|
+
return value;
|
|
1628
|
+
}
|
|
1629
|
+
if (actualType.contentEncoding === "blob") {
|
|
1630
|
+
const v = value;
|
|
1631
|
+
if (v instanceof Uint8Array) {
|
|
1632
|
+
return v;
|
|
1633
|
+
}
|
|
1634
|
+
if (typeof Buffer !== "undefined" && v instanceof Buffer) {
|
|
1635
|
+
return new Uint8Array(v);
|
|
1636
|
+
}
|
|
1637
|
+
if (Array.isArray(v)) {
|
|
1638
|
+
return new Uint8Array(v);
|
|
1639
|
+
}
|
|
1640
|
+
return v;
|
|
1641
|
+
} else if (value instanceof Date) {
|
|
1642
|
+
return value.toISOString();
|
|
1643
|
+
} else {
|
|
1644
|
+
return value;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
sqlToJsValue(column, value) {
|
|
1648
|
+
const typeDef = this.schema.properties[column];
|
|
1649
|
+
if (!typeDef) {
|
|
1650
|
+
return value;
|
|
1651
|
+
}
|
|
1652
|
+
if (value === null && this.isNullable(typeDef)) {
|
|
1653
|
+
return null;
|
|
1654
|
+
}
|
|
1655
|
+
const actualType = this.getNonNullType(typeDef);
|
|
1656
|
+
if (typeof actualType === "boolean") {
|
|
1657
|
+
return value;
|
|
1658
|
+
}
|
|
1659
|
+
if (actualType.contentEncoding === "blob") {
|
|
1660
|
+
const v = value;
|
|
1661
|
+
if (typeof Buffer !== "undefined" && v instanceof Buffer) {
|
|
1662
|
+
return new Uint8Array(v);
|
|
1663
|
+
}
|
|
1664
|
+
if (v instanceof Uint8Array) {
|
|
1665
|
+
return v;
|
|
1666
|
+
}
|
|
1667
|
+
return v;
|
|
1668
|
+
} else {
|
|
1669
|
+
return value;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
validateTableAndSchema() {
|
|
1673
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(this.table)) {
|
|
1674
|
+
throw new Error("Table name must start with a letter and contain only letters, digits, and underscores, got: " + this.table);
|
|
1675
|
+
}
|
|
1676
|
+
const validateSchemaKeys = (schema) => {
|
|
1677
|
+
for (const key in schema.properties) {
|
|
1678
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(key)) {
|
|
1679
|
+
throw new Error("Schema keys must start with a letter and contain only letters, digits, and underscores, got: " + key);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
validateSchemaKeys(this.primaryKeySchema);
|
|
1684
|
+
validateSchemaKeys(this.valueSchema);
|
|
1685
|
+
const primaryKeys = new Set(Object.keys(this.primaryKeySchema.properties));
|
|
1686
|
+
const valueKeys = Object.keys(this.valueSchema.properties);
|
|
1687
|
+
const duplicates = valueKeys.filter((key) => primaryKeys.has(key));
|
|
1688
|
+
if (duplicates.length > 0) {
|
|
1689
|
+
throw new Error(`Duplicate keys found in schemas: ${duplicates.join(", ")}`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
// src/tabular/SupabaseTabularRepository.ts
|
|
1695
|
+
var SUPABASE_TABULAR_REPOSITORY = createServiceToken10("storage.tabularRepository.supabase");
|
|
1696
|
+
|
|
1697
|
+
class SupabaseTabularRepository extends BaseSqlTabularRepository {
|
|
1698
|
+
client;
|
|
1699
|
+
constructor(client, table = "tabular_store", schema, primaryKeyNames, indexes = []) {
|
|
1700
|
+
super(table, schema, primaryKeyNames, indexes);
|
|
1701
|
+
this.client = client;
|
|
1702
|
+
}
|
|
1703
|
+
isSetup = true;
|
|
1704
|
+
async setupDatabase() {
|
|
1705
|
+
if (this.isSetup) {
|
|
1706
|
+
return this.client;
|
|
1707
|
+
}
|
|
1708
|
+
const sql = `
|
|
1709
|
+
CREATE TABLE IF NOT EXISTS "${this.table}" (
|
|
1710
|
+
${this.constructPrimaryKeyColumns('"')} ${this.constructValueColumns('"')},
|
|
1711
|
+
PRIMARY KEY (${this.primaryKeyColumnList()})
|
|
1712
|
+
)
|
|
1713
|
+
`;
|
|
1714
|
+
const { error } = await this.client.rpc("exec_sql", { query: sql });
|
|
1715
|
+
if (error && !error.message.includes("already exists")) {
|
|
1716
|
+
throw error;
|
|
1717
|
+
}
|
|
1718
|
+
const pkColumns = this.primaryKeyColumns();
|
|
1719
|
+
const createdIndexes = new Set;
|
|
1720
|
+
for (const columns of this.indexes) {
|
|
1721
|
+
if (columns.length <= pkColumns.length) {
|
|
1722
|
+
const isPkPrefix = columns.every((col, idx) => col === pkColumns[idx]);
|
|
1723
|
+
if (isPkPrefix)
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
const indexName = `${this.table}_${columns.join("_")}`;
|
|
1727
|
+
const columnList = columns.map((col) => `"${String(col)}"`).join(", ");
|
|
1728
|
+
const columnKey = columns.join(",");
|
|
1729
|
+
if (createdIndexes.has(columnKey))
|
|
1730
|
+
continue;
|
|
1731
|
+
const isRedundant = Array.from(createdIndexes).some((existing) => {
|
|
1732
|
+
const existingCols = existing.split(",");
|
|
1733
|
+
return existingCols.length >= columns.length && columns.every((col, idx) => col === existingCols[idx]);
|
|
1734
|
+
});
|
|
1735
|
+
if (!isRedundant) {
|
|
1736
|
+
const indexSql = `CREATE INDEX IF NOT EXISTS "${indexName}" ON "${this.table}" (${columnList})`;
|
|
1737
|
+
const { error: indexError } = await this.client.rpc("exec_sql", { query: indexSql });
|
|
1738
|
+
if (indexError && !indexError.message.includes("already exists")) {
|
|
1739
|
+
console.warn(`Failed to create index ${indexName}:`, indexError);
|
|
1740
|
+
}
|
|
1741
|
+
createdIndexes.add(columnKey);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
this.isSetup = true;
|
|
1745
|
+
return this.client;
|
|
1746
|
+
}
|
|
1747
|
+
mapTypeToSQL(typeDef) {
|
|
1748
|
+
const actualType = this.getNonNullType(typeDef);
|
|
1749
|
+
if (typeof actualType === "boolean") {
|
|
1750
|
+
return "TEXT /* boolean schema */";
|
|
1751
|
+
}
|
|
1752
|
+
if (actualType.contentEncoding === "blob")
|
|
1753
|
+
return "BYTEA";
|
|
1754
|
+
switch (actualType.type) {
|
|
1755
|
+
case "string":
|
|
1756
|
+
if (actualType.format === "date-time")
|
|
1757
|
+
return "TIMESTAMP";
|
|
1758
|
+
if (actualType.format === "date")
|
|
1759
|
+
return "DATE";
|
|
1760
|
+
if (actualType.format === "email")
|
|
1761
|
+
return "VARCHAR(255)";
|
|
1762
|
+
if (actualType.format === "uri")
|
|
1763
|
+
return "VARCHAR(2048)";
|
|
1764
|
+
if (actualType.format === "uuid")
|
|
1765
|
+
return "UUID";
|
|
1766
|
+
if (typeof actualType.maxLength === "number") {
|
|
1767
|
+
return `VARCHAR(${actualType.maxLength})`;
|
|
1768
|
+
}
|
|
1769
|
+
return "TEXT";
|
|
1770
|
+
case "number":
|
|
1771
|
+
case "integer":
|
|
1772
|
+
if (actualType.multipleOf === 1 || actualType.type === "integer") {
|
|
1773
|
+
if (typeof actualType.minimum === "number") {
|
|
1774
|
+
if (actualType.minimum >= 0) {
|
|
1775
|
+
if (typeof actualType.maximum === "number") {
|
|
1776
|
+
if (actualType.maximum <= 32767)
|
|
1777
|
+
return "SMALLINT";
|
|
1778
|
+
if (actualType.maximum <= 2147483647)
|
|
1779
|
+
return "INTEGER";
|
|
1780
|
+
}
|
|
1781
|
+
return "BIGINT";
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return "INTEGER";
|
|
1785
|
+
}
|
|
1786
|
+
if (actualType.format === "float")
|
|
1787
|
+
return "REAL";
|
|
1788
|
+
if (actualType.format === "double")
|
|
1789
|
+
return "DOUBLE PRECISION";
|
|
1790
|
+
if (typeof actualType.multipleOf === "number") {
|
|
1791
|
+
const decimalPlaces = String(actualType.multipleOf).split(".")[1]?.length || 0;
|
|
1792
|
+
if (decimalPlaces > 0) {
|
|
1793
|
+
return `NUMERIC(38, ${decimalPlaces})`;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
return "NUMERIC";
|
|
1797
|
+
case "boolean":
|
|
1798
|
+
return "BOOLEAN";
|
|
1799
|
+
case "array":
|
|
1800
|
+
if (actualType.items && typeof actualType.items === "object" && !Array.isArray(actualType.items)) {
|
|
1801
|
+
const itemType = this.mapTypeToSQL(actualType.items);
|
|
1802
|
+
const supportedArrayElementTypes = [
|
|
1803
|
+
"TEXT",
|
|
1804
|
+
"VARCHAR",
|
|
1805
|
+
"CHAR",
|
|
1806
|
+
"INTEGER",
|
|
1807
|
+
"SMALLINT",
|
|
1808
|
+
"BIGINT",
|
|
1809
|
+
"REAL",
|
|
1810
|
+
"DOUBLE PRECISION",
|
|
1811
|
+
"NUMERIC",
|
|
1812
|
+
"BOOLEAN",
|
|
1813
|
+
"UUID",
|
|
1814
|
+
"DATE",
|
|
1815
|
+
"TIMESTAMP"
|
|
1816
|
+
];
|
|
1817
|
+
const isSupported = supportedArrayElementTypes.some((type) => itemType === type || itemType.startsWith(type + "(") && type !== "VARCHAR");
|
|
1818
|
+
if (isSupported) {
|
|
1819
|
+
return `${itemType}[]`;
|
|
1820
|
+
} else {
|
|
1821
|
+
return "JSONB /* complex array */";
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return "JSONB /* generic array */";
|
|
1825
|
+
case "object":
|
|
1826
|
+
return "JSONB /* object */";
|
|
1827
|
+
default:
|
|
1828
|
+
return "TEXT /* unknown type */";
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
constructPrimaryKeyColumns($delimiter = "") {
|
|
1832
|
+
const cols = Object.entries(this.primaryKeySchema.properties).map(([key, typeDef]) => {
|
|
1833
|
+
const sqlType = this.mapTypeToSQL(typeDef);
|
|
1834
|
+
let constraints = "NOT NULL";
|
|
1835
|
+
if (this.shouldBeUnsigned(typeDef)) {
|
|
1836
|
+
constraints += ` CHECK (${$delimiter}${key}${$delimiter} >= 0)`;
|
|
1837
|
+
}
|
|
1838
|
+
return `${$delimiter}${key}${$delimiter} ${sqlType} ${constraints}`;
|
|
1839
|
+
}).join(", ");
|
|
1840
|
+
return cols;
|
|
1841
|
+
}
|
|
1842
|
+
constructValueColumns($delimiter = "") {
|
|
1843
|
+
const delimiter = $delimiter || '"';
|
|
1844
|
+
const requiredSet = new Set(this.valueSchema.required ?? []);
|
|
1845
|
+
const cols = Object.entries(this.valueSchema.properties).map(([key, typeDef]) => {
|
|
1846
|
+
const sqlType = this.mapTypeToSQL(typeDef);
|
|
1847
|
+
const isRequired = requiredSet.has(key);
|
|
1848
|
+
const nullable = !isRequired || this.isNullable(typeDef);
|
|
1849
|
+
let constraints = nullable ? "NULL" : "NOT NULL";
|
|
1850
|
+
if (this.shouldBeUnsigned(typeDef)) {
|
|
1851
|
+
constraints += ` CHECK (${delimiter}${key}${delimiter} >= 0)`;
|
|
1852
|
+
}
|
|
1853
|
+
return `${delimiter}${key}${delimiter} ${sqlType} ${constraints}`;
|
|
1854
|
+
}).join(", ");
|
|
1855
|
+
if (cols.length > 0) {
|
|
1856
|
+
return `, ${cols}`;
|
|
1857
|
+
} else {
|
|
1858
|
+
return "";
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
sqlToJsValue(column, value) {
|
|
1862
|
+
const typeDef = this.schema.properties[column];
|
|
1863
|
+
if (typeDef) {
|
|
1864
|
+
if (value === null && this.isNullable(typeDef)) {
|
|
1865
|
+
return null;
|
|
1866
|
+
}
|
|
1867
|
+
const actualType = this.getNonNullType(typeDef);
|
|
1868
|
+
if (typeof actualType !== "boolean" && (actualType.type === "number" || actualType.type === "integer")) {
|
|
1869
|
+
const v = value;
|
|
1870
|
+
if (typeof v === "number")
|
|
1871
|
+
return v;
|
|
1872
|
+
if (typeof v === "string") {
|
|
1873
|
+
const parsed = Number(v);
|
|
1874
|
+
if (!isNaN(parsed))
|
|
1875
|
+
return parsed;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return super.sqlToJsValue(column, value);
|
|
1880
|
+
}
|
|
1881
|
+
shouldBeUnsigned(typeDef) {
|
|
1882
|
+
const actualType = this.getNonNullType(typeDef);
|
|
1883
|
+
if (typeof actualType === "boolean") {
|
|
1884
|
+
return false;
|
|
1885
|
+
}
|
|
1886
|
+
if ((actualType.type === "number" || actualType.type === "integer") && typeof actualType.minimum === "number" && actualType.minimum >= 0) {
|
|
1887
|
+
return true;
|
|
1888
|
+
}
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
async put(entity) {
|
|
1892
|
+
await this.setupDatabase();
|
|
1893
|
+
const normalizedEntity = { ...entity };
|
|
1894
|
+
const requiredSet = new Set(this.valueSchema.required ?? []);
|
|
1895
|
+
for (const key in this.valueSchema.properties) {
|
|
1896
|
+
if (!(key in normalizedEntity) || normalizedEntity[key] === undefined) {
|
|
1897
|
+
if (!requiredSet.has(key)) {
|
|
1898
|
+
normalizedEntity[key] = null;
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
const { data, error } = await this.client.from(this.table).upsert(normalizedEntity, { onConflict: this.primaryKeyColumnList() }).select().single();
|
|
1903
|
+
if (error)
|
|
1904
|
+
throw error;
|
|
1905
|
+
const updatedEntity = data;
|
|
1906
|
+
for (const key in this.schema.properties) {
|
|
1907
|
+
updatedEntity[key] = this.sqlToJsValue(key, updatedEntity[key]);
|
|
1908
|
+
}
|
|
1909
|
+
this.events.emit("put", updatedEntity);
|
|
1910
|
+
return updatedEntity;
|
|
1911
|
+
}
|
|
1912
|
+
async putBulk(entities) {
|
|
1913
|
+
if (entities.length === 0)
|
|
1914
|
+
return [];
|
|
1915
|
+
await this.setupDatabase();
|
|
1916
|
+
const requiredSet = new Set(this.valueSchema.required ?? []);
|
|
1917
|
+
const normalizedEntities = entities.map((entity) => {
|
|
1918
|
+
const normalized = { ...entity };
|
|
1919
|
+
for (const key in this.valueSchema.properties) {
|
|
1920
|
+
if (!(key in normalized) || normalized[key] === undefined) {
|
|
1921
|
+
if (!requiredSet.has(key)) {
|
|
1922
|
+
normalized[key] = null;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
return normalized;
|
|
1927
|
+
});
|
|
1928
|
+
const { data, error } = await this.client.from(this.table).upsert(normalizedEntities, { onConflict: this.primaryKeyColumnList() }).select();
|
|
1929
|
+
if (error)
|
|
1930
|
+
throw error;
|
|
1931
|
+
const updatedEntities = data;
|
|
1932
|
+
for (const entity of updatedEntities) {
|
|
1933
|
+
for (const key in this.schema.properties) {
|
|
1934
|
+
entity[key] = this.sqlToJsValue(key, entity[key]);
|
|
1935
|
+
}
|
|
1936
|
+
this.events.emit("put", entity);
|
|
1937
|
+
}
|
|
1938
|
+
return updatedEntities;
|
|
1939
|
+
}
|
|
1940
|
+
async get(key) {
|
|
1941
|
+
await this.setupDatabase();
|
|
1942
|
+
let query = this.client.from(this.table).select("*");
|
|
1943
|
+
for (const pkName of this.primaryKeyNames) {
|
|
1944
|
+
query = query.eq(String(pkName), key[pkName]);
|
|
1945
|
+
}
|
|
1946
|
+
const { data, error } = await query.single();
|
|
1947
|
+
if (error) {
|
|
1948
|
+
if (error.code === "PGRST116") {
|
|
1949
|
+
this.events.emit("get", key, undefined);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
throw error;
|
|
1953
|
+
}
|
|
1954
|
+
const val = data;
|
|
1955
|
+
if (val) {
|
|
1956
|
+
for (const key2 in this.schema.properties) {
|
|
1957
|
+
val[key2] = this.sqlToJsValue(key2, val[key2]);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
this.events.emit("get", key, val);
|
|
1961
|
+
return val;
|
|
1962
|
+
}
|
|
1963
|
+
async search(searchCriteria) {
|
|
1964
|
+
await this.setupDatabase();
|
|
1965
|
+
const searchKeys = Object.keys(searchCriteria);
|
|
1966
|
+
if (searchKeys.length === 0) {
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
const bestIndex = this.findBestMatchingIndex(searchKeys);
|
|
1970
|
+
if (!bestIndex) {
|
|
1971
|
+
throw new Error(`No suitable index found for the search criteria, searching for ['${searchKeys.join("', '")}'] with pk ['${this.primaryKeyNames.join("', '")}'] and indexes ['${this.indexes.join("', '")}']`);
|
|
1972
|
+
}
|
|
1973
|
+
const validColumns = [...this.primaryKeyColumns(), ...this.valueColumns()];
|
|
1974
|
+
const invalidColumns = searchKeys.filter((key) => !validColumns.includes(key));
|
|
1975
|
+
if (invalidColumns.length > 0) {
|
|
1976
|
+
throw new Error(`Invalid columns in search criteria: ${invalidColumns.join(", ")}`);
|
|
1977
|
+
}
|
|
1978
|
+
let query = this.client.from(this.table).select("*");
|
|
1979
|
+
for (const [key, value] of Object.entries(searchCriteria)) {
|
|
1980
|
+
query = query.eq(key, value);
|
|
1981
|
+
}
|
|
1982
|
+
const { data, error } = await query;
|
|
1983
|
+
if (error)
|
|
1984
|
+
throw error;
|
|
1985
|
+
if (data && data.length > 0) {
|
|
1986
|
+
for (const row of data) {
|
|
1987
|
+
for (const key in this.schema.properties) {
|
|
1988
|
+
row[key] = this.sqlToJsValue(key, row[key]);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
this.events.emit("search", searchCriteria, data);
|
|
1992
|
+
return data;
|
|
1993
|
+
} else {
|
|
1994
|
+
this.events.emit("search", searchCriteria, undefined);
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async delete(value) {
|
|
1999
|
+
await this.setupDatabase();
|
|
2000
|
+
const { key } = this.separateKeyValueFromCombined(value);
|
|
2001
|
+
let query = this.client.from(this.table).delete();
|
|
2002
|
+
for (const pkName of this.primaryKeyNames) {
|
|
2003
|
+
query = query.eq(String(pkName), key[pkName]);
|
|
2004
|
+
}
|
|
2005
|
+
const { error } = await query;
|
|
2006
|
+
if (error)
|
|
2007
|
+
throw error;
|
|
2008
|
+
this.events.emit("delete", key);
|
|
2009
|
+
}
|
|
2010
|
+
async getAll() {
|
|
2011
|
+
await this.setupDatabase();
|
|
2012
|
+
const { data, error } = await this.client.from(this.table).select("*");
|
|
2013
|
+
if (error)
|
|
2014
|
+
throw error;
|
|
2015
|
+
if (data && data.length) {
|
|
2016
|
+
for (const row of data) {
|
|
2017
|
+
for (const key in this.schema.properties) {
|
|
2018
|
+
row[key] = this.sqlToJsValue(key, row[key]);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return data;
|
|
2022
|
+
}
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
async deleteAll() {
|
|
2026
|
+
await this.setupDatabase();
|
|
2027
|
+
const firstPkColumn = this.primaryKeyNames[0];
|
|
2028
|
+
const { error } = await this.client.from(this.table).delete().neq(String(firstPkColumn), null);
|
|
2029
|
+
if (error)
|
|
2030
|
+
throw error;
|
|
2031
|
+
this.events.emit("clearall");
|
|
2032
|
+
}
|
|
2033
|
+
async size() {
|
|
2034
|
+
await this.setupDatabase();
|
|
2035
|
+
const { count, error } = await this.client.from(this.table).select("*", { count: "exact", head: true });
|
|
2036
|
+
if (error)
|
|
2037
|
+
throw error;
|
|
2038
|
+
return count ?? 0;
|
|
2039
|
+
}
|
|
2040
|
+
generateWhereClause(column, operator = "=") {
|
|
2041
|
+
if (!(column in this.schema.properties)) {
|
|
2042
|
+
throw new Error(`Schema must have a ${String(column)} field to use deleteSearch`);
|
|
2043
|
+
}
|
|
2044
|
+
return `${String(column)} ${operator} $1`;
|
|
2045
|
+
}
|
|
2046
|
+
async deleteSearch(column, value, operator = "=") {
|
|
2047
|
+
await this.setupDatabase();
|
|
2048
|
+
let query = this.client.from(this.table).delete();
|
|
2049
|
+
switch (operator) {
|
|
2050
|
+
case "=":
|
|
2051
|
+
query = query.eq(String(column), value);
|
|
2052
|
+
break;
|
|
2053
|
+
case "<":
|
|
2054
|
+
query = query.lt(String(column), value);
|
|
2055
|
+
break;
|
|
2056
|
+
case "<=":
|
|
2057
|
+
query = query.lte(String(column), value);
|
|
2058
|
+
break;
|
|
2059
|
+
case ">":
|
|
2060
|
+
query = query.gt(String(column), value);
|
|
2061
|
+
break;
|
|
2062
|
+
case ">=":
|
|
2063
|
+
query = query.gte(String(column), value);
|
|
2064
|
+
break;
|
|
2065
|
+
}
|
|
2066
|
+
const { error } = await query;
|
|
2067
|
+
if (error)
|
|
2068
|
+
throw error;
|
|
2069
|
+
this.events.emit("delete", column);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
// src/kv/IndexedDbKvRepository.ts
|
|
2073
|
+
import { createServiceToken as createServiceToken11 } from "@workglow/util";
|
|
2074
|
+
var IDB_KV_REPOSITORY = createServiceToken11("storage.kvRepository.indexedDb");
|
|
2075
|
+
|
|
2076
|
+
class IndexedDbKvRepository extends KvViaTabularRepository {
|
|
2077
|
+
dbName;
|
|
2078
|
+
tabularRepository;
|
|
2079
|
+
constructor(dbName, keySchema = { type: "string" }, valueSchema = {}) {
|
|
2080
|
+
super(keySchema, valueSchema);
|
|
2081
|
+
this.dbName = dbName;
|
|
2082
|
+
this.tabularRepository = new IndexedDbTabularRepository(dbName, DefaultKeyValueSchema, DefaultKeyValueKey);
|
|
2083
|
+
}
|
|
2084
|
+
}
|
|
2085
|
+
// src/kv/SupabaseKvRepository.ts
|
|
2086
|
+
import { createServiceToken as createServiceToken12 } from "@workglow/util";
|
|
2087
|
+
var SUPABASE_KV_REPOSITORY = createServiceToken12("storage.kvRepository.supabase");
|
|
2088
|
+
|
|
2089
|
+
class SupabaseKvRepository extends KvViaTabularRepository {
|
|
2090
|
+
client;
|
|
2091
|
+
tableName;
|
|
2092
|
+
tabularRepository;
|
|
2093
|
+
constructor(client, tableName, keySchema = { type: "string" }, valueSchema = {}, tabularRepository) {
|
|
2094
|
+
super(keySchema, valueSchema);
|
|
2095
|
+
this.client = client;
|
|
2096
|
+
this.tableName = tableName;
|
|
2097
|
+
this.tabularRepository = tabularRepository ?? new SupabaseTabularRepository(client, tableName, DefaultKeyValueSchema, DefaultKeyValueKey);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
// src/queue/IndexedDbQueueStorage.ts
|
|
2101
|
+
import { createServiceToken as createServiceToken13, makeFingerprint as makeFingerprint5, uuid4 as uuid42 } from "@workglow/util";
|
|
2102
|
+
var INDEXED_DB_QUEUE_STORAGE = createServiceToken13("jobqueue.storage.indexedDb");
|
|
2103
|
+
|
|
2104
|
+
class IndexedDbQueueStorage {
|
|
2105
|
+
queueName;
|
|
2106
|
+
db;
|
|
2107
|
+
tableName;
|
|
2108
|
+
migrationOptions;
|
|
2109
|
+
constructor(queueName, migrationOptions = {}) {
|
|
2110
|
+
this.queueName = queueName;
|
|
2111
|
+
this.tableName = `jobs_${queueName}`;
|
|
2112
|
+
this.migrationOptions = migrationOptions;
|
|
2113
|
+
}
|
|
2114
|
+
async getDb() {
|
|
2115
|
+
if (this.db)
|
|
2116
|
+
return this.db;
|
|
2117
|
+
await this.setupDatabase();
|
|
2118
|
+
return this.db;
|
|
2119
|
+
}
|
|
2120
|
+
async setupDatabase() {
|
|
2121
|
+
const expectedIndexes = [
|
|
2122
|
+
{
|
|
2123
|
+
name: "status",
|
|
2124
|
+
keyPath: `status`,
|
|
2125
|
+
options: { unique: false }
|
|
2126
|
+
},
|
|
2127
|
+
{
|
|
2128
|
+
name: "status_run_after",
|
|
2129
|
+
keyPath: ["status", "run_after"],
|
|
2130
|
+
options: { unique: false }
|
|
2131
|
+
},
|
|
2132
|
+
{
|
|
2133
|
+
name: "job_run_id",
|
|
2134
|
+
keyPath: `job_run_id`,
|
|
2135
|
+
options: { unique: false }
|
|
2136
|
+
},
|
|
2137
|
+
{
|
|
2138
|
+
name: "fingerprint_status",
|
|
2139
|
+
keyPath: ["fingerprint", "status"],
|
|
2140
|
+
options: { unique: false }
|
|
2141
|
+
}
|
|
2142
|
+
];
|
|
2143
|
+
this.db = await ensureIndexedDbTable(this.tableName, "id", expectedIndexes, this.migrationOptions);
|
|
2144
|
+
}
|
|
2145
|
+
async add(job) {
|
|
2146
|
+
const db = await this.getDb();
|
|
2147
|
+
const now = new Date().toISOString();
|
|
2148
|
+
job.id = job.id ?? uuid42();
|
|
2149
|
+
job.job_run_id = job.job_run_id ?? uuid42();
|
|
2150
|
+
job.queue = this.queueName;
|
|
2151
|
+
job.fingerprint = await makeFingerprint5(job.input);
|
|
2152
|
+
job.status = "PENDING" /* PENDING */;
|
|
2153
|
+
job.progress = 0;
|
|
2154
|
+
job.progress_message = "";
|
|
2155
|
+
job.progress_details = null;
|
|
2156
|
+
job.created_at = now;
|
|
2157
|
+
job.run_after = now;
|
|
2158
|
+
const tx = db.transaction(this.tableName, "readwrite");
|
|
2159
|
+
const store = tx.objectStore(this.tableName);
|
|
2160
|
+
return new Promise((resolve, reject) => {
|
|
2161
|
+
const request = store.add(job);
|
|
2162
|
+
tx.oncomplete = () => resolve(job.id);
|
|
2163
|
+
tx.onerror = () => reject(tx.error);
|
|
2164
|
+
request.onerror = () => reject(request.error);
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
async get(id) {
|
|
2168
|
+
const db = await this.getDb();
|
|
2169
|
+
const tx = db.transaction(this.tableName, "readonly");
|
|
2170
|
+
const store = tx.objectStore(this.tableName);
|
|
2171
|
+
const request = store.get(id);
|
|
2172
|
+
return new Promise((resolve, reject) => {
|
|
2173
|
+
request.onsuccess = () => resolve(request.result);
|
|
2174
|
+
request.onerror = () => reject(request.error);
|
|
2175
|
+
tx.onerror = () => reject(tx.error);
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
async peek(status = "PENDING" /* PENDING */, num = 100) {
|
|
2179
|
+
const db = await this.getDb();
|
|
2180
|
+
const tx = db.transaction(this.tableName, "readonly");
|
|
2181
|
+
const store = tx.objectStore(this.tableName);
|
|
2182
|
+
const index = store.index("status_run_after");
|
|
2183
|
+
return new Promise((resolve, reject) => {
|
|
2184
|
+
const ret = new Map;
|
|
2185
|
+
const keyRange = IDBKeyRange.bound([status, ""], [status, ""]);
|
|
2186
|
+
const cursorRequest = index.openCursor(keyRange);
|
|
2187
|
+
const handleCursor = (e) => {
|
|
2188
|
+
const cursor = e.target.result;
|
|
2189
|
+
if (!cursor || ret.size >= num) {
|
|
2190
|
+
resolve(Array.from(ret.values()));
|
|
2191
|
+
return;
|
|
2192
|
+
}
|
|
2193
|
+
ret.set(cursor.value.id, cursor.value);
|
|
2194
|
+
cursor.continue();
|
|
2195
|
+
};
|
|
2196
|
+
cursorRequest.onsuccess = handleCursor;
|
|
2197
|
+
cursorRequest.onerror = () => reject(cursorRequest.error);
|
|
2198
|
+
tx.onerror = () => reject(tx.error);
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
async next() {
|
|
2202
|
+
const db = await this.getDb();
|
|
2203
|
+
const tx = db.transaction(this.tableName, "readwrite");
|
|
2204
|
+
const store = tx.objectStore(this.tableName);
|
|
2205
|
+
const index = store.index("status_run_after");
|
|
2206
|
+
const now = new Date().toISOString();
|
|
2207
|
+
return new Promise((resolve, reject) => {
|
|
2208
|
+
const cursorRequest = index.openCursor(IDBKeyRange.bound(["PENDING" /* PENDING */, ""], ["PENDING" /* PENDING */, now], false, true));
|
|
2209
|
+
let jobToReturn;
|
|
2210
|
+
cursorRequest.onsuccess = (e) => {
|
|
2211
|
+
const cursor = e.target.result;
|
|
2212
|
+
if (!cursor) {
|
|
2213
|
+
if (jobToReturn) {
|
|
2214
|
+
resolve(jobToReturn);
|
|
2215
|
+
} else {
|
|
2216
|
+
resolve(undefined);
|
|
2217
|
+
}
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
const job = cursor.value;
|
|
2221
|
+
if (job.status !== "PENDING" /* PENDING */) {
|
|
2222
|
+
cursor.continue();
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
job.status = "PROCESSING" /* PROCESSING */;
|
|
2226
|
+
job.last_ran_at = now;
|
|
2227
|
+
try {
|
|
2228
|
+
const updateRequest = store.put(job);
|
|
2229
|
+
updateRequest.onsuccess = () => {
|
|
2230
|
+
jobToReturn = job;
|
|
2231
|
+
};
|
|
2232
|
+
updateRequest.onerror = (err) => {
|
|
2233
|
+
console.error("Failed to update job status:", err);
|
|
2234
|
+
cursor.continue();
|
|
2235
|
+
};
|
|
2236
|
+
} catch (err) {
|
|
2237
|
+
console.error("Error updating job:", err);
|
|
2238
|
+
cursor.continue();
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
cursorRequest.onerror = () => reject(cursorRequest.error);
|
|
2242
|
+
tx.oncomplete = () => {
|
|
2243
|
+
resolve(jobToReturn);
|
|
2244
|
+
};
|
|
2245
|
+
tx.onerror = () => reject(tx.error);
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
async size(status = "PENDING" /* PENDING */) {
|
|
2249
|
+
const db = await this.getDb();
|
|
2250
|
+
return new Promise((resolve, reject) => {
|
|
2251
|
+
const tx = db.transaction(this.tableName, "readonly");
|
|
2252
|
+
const store = tx.objectStore(this.tableName);
|
|
2253
|
+
const index = store.index("status");
|
|
2254
|
+
const request = index.count(status);
|
|
2255
|
+
request.onsuccess = () => resolve(request.result);
|
|
2256
|
+
request.onerror = () => reject(request.error);
|
|
2257
|
+
tx.onerror = () => reject(tx.error);
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
async complete(job) {
|
|
2261
|
+
const db = await this.getDb();
|
|
2262
|
+
const tx = db.transaction(this.tableName, "readwrite");
|
|
2263
|
+
const store = tx.objectStore(this.tableName);
|
|
2264
|
+
return new Promise((resolve, reject) => {
|
|
2265
|
+
const getReq = store.get(job.id);
|
|
2266
|
+
getReq.onsuccess = () => {
|
|
2267
|
+
const existing = getReq.result;
|
|
2268
|
+
const currentAttempts = existing?.run_attempts ?? 0;
|
|
2269
|
+
job.run_attempts = currentAttempts + 1;
|
|
2270
|
+
const putReq = store.put(job);
|
|
2271
|
+
putReq.onsuccess = () => {};
|
|
2272
|
+
putReq.onerror = () => reject(putReq.error);
|
|
2273
|
+
};
|
|
2274
|
+
getReq.onerror = () => reject(getReq.error);
|
|
2275
|
+
tx.oncomplete = () => resolve();
|
|
2276
|
+
tx.onerror = () => reject(tx.error);
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
async abort(id) {
|
|
2280
|
+
const job = await this.get(id);
|
|
2281
|
+
if (!job)
|
|
2282
|
+
return;
|
|
2283
|
+
job.status = "ABORTING" /* ABORTING */;
|
|
2284
|
+
await this.complete(job);
|
|
2285
|
+
}
|
|
2286
|
+
async getByRunId(job_run_id) {
|
|
2287
|
+
const db = await this.getDb();
|
|
2288
|
+
const tx = db.transaction(this.tableName, "readonly");
|
|
2289
|
+
const store = tx.objectStore(this.tableName);
|
|
2290
|
+
const index = store.index("job_run_id");
|
|
2291
|
+
const request = index.getAll(job_run_id);
|
|
2292
|
+
return new Promise((resolve, reject) => {
|
|
2293
|
+
request.onsuccess = () => resolve(request.result);
|
|
2294
|
+
request.onerror = () => reject(request.error);
|
|
2295
|
+
tx.onerror = () => reject(tx.error);
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
async deleteAll() {
|
|
2299
|
+
const db = await this.getDb();
|
|
2300
|
+
const tx = db.transaction(this.tableName, "readwrite");
|
|
2301
|
+
const store = tx.objectStore(this.tableName);
|
|
2302
|
+
const request = store.clear();
|
|
2303
|
+
return new Promise((resolve, reject) => {
|
|
2304
|
+
request.onsuccess = () => resolve();
|
|
2305
|
+
request.onerror = () => reject(request.error);
|
|
2306
|
+
tx.onerror = () => reject(tx.error);
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
async outputForInput(input) {
|
|
2310
|
+
const fingerprint = await makeFingerprint5(input);
|
|
2311
|
+
const db = await this.getDb();
|
|
2312
|
+
const tx = db.transaction(this.tableName, "readonly");
|
|
2313
|
+
const store = tx.objectStore(this.tableName);
|
|
2314
|
+
const index = store.index("fingerprint_status");
|
|
2315
|
+
const request = index.get([fingerprint, "COMPLETED" /* COMPLETED */]);
|
|
2316
|
+
return new Promise((resolve, reject) => {
|
|
2317
|
+
request.onsuccess = () => resolve(request.result?.output ?? null);
|
|
2318
|
+
request.onerror = () => reject(request.error);
|
|
2319
|
+
tx.onerror = () => reject(tx.error);
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
async saveProgress(id, progress, message, details) {
|
|
2323
|
+
const job = await this.get(id);
|
|
2324
|
+
if (!job)
|
|
2325
|
+
throw new Error(`Job ${id} not found`);
|
|
2326
|
+
job.progress = progress;
|
|
2327
|
+
job.progress_message = message;
|
|
2328
|
+
job.progress_details = details;
|
|
2329
|
+
await this.complete(job);
|
|
2330
|
+
}
|
|
2331
|
+
async delete(id) {
|
|
2332
|
+
const db = await this.getDb();
|
|
2333
|
+
const tx = db.transaction(this.tableName, "readwrite");
|
|
2334
|
+
const store = tx.objectStore(this.tableName);
|
|
2335
|
+
const request = store.delete(id);
|
|
2336
|
+
return new Promise((resolve, reject) => {
|
|
2337
|
+
request.onsuccess = () => resolve();
|
|
2338
|
+
request.onerror = () => reject(request.error);
|
|
2339
|
+
tx.onerror = () => reject(tx.error);
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
async deleteJobsByStatusAndAge(status, olderThanMs) {
|
|
2343
|
+
const db = await this.getDb();
|
|
2344
|
+
const tx = db.transaction(this.tableName, "readwrite");
|
|
2345
|
+
const store = tx.objectStore(this.tableName);
|
|
2346
|
+
const index = store.index("status");
|
|
2347
|
+
const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();
|
|
2348
|
+
return new Promise((resolve, reject) => {
|
|
2349
|
+
const request = index.openCursor();
|
|
2350
|
+
request.onsuccess = (event) => {
|
|
2351
|
+
const cursor = event.target.result;
|
|
2352
|
+
if (cursor) {
|
|
2353
|
+
const job = cursor.value;
|
|
2354
|
+
if (job.status === status && job.completed_at && job.completed_at <= cutoffDate) {
|
|
2355
|
+
cursor.delete();
|
|
2356
|
+
}
|
|
2357
|
+
cursor.continue();
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
tx.oncomplete = () => resolve();
|
|
2361
|
+
tx.onerror = () => reject(tx.error);
|
|
2362
|
+
request.onerror = () => reject(request.error);
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
// src/queue/SupabaseQueueStorage.ts
|
|
2367
|
+
import { createServiceToken as createServiceToken14, makeFingerprint as makeFingerprint6, uuid4 as uuid43 } from "@workglow/util";
|
|
2368
|
+
var SUPABASE_QUEUE_STORAGE = createServiceToken14("jobqueue.storage.supabase");
|
|
2369
|
+
|
|
2370
|
+
class SupabaseQueueStorage {
|
|
2371
|
+
client;
|
|
2372
|
+
queueName;
|
|
2373
|
+
constructor(client, queueName) {
|
|
2374
|
+
this.client = client;
|
|
2375
|
+
this.queueName = queueName;
|
|
2376
|
+
}
|
|
2377
|
+
async setupDatabase() {
|
|
2378
|
+
const createTypeSql = `CREATE TYPE job_status AS ENUM (${Object.values(JobStatus).map((v) => `'${v}'`).join(",")})`;
|
|
2379
|
+
const { error: typeError } = await this.client.rpc("exec_sql", { query: createTypeSql });
|
|
2380
|
+
if (typeError && typeError.code !== "42710") {
|
|
2381
|
+
throw typeError;
|
|
2382
|
+
}
|
|
2383
|
+
const createTableSql = `
|
|
2384
|
+
CREATE TABLE IF NOT EXISTS job_queue (
|
|
2385
|
+
id SERIAL NOT NULL,
|
|
2386
|
+
fingerprint text NOT NULL,
|
|
2387
|
+
queue text NOT NULL,
|
|
2388
|
+
job_run_id text NOT NULL,
|
|
2389
|
+
status job_status NOT NULL default 'PENDING',
|
|
2390
|
+
input jsonb NOT NULL,
|
|
2391
|
+
output jsonb,
|
|
2392
|
+
run_attempts integer default 0,
|
|
2393
|
+
max_retries integer default 20,
|
|
2394
|
+
run_after timestamp with time zone DEFAULT now(),
|
|
2395
|
+
last_ran_at timestamp with time zone,
|
|
2396
|
+
created_at timestamp with time zone DEFAULT now(),
|
|
2397
|
+
deadline_at timestamp with time zone,
|
|
2398
|
+
completed_at timestamp with time zone,
|
|
2399
|
+
error text,
|
|
2400
|
+
error_code text,
|
|
2401
|
+
progress real DEFAULT 0,
|
|
2402
|
+
progress_message text DEFAULT '',
|
|
2403
|
+
progress_details jsonb
|
|
2404
|
+
)`;
|
|
2405
|
+
const { error: tableError } = await this.client.rpc("exec_sql", { query: createTableSql });
|
|
2406
|
+
if (tableError) {
|
|
2407
|
+
if (tableError.code !== "42P07") {
|
|
2408
|
+
throw tableError;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
const indexes = [
|
|
2412
|
+
`CREATE INDEX IF NOT EXISTS job_fetcher_idx ON job_queue (id, status, run_after)`,
|
|
2413
|
+
`CREATE INDEX IF NOT EXISTS job_queue_fetcher_idx ON job_queue (queue, status, run_after)`,
|
|
2414
|
+
`CREATE INDEX IF NOT EXISTS jobs_fingerprint_unique_idx ON job_queue (queue, fingerprint, status)`
|
|
2415
|
+
];
|
|
2416
|
+
for (const indexSql of indexes) {
|
|
2417
|
+
const { error: indexError } = await this.client.rpc("exec_sql", { query: indexSql });
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
async add(job) {
|
|
2421
|
+
const now = new Date().toISOString();
|
|
2422
|
+
job.queue = this.queueName;
|
|
2423
|
+
job.job_run_id = job.job_run_id ?? uuid43();
|
|
2424
|
+
job.fingerprint = await makeFingerprint6(job.input);
|
|
2425
|
+
job.status = "PENDING" /* PENDING */;
|
|
2426
|
+
job.progress = 0;
|
|
2427
|
+
job.progress_message = "";
|
|
2428
|
+
job.progress_details = null;
|
|
2429
|
+
job.created_at = now;
|
|
2430
|
+
job.run_after = now;
|
|
2431
|
+
const { data, error } = await this.client.from("job_queue").insert({
|
|
2432
|
+
queue: job.queue,
|
|
2433
|
+
fingerprint: job.fingerprint,
|
|
2434
|
+
input: job.input,
|
|
2435
|
+
run_after: job.run_after,
|
|
2436
|
+
created_at: job.created_at,
|
|
2437
|
+
deadline_at: job.deadline_at,
|
|
2438
|
+
max_retries: job.max_retries,
|
|
2439
|
+
job_run_id: job.job_run_id,
|
|
2440
|
+
progress: job.progress,
|
|
2441
|
+
progress_message: job.progress_message,
|
|
2442
|
+
progress_details: job.progress_details
|
|
2443
|
+
}).select("id").single();
|
|
2444
|
+
if (error)
|
|
2445
|
+
throw error;
|
|
2446
|
+
if (!data)
|
|
2447
|
+
throw new Error("Failed to add to queue");
|
|
2448
|
+
job.id = data.id;
|
|
2449
|
+
return job.id;
|
|
2450
|
+
}
|
|
2451
|
+
async get(id) {
|
|
2452
|
+
const { data, error } = await this.client.from("job_queue").select("*").eq("id", id).eq("queue", this.queueName).single();
|
|
2453
|
+
if (error) {
|
|
2454
|
+
if (error.code === "PGRST116")
|
|
2455
|
+
return;
|
|
2456
|
+
throw error;
|
|
2457
|
+
}
|
|
2458
|
+
return data;
|
|
2459
|
+
}
|
|
2460
|
+
async peek(status = "PENDING" /* PENDING */, num = 100) {
|
|
2461
|
+
num = Number(num) || 100;
|
|
2462
|
+
const { data, error } = await this.client.from("job_queue").select("*").eq("queue", this.queueName).eq("status", status).order("run_after", { ascending: true }).limit(num);
|
|
2463
|
+
if (error)
|
|
2464
|
+
throw error;
|
|
2465
|
+
return data ?? [];
|
|
2466
|
+
}
|
|
2467
|
+
async next() {
|
|
2468
|
+
const { data: jobs, error: selectError } = await this.client.from("job_queue").select("*").eq("queue", this.queueName).eq("status", "PENDING" /* PENDING */).lte("run_after", new Date().toISOString()).order("run_after", { ascending: true }).limit(1);
|
|
2469
|
+
if (selectError)
|
|
2470
|
+
throw selectError;
|
|
2471
|
+
if (!jobs || jobs.length === 0)
|
|
2472
|
+
return;
|
|
2473
|
+
const job = jobs[0];
|
|
2474
|
+
const { data: updatedJob, error: updateError } = await this.client.from("job_queue").update({
|
|
2475
|
+
status: "PROCESSING" /* PROCESSING */,
|
|
2476
|
+
last_ran_at: new Date().toISOString()
|
|
2477
|
+
}).eq("id", job.id).eq("queue", this.queueName).select().single();
|
|
2478
|
+
if (updateError)
|
|
2479
|
+
throw updateError;
|
|
2480
|
+
return updatedJob;
|
|
2481
|
+
}
|
|
2482
|
+
async size(status = "PENDING" /* PENDING */) {
|
|
2483
|
+
const { count, error } = await this.client.from("job_queue").select("*", { count: "exact", head: true }).eq("queue", this.queueName).eq("status", status);
|
|
2484
|
+
if (error)
|
|
2485
|
+
throw error;
|
|
2486
|
+
return count ?? 0;
|
|
2487
|
+
}
|
|
2488
|
+
async complete(jobDetails) {
|
|
2489
|
+
const now = new Date().toISOString();
|
|
2490
|
+
if (jobDetails.status === "DISABLED" /* DISABLED */) {
|
|
2491
|
+
const { error: error2 } = await this.client.from("job_queue").update({
|
|
2492
|
+
status: jobDetails.status,
|
|
2493
|
+
progress: 100,
|
|
2494
|
+
progress_message: "",
|
|
2495
|
+
progress_details: null,
|
|
2496
|
+
completed_at: now,
|
|
2497
|
+
last_ran_at: now
|
|
2498
|
+
}).eq("id", jobDetails.id).eq("queue", this.queueName);
|
|
2499
|
+
if (error2)
|
|
2500
|
+
throw error2;
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const { data: current, error: getError } = await this.client.from("job_queue").select("run_attempts").eq("id", jobDetails.id).eq("queue", this.queueName).single();
|
|
2504
|
+
if (getError)
|
|
2505
|
+
throw getError;
|
|
2506
|
+
const nextAttempts = (current?.run_attempts ?? 0) + 1;
|
|
2507
|
+
if (jobDetails.status === "PENDING" /* PENDING */) {
|
|
2508
|
+
const { error: error2 } = await this.client.from("job_queue").update({
|
|
2509
|
+
error: jobDetails.error ?? null,
|
|
2510
|
+
error_code: jobDetails.error_code ?? null,
|
|
2511
|
+
status: jobDetails.status,
|
|
2512
|
+
run_after: jobDetails.run_after,
|
|
2513
|
+
progress: 0,
|
|
2514
|
+
progress_message: "",
|
|
2515
|
+
progress_details: null,
|
|
2516
|
+
run_attempts: nextAttempts,
|
|
2517
|
+
last_ran_at: now
|
|
2518
|
+
}).eq("id", jobDetails.id).eq("queue", this.queueName);
|
|
2519
|
+
if (error2)
|
|
2520
|
+
throw error2;
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
if (jobDetails.status === "COMPLETED" /* COMPLETED */ || jobDetails.status === "FAILED" /* FAILED */) {
|
|
2524
|
+
const { error: error2 } = await this.client.from("job_queue").update({
|
|
2525
|
+
output: jobDetails.output ?? null,
|
|
2526
|
+
error: jobDetails.error ?? null,
|
|
2527
|
+
error_code: jobDetails.error_code ?? null,
|
|
2528
|
+
status: jobDetails.status,
|
|
2529
|
+
progress: 100,
|
|
2530
|
+
progress_message: "",
|
|
2531
|
+
progress_details: null,
|
|
2532
|
+
run_attempts: nextAttempts,
|
|
2533
|
+
completed_at: now,
|
|
2534
|
+
last_ran_at: now
|
|
2535
|
+
}).eq("id", jobDetails.id).eq("queue", this.queueName);
|
|
2536
|
+
if (error2)
|
|
2537
|
+
throw error2;
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
const { error } = await this.client.from("job_queue").update({
|
|
2541
|
+
status: jobDetails.status,
|
|
2542
|
+
output: jobDetails.output ?? null,
|
|
2543
|
+
error: jobDetails.error ?? null,
|
|
2544
|
+
error_code: jobDetails.error_code ?? null,
|
|
2545
|
+
run_after: jobDetails.run_after ?? null,
|
|
2546
|
+
run_attempts: nextAttempts,
|
|
2547
|
+
last_ran_at: now
|
|
2548
|
+
}).eq("id", jobDetails.id).eq("queue", this.queueName);
|
|
2549
|
+
if (error)
|
|
2550
|
+
throw error;
|
|
2551
|
+
}
|
|
2552
|
+
async deleteAll() {
|
|
2553
|
+
const { error } = await this.client.from("job_queue").delete().eq("queue", this.queueName);
|
|
2554
|
+
if (error)
|
|
2555
|
+
throw error;
|
|
2556
|
+
}
|
|
2557
|
+
async outputForInput(input) {
|
|
2558
|
+
const fingerprint = await makeFingerprint6(input);
|
|
2559
|
+
const { data, error } = await this.client.from("job_queue").select("output").eq("fingerprint", fingerprint).eq("queue", this.queueName).eq("status", "COMPLETED" /* COMPLETED */).single();
|
|
2560
|
+
if (error) {
|
|
2561
|
+
if (error.code === "PGRST116")
|
|
2562
|
+
return null;
|
|
2563
|
+
throw error;
|
|
2564
|
+
}
|
|
2565
|
+
return data?.output ?? null;
|
|
2566
|
+
}
|
|
2567
|
+
async abort(jobId) {
|
|
2568
|
+
const { error } = await this.client.from("job_queue").update({ status: "ABORTING" /* ABORTING */ }).eq("id", jobId).eq("queue", this.queueName);
|
|
2569
|
+
if (error)
|
|
2570
|
+
throw error;
|
|
2571
|
+
}
|
|
2572
|
+
async getByRunId(job_run_id) {
|
|
2573
|
+
const { data, error } = await this.client.from("job_queue").select("*").eq("job_run_id", job_run_id).eq("queue", this.queueName);
|
|
2574
|
+
if (error)
|
|
2575
|
+
throw error;
|
|
2576
|
+
return data ?? [];
|
|
2577
|
+
}
|
|
2578
|
+
async saveProgress(jobId, progress, message, details) {
|
|
2579
|
+
const { error } = await this.client.from("job_queue").update({
|
|
2580
|
+
progress,
|
|
2581
|
+
progress_message: message,
|
|
2582
|
+
progress_details: details
|
|
2583
|
+
}).eq("id", jobId).eq("queue", this.queueName);
|
|
2584
|
+
if (error)
|
|
2585
|
+
throw error;
|
|
2586
|
+
}
|
|
2587
|
+
async delete(jobId) {
|
|
2588
|
+
const { error } = await this.client.from("job_queue").delete().eq("id", jobId).eq("queue", this.queueName);
|
|
2589
|
+
if (error)
|
|
2590
|
+
throw error;
|
|
2591
|
+
}
|
|
2592
|
+
async deleteJobsByStatusAndAge(status, olderThanMs) {
|
|
2593
|
+
const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();
|
|
2594
|
+
const { error } = await this.client.from("job_queue").delete().eq("queue", this.queueName).eq("status", status).not("completed_at", "is", null).lte("completed_at", cutoffDate);
|
|
2595
|
+
if (error)
|
|
2596
|
+
throw error;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
export {
|
|
2600
|
+
ensureIndexedDbTable,
|
|
2601
|
+
dropIndexedDbTable,
|
|
2602
|
+
TabularRepository,
|
|
2603
|
+
TABULAR_REPOSITORY,
|
|
2604
|
+
SupabaseTabularRepository,
|
|
2605
|
+
SupabaseQueueStorage,
|
|
2606
|
+
SupabaseKvRepository,
|
|
2607
|
+
SharedInMemoryTabularRepository,
|
|
2608
|
+
SUPABASE_TABULAR_REPOSITORY,
|
|
2609
|
+
SUPABASE_QUEUE_STORAGE,
|
|
2610
|
+
SUPABASE_KV_REPOSITORY,
|
|
2611
|
+
SHARED_IN_MEMORY_TABULAR_REPOSITORY,
|
|
2612
|
+
QUEUE_STORAGE,
|
|
2613
|
+
MEMORY_TABULAR_REPOSITORY,
|
|
2614
|
+
MEMORY_KV_REPOSITORY,
|
|
2615
|
+
KvViaTabularRepository,
|
|
2616
|
+
KvRepository,
|
|
2617
|
+
KV_REPOSITORY,
|
|
2618
|
+
JobStatus,
|
|
2619
|
+
IndexedDbTabularRepository,
|
|
2620
|
+
IndexedDbQueueStorage,
|
|
2621
|
+
IndexedDbKvRepository,
|
|
2622
|
+
InMemoryTabularRepository,
|
|
2623
|
+
InMemoryQueueStorage,
|
|
2624
|
+
InMemoryKvRepository,
|
|
2625
|
+
IN_MEMORY_QUEUE_STORAGE,
|
|
2626
|
+
INDEXED_DB_QUEUE_STORAGE,
|
|
2627
|
+
IDB_TABULAR_REPOSITORY,
|
|
2628
|
+
IDB_KV_REPOSITORY,
|
|
2629
|
+
DefaultKeyValueSchema,
|
|
2630
|
+
DefaultKeyValueKey,
|
|
2631
|
+
CachedTabularRepository,
|
|
2632
|
+
CACHED_TABULAR_REPOSITORY
|
|
2633
|
+
};
|
|
2634
|
+
|
|
2635
|
+
//# debugId=706648683071DA7B64756E2164756E21
|