@peers-app/peers-sdk 0.15.5 → 0.16.1
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/dist/data/index.d.ts +0 -1
- package/dist/data/index.js +0 -1
- package/dist/data/orm/table-container.d.ts +22 -0
- package/dist/data/orm/table-container.js +68 -2
- package/dist/data/orm/table-container.test.js +57 -2
- package/dist/data/table-definitions-table.d.ts +14 -0
- package/dist/data/table-definitions-table.js +13 -0
- package/dist/data/workflow-logs.js +1 -0
- package/dist/data/workflow-runs.d.ts +2 -13
- package/dist/data/workflow-runs.js +3 -98
- package/dist/data/workflows.js +1 -0
- package/dist/rpc-types.d.ts +1 -0
- package/dist/rpc-types.js +1 -0
- package/dist/types/workflow-run-context.d.ts +2 -0
- package/package.json +1 -1
- package/dist/data/data-locks.d.ts +0 -37
- package/dist/data/data-locks.js +0 -189
- package/dist/data/data-locks.test.d.ts +0 -1
- package/dist/data/data-locks.test.js +0 -464
package/dist/data/index.d.ts
CHANGED
package/dist/data/index.js
CHANGED
|
@@ -17,7 +17,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
__exportStar(require("./assistants"), exports);
|
|
18
18
|
__exportStar(require("./change-tracking"), exports);
|
|
19
19
|
__exportStar(require("./channels"), exports);
|
|
20
|
-
__exportStar(require("./data-locks"), exports);
|
|
21
20
|
__exportStar(require("./device-sync-info"), exports);
|
|
22
21
|
__exportStar(require("./devices"), exports);
|
|
23
22
|
__exportStar(require("./embeddings"), exports);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
+
import type { ITableDefinitionRecord } from "../table-definitions-table";
|
|
2
3
|
import { Table } from "./table";
|
|
3
4
|
import type { ITableDefinition, TableConstructor, TableFactory } from "./table-definitions.type";
|
|
4
5
|
import { type ITableMetaData } from "./types";
|
|
@@ -52,6 +53,27 @@ export declare class TableContainer {
|
|
|
52
53
|
* Returns the table instance if found, or null if no definition exists.
|
|
53
54
|
*/
|
|
54
55
|
getFallbackTable(tableName: string): Promise<Table<any> | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Return the names of all tables whose TableDefinitions record has `deleted` set.
|
|
58
|
+
* Used by sync to exclude change records for deleted tables.
|
|
59
|
+
*
|
|
60
|
+
* @returns Array of table names (as stored in the `name` column of TableDefinitions).
|
|
61
|
+
*/
|
|
62
|
+
getDeletedTableNames(): Promise<string[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Return all TableDefinitions records that have been marked as deleted.
|
|
65
|
+
* Used by the Data Explorer UI to display deleted tables.
|
|
66
|
+
*
|
|
67
|
+
* @returns Full {@link ITableDefinitionRecord} objects with populated `deleted` timestamps.
|
|
68
|
+
*/
|
|
69
|
+
getDeletedTableDefinitions(): Promise<ITableDefinitionRecord[]>;
|
|
70
|
+
/**
|
|
71
|
+
* Return the names of all tables whose metadata marks them as local-only.
|
|
72
|
+
* Used by sync to exclude local-only change records from remote exchange.
|
|
73
|
+
*
|
|
74
|
+
* @returns Array of full table names for local-only table definitions.
|
|
75
|
+
*/
|
|
76
|
+
getLocalOnlyTableNames(): string[];
|
|
55
77
|
/**
|
|
56
78
|
* Register a table definition from a synced TableDefinitions record.
|
|
57
79
|
* These go into the fallback tier (no custom constructor) so they never
|
|
@@ -158,6 +158,15 @@ class TableContainer {
|
|
|
158
158
|
.get(metaData.tableId)
|
|
159
159
|
.then((existing) => {
|
|
160
160
|
if (existing) {
|
|
161
|
+
// Code-registration is authoritative: if the table was marked deleted,
|
|
162
|
+
// clear the flag and force an update so sync propagates the revival.
|
|
163
|
+
if (existing.deleted) {
|
|
164
|
+
record.deleted = undefined;
|
|
165
|
+
return tableDefsTable.save(record, {
|
|
166
|
+
restoreIfDeleted: true,
|
|
167
|
+
saveAsSnapshot: true,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
161
170
|
const result = checkVersionedUpdate(existing.versionNumber, record.versionNumber, existing.metaData, record.metaData);
|
|
162
171
|
if (result === "skip") {
|
|
163
172
|
return;
|
|
@@ -225,7 +234,7 @@ class TableContainer {
|
|
|
225
234
|
if (!this.fallbackSubscribed) {
|
|
226
235
|
this.fallbackSubscribed = true;
|
|
227
236
|
tableDefsTable.dataChanged.subscribe((event) => {
|
|
228
|
-
if (event.op === "delete") {
|
|
237
|
+
if (event.op === "delete" || event.dataObject?.deleted) {
|
|
229
238
|
const md = event.dataObject?.metaData;
|
|
230
239
|
if (md) {
|
|
231
240
|
const name = (0, utils_1.getFullTableName)(md);
|
|
@@ -239,12 +248,63 @@ class TableContainer {
|
|
|
239
248
|
}
|
|
240
249
|
// Query for this specific table definition
|
|
241
250
|
const record = await tableDefsTable.findOne({ name: tableName });
|
|
242
|
-
if (!record)
|
|
251
|
+
if (!record || record.deleted)
|
|
243
252
|
return null;
|
|
244
253
|
this.registerFromTableDefinitionRecord(record);
|
|
245
254
|
// Now instantiate via getTableByName which checks fallbackDefinitions
|
|
246
255
|
return this.getTableByName(tableName);
|
|
247
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Return the names of all tables whose TableDefinitions record has `deleted` set.
|
|
259
|
+
* Used by sync to exclude change records for deleted tables.
|
|
260
|
+
*
|
|
261
|
+
* @returns Array of table names (as stored in the `name` column of TableDefinitions).
|
|
262
|
+
*/
|
|
263
|
+
async getDeletedTableNames() {
|
|
264
|
+
const tableDefsTable = this.tableInstances.TableDefinitions;
|
|
265
|
+
if (!tableDefsTable)
|
|
266
|
+
return [];
|
|
267
|
+
const deletedRecords = await tableDefsTable.list({
|
|
268
|
+
deleted: { $exists: true },
|
|
269
|
+
});
|
|
270
|
+
return deletedRecords.map((r) => r.name);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Return all TableDefinitions records that have been marked as deleted.
|
|
274
|
+
* Used by the Data Explorer UI to display deleted tables.
|
|
275
|
+
*
|
|
276
|
+
* @returns Full {@link ITableDefinitionRecord} objects with populated `deleted` timestamps.
|
|
277
|
+
*/
|
|
278
|
+
async getDeletedTableDefinitions() {
|
|
279
|
+
const tableDefsTable = this.tableInstances.TableDefinitions;
|
|
280
|
+
if (!tableDefsTable)
|
|
281
|
+
return [];
|
|
282
|
+
return tableDefsTable.list({ deleted: { $exists: true } });
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Return the names of all tables whose metadata marks them as local-only.
|
|
286
|
+
* Used by sync to exclude local-only change records from remote exchange.
|
|
287
|
+
*
|
|
288
|
+
* @returns Array of full table names for local-only table definitions.
|
|
289
|
+
*/
|
|
290
|
+
getLocalOnlyTableNames() {
|
|
291
|
+
const names = [];
|
|
292
|
+
const fullDefinitions = [
|
|
293
|
+
...Object.values(table_definitions_system_1.systemTableDefinitions),
|
|
294
|
+
...Object.values(this.tableDefinitions),
|
|
295
|
+
];
|
|
296
|
+
for (const tableDefinition of fullDefinitions) {
|
|
297
|
+
if (tableDefinition.metaData.localOnly) {
|
|
298
|
+
names.push((0, utils_1.getFullTableName)(tableDefinition.metaData));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
for (const [tableName, fallbackDefinition] of Object.entries(this.fallbackDefinitions)) {
|
|
302
|
+
if (fallbackDefinition.metaData.localOnly && !names.includes(tableName)) {
|
|
303
|
+
names.push(tableName);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return names;
|
|
307
|
+
}
|
|
248
308
|
/**
|
|
249
309
|
* Register a table definition from a synced TableDefinitions record.
|
|
250
310
|
* These go into the fallback tier (no custom constructor) so they never
|
|
@@ -260,6 +320,12 @@ class TableContainer {
|
|
|
260
320
|
return;
|
|
261
321
|
}
|
|
262
322
|
const tableName = (0, utils_1.getFullTableName)(metaData);
|
|
323
|
+
// Deleted tables should not be registered as fallbacks
|
|
324
|
+
if (record.deleted) {
|
|
325
|
+
delete this.fallbackDefinitions[tableName];
|
|
326
|
+
delete this.fallbackInstances[tableName];
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
263
329
|
// Don't overwrite a full (code-registered) definition -- that always takes precedence
|
|
264
330
|
const fullDef = this.tableDefinitions[tableName] || table_definitions_system_1.systemTableDefinitions[tableName];
|
|
265
331
|
if (fullDef) {
|
|
@@ -67,7 +67,7 @@ function createTestHarness() {
|
|
|
67
67
|
.fn()
|
|
68
68
|
.mockImplementation((metaData, schema, TableClass) => {
|
|
69
69
|
const deps = {
|
|
70
|
-
dataSource: new client_proxy_data_source_1.ClientProxyDataSource(metaData, schema, "test-context"),
|
|
70
|
+
dataSource: new client_proxy_data_source_1.ClientProxyDataSource(metaData, schema ?? zod_1.z.object({}), "test-context"),
|
|
71
71
|
eventRegistry,
|
|
72
72
|
};
|
|
73
73
|
return new (TableClass || table_1.Table)(metaData, deps);
|
|
@@ -134,7 +134,7 @@ describe("Tiered table instances", () => {
|
|
|
134
134
|
expect(table).not.toBeInstanceOf(CustomTestTable);
|
|
135
135
|
});
|
|
136
136
|
it("getAllTables should prefer full instances over fallback instances", () => {
|
|
137
|
-
const { container, testMetaData, testSchema, defRecord
|
|
137
|
+
const { container, testMetaData, testSchema, defRecord } = createTestHarness();
|
|
138
138
|
// Create a second table that will only have a fallback definition
|
|
139
139
|
const tableId2 = (0, utils_1.newid)();
|
|
140
140
|
const metaData2 = {
|
|
@@ -172,3 +172,58 @@ describe("Tiered table instances", () => {
|
|
|
172
172
|
expect(secondTable).not.toBeInstanceOf(CustomTestTable);
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
|
+
describe("getLocalOnlyTableNames", () => {
|
|
176
|
+
it("returns localOnly code-registered table names and excludes non-local tables", () => {
|
|
177
|
+
const { container, testSchema } = createTestHarness();
|
|
178
|
+
const localOnlyMetaData = {
|
|
179
|
+
tableId: (0, utils_1.newid)(),
|
|
180
|
+
name: "LocalOnlyTable",
|
|
181
|
+
description: "A local-only table",
|
|
182
|
+
primaryKeyName: "id",
|
|
183
|
+
fields: [
|
|
184
|
+
{ name: "id", type: field_type_1.FieldType.id },
|
|
185
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
186
|
+
],
|
|
187
|
+
localOnly: true,
|
|
188
|
+
};
|
|
189
|
+
const syncedMetaData = {
|
|
190
|
+
tableId: (0, utils_1.newid)(),
|
|
191
|
+
name: "SyncedTable",
|
|
192
|
+
description: "A synced table",
|
|
193
|
+
primaryKeyName: "id",
|
|
194
|
+
fields: [
|
|
195
|
+
{ name: "id", type: field_type_1.FieldType.id },
|
|
196
|
+
{ name: "title", type: field_type_1.FieldType.string },
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
container.registerTableDefinition({
|
|
200
|
+
metaData: localOnlyMetaData,
|
|
201
|
+
schema: testSchema,
|
|
202
|
+
});
|
|
203
|
+
container.registerTableDefinition({
|
|
204
|
+
metaData: syncedMetaData,
|
|
205
|
+
schema: testSchema,
|
|
206
|
+
});
|
|
207
|
+
const localOnlyNames = container.getLocalOnlyTableNames();
|
|
208
|
+
expect(localOnlyNames).toContain((0, utils_1.getFullTableName)(localOnlyMetaData));
|
|
209
|
+
expect(localOnlyNames).not.toContain((0, utils_1.getFullTableName)(syncedMetaData));
|
|
210
|
+
});
|
|
211
|
+
it("returns localOnly fallback table names without duplicates", () => {
|
|
212
|
+
const { container, testMetaData, testSchema, defRecord } = createTestHarness();
|
|
213
|
+
const localOnlyMetaData = {
|
|
214
|
+
...testMetaData,
|
|
215
|
+
localOnly: true,
|
|
216
|
+
};
|
|
217
|
+
const localOnlyDefRecord = {
|
|
218
|
+
...defRecord,
|
|
219
|
+
metaData: localOnlyMetaData,
|
|
220
|
+
};
|
|
221
|
+
container.registerFromTableDefinitionRecord(localOnlyDefRecord);
|
|
222
|
+
container.registerTableDefinition({
|
|
223
|
+
metaData: localOnlyMetaData,
|
|
224
|
+
schema: testSchema,
|
|
225
|
+
});
|
|
226
|
+
const localOnlyNames = container.getLocalOnlyTableNames();
|
|
227
|
+
expect(localOnlyNames.filter((name) => name === defRecord.name)).toHaveLength(1);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { DataContext } from "../context/data-context";
|
|
3
|
+
/** Zod schema for rows in the {@link TableDefinitions} system table. */
|
|
3
4
|
export declare const tableDefinitionRecordSchema: z.ZodObject<{
|
|
4
5
|
tableId: z.ZodString;
|
|
5
6
|
name: z.ZodString;
|
|
6
7
|
metaData: z.ZodObject<{}, "strip", z.ZodAny, z.objectOutputType<{}, z.ZodAny, "strip">, z.objectInputType<{}, z.ZodAny, "strip">>;
|
|
7
8
|
versionNumber: z.ZodDefault<z.ZodNumber>;
|
|
9
|
+
deleted: z.ZodOptional<z.ZodDate>;
|
|
8
10
|
}, "strip", z.ZodTypeAny, {
|
|
9
11
|
name: string;
|
|
10
12
|
metaData: {} & {
|
|
@@ -12,15 +14,26 @@ export declare const tableDefinitionRecordSchema: z.ZodObject<{
|
|
|
12
14
|
};
|
|
13
15
|
tableId: string;
|
|
14
16
|
versionNumber: number;
|
|
17
|
+
deleted?: Date | undefined;
|
|
15
18
|
}, {
|
|
16
19
|
name: string;
|
|
17
20
|
metaData: {} & {
|
|
18
21
|
[k: string]: any;
|
|
19
22
|
};
|
|
20
23
|
tableId: string;
|
|
24
|
+
deleted?: Date | undefined;
|
|
21
25
|
versionNumber?: number | undefined;
|
|
22
26
|
}>;
|
|
27
|
+
/** A single row in the {@link TableDefinitions} system table. */
|
|
23
28
|
export type ITableDefinitionRecord = z.infer<typeof tableDefinitionRecordSchema>;
|
|
29
|
+
/**
|
|
30
|
+
* Accessor for the TableDefinitions system table.
|
|
31
|
+
*
|
|
32
|
+
* This table stores table schemas as tracked, synced data so that peers can
|
|
33
|
+
* learn about table definitions without needing the originating package installed.
|
|
34
|
+
*
|
|
35
|
+
* @param dataContext - Optional data context; when omitted the singleton context is used.
|
|
36
|
+
*/
|
|
24
37
|
export declare function TableDefinitions(dataContext?: DataContext): import("./orm").Table<{
|
|
25
38
|
name: string;
|
|
26
39
|
metaData: {} & {
|
|
@@ -28,4 +41,5 @@ export declare function TableDefinitions(dataContext?: DataContext): import("./o
|
|
|
28
41
|
};
|
|
29
42
|
tableId: string;
|
|
30
43
|
versionNumber: number;
|
|
44
|
+
deleted?: Date | undefined;
|
|
31
45
|
}>;
|
|
@@ -7,6 +7,7 @@ const user_context_singleton_1 = require("../context/user-context-singleton");
|
|
|
7
7
|
const zod_types_1 = require("../types/zod-types");
|
|
8
8
|
const table_definitions_system_1 = require("./orm/table-definitions.system");
|
|
9
9
|
const types_1 = require("./orm/types");
|
|
10
|
+
/** Zod schema for rows in the {@link TableDefinitions} system table. */
|
|
10
11
|
exports.tableDefinitionRecordSchema = zod_1.z.object({
|
|
11
12
|
tableId: zod_1.z.string().describe("Primary key - same as ITableMetaData.tableId"),
|
|
12
13
|
name: zod_1.z.string().describe("Table name"),
|
|
@@ -15,6 +16,10 @@ exports.tableDefinitionRecordSchema = zod_1.z.object({
|
|
|
15
16
|
.number()
|
|
16
17
|
.default(0)
|
|
17
18
|
.describe("Version number set by the developer in the table definition; higher value wins during sync"),
|
|
19
|
+
deleted: zod_1.z
|
|
20
|
+
.date()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("When set, the table was deleted at this time. Changes for this table should be ignored during sync."),
|
|
18
23
|
});
|
|
19
24
|
const metaData = {
|
|
20
25
|
name: "TableDefinitions",
|
|
@@ -24,6 +29,14 @@ const metaData = {
|
|
|
24
29
|
indexes: [{ fields: ["name"] }],
|
|
25
30
|
};
|
|
26
31
|
(0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.tableDefinitionRecordSchema);
|
|
32
|
+
/**
|
|
33
|
+
* Accessor for the TableDefinitions system table.
|
|
34
|
+
*
|
|
35
|
+
* This table stores table schemas as tracked, synced data so that peers can
|
|
36
|
+
* learn about table definitions without needing the originating package installed.
|
|
37
|
+
*
|
|
38
|
+
* @param dataContext - Optional data context; when omitted the singleton context is used.
|
|
39
|
+
*/
|
|
27
40
|
function TableDefinitions(dataContext) {
|
|
28
41
|
return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(metaData, exports.tableDefinitionRecordSchema);
|
|
29
42
|
}
|
|
@@ -36,6 +36,7 @@ const metaData = {
|
|
|
36
36
|
description: "The log entries for workflow runs.",
|
|
37
37
|
primaryKeyName: "workflowLogId",
|
|
38
38
|
fields: (0, types_1.schemaToFields)(exports.workflowLogSchema),
|
|
39
|
+
localOnly: true,
|
|
39
40
|
};
|
|
40
41
|
(0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.workflowLogSchema);
|
|
41
42
|
function WorkflowLogs(dataContext) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { DataContext } from "../context/data-context";
|
|
3
|
-
import { type IDataLock } from "./data-locks";
|
|
4
3
|
import { Table } from "./orm/table";
|
|
5
4
|
export declare const workflowRunSchema: z.ZodObject<{
|
|
6
5
|
workflowRunId: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -77,19 +76,9 @@ export declare const workflowRunSchema: z.ZodObject<{
|
|
|
77
76
|
}>;
|
|
78
77
|
export type IWorkflowRun = z.infer<typeof workflowRunSchema>;
|
|
79
78
|
declare class WorkflowRunTable extends Table<IWorkflowRun> {
|
|
80
|
-
/** @deprecated Direct calls to save forbidden; use insert or saveWithLock */
|
|
81
|
-
save(..._args: Parameters<Table<any>["insert"]>): never;
|
|
82
|
-
/** @deprecated Direct calls to update forbidden; use saveWithLock() */
|
|
83
|
-
update(..._args: Parameters<Table<any>["update"]>): never;
|
|
84
|
-
saveWithLock(workflowRun: IWorkflowRun, lock?: IDataLock): Promise<IWorkflowRun>;
|
|
85
|
-
acquireLock(workflowRunId: string, timeoutMs?: number, lockTimeMs?: number): Promise<IDataLock | undefined>;
|
|
86
|
-
saveAndRelease(workflowRun: IWorkflowRun, lock: IDataLock): Promise<void>;
|
|
87
79
|
haltRun(workflowRunId: string): Promise<void>;
|
|
88
|
-
/**
|
|
89
|
-
|
|
90
|
-
* additional editing can be done before it is re-run.
|
|
91
|
-
*/
|
|
92
|
-
clearErrorState(workflowRunId: string): Promise<IDataLock | false>;
|
|
80
|
+
/** Removes the error state from a workflow run so it can be re-run. */
|
|
81
|
+
clearErrorState(workflowRunId: string): Promise<boolean>;
|
|
93
82
|
}
|
|
94
83
|
export declare function WorkflowRuns(dataContext?: DataContext): WorkflowRunTable;
|
|
95
84
|
export interface IRunWorkflowOptions {
|
|
@@ -47,7 +47,6 @@ const zod_types_1 = require("../types/zod-types");
|
|
|
47
47
|
const utils_1 = require("../utils");
|
|
48
48
|
const assistants_1 = require("./assistants");
|
|
49
49
|
const channels_1 = require("./channels");
|
|
50
|
-
const data_locks_1 = require("./data-locks");
|
|
51
50
|
const groups_1 = require("./groups");
|
|
52
51
|
const messages_1 = require("./messages");
|
|
53
52
|
const orm_1 = require("./orm");
|
|
@@ -104,6 +103,7 @@ const metaData = {
|
|
|
104
103
|
description: "workflows instances that have been run manually, from messages, or from events",
|
|
105
104
|
primaryKeyName: "workflowRunId",
|
|
106
105
|
fields: (0, types_1.schemaToFields)(exports.workflowRunSchema),
|
|
106
|
+
localOnly: true,
|
|
107
107
|
indexes: [
|
|
108
108
|
{ fields: ["workflowId"] },
|
|
109
109
|
{ fields: ["parentMessageId"] },
|
|
@@ -123,83 +123,6 @@ let WorkflowRunTable = (() => {
|
|
|
123
123
|
__esDecorate(this, null, _haltRun_decorators, { kind: "method", name: "haltRun", static: false, private: false, access: { has: obj => "haltRun" in obj, get: obj => obj.haltRun }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
124
124
|
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
125
125
|
}
|
|
126
|
-
/** @deprecated Direct calls to save forbidden; use insert or saveWithLock */
|
|
127
|
-
save(..._args) {
|
|
128
|
-
throw new Error("Direct inserts forbidden; use insert() or saveWithLock()");
|
|
129
|
-
}
|
|
130
|
-
/** @deprecated Direct calls to update forbidden; use saveWithLock() */
|
|
131
|
-
update(..._args) {
|
|
132
|
-
throw new Error("Direct updates forbidden; use saveWithLock()");
|
|
133
|
-
}
|
|
134
|
-
async saveWithLock(workflowRun, lock) {
|
|
135
|
-
if (!workflowRun.workflowRunId) {
|
|
136
|
-
workflowRun.workflowRunId = (0, utils_1.newid)();
|
|
137
|
-
return super.insert(workflowRun);
|
|
138
|
-
}
|
|
139
|
-
let lockedForThisSave = false;
|
|
140
|
-
if (!lock) {
|
|
141
|
-
lock = await (0, data_locks_1.DataLocks)().acquireLock(workflowRun.workflowRunId);
|
|
142
|
-
lockedForThisSave = true;
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
const currentLock = await (0, data_locks_1.DataLocks)().getCurrentLock(workflowRun.workflowRunId);
|
|
146
|
-
if (currentLock?.dataLockId !== lock.dataLockId) {
|
|
147
|
-
throw new Error(`Provided lock is not the current lock for workflowRunId ${workflowRun.workflowRunId} - aborting save`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (!lock) {
|
|
151
|
-
throw new Error(`Could not acquire lock for workflow run ${workflowRun.workflowRunId}`);
|
|
152
|
-
}
|
|
153
|
-
if (lock.recordId !== workflowRun.workflowRunId) {
|
|
154
|
-
throw new Error(`Provided lock recordId ${lock?.recordId} does not match workflowRunId ${workflowRun.workflowRunId}`);
|
|
155
|
-
}
|
|
156
|
-
let dbData = await this.get(workflowRun.workflowRunId);
|
|
157
|
-
if (dbData && !lock) {
|
|
158
|
-
const currentLock = await (0, data_locks_1.DataLocks)().getCurrentLock(workflowRun.workflowRunId);
|
|
159
|
-
if (currentLock) {
|
|
160
|
-
throw new Error(`Workflow run ${workflowRun.workflowRunId} is locked by ${currentLock.dataLockId} - the lock will timeout at ${new Date(currentLock.lockedUntil)}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (dbData?.inErrorState) {
|
|
164
|
-
throw new Error(`Workflow run ${workflowRun.workflowRunId} is in an error state - use clearErrorState to continue the run`);
|
|
165
|
-
}
|
|
166
|
-
if (dbData) {
|
|
167
|
-
dbData = await super.update(workflowRun);
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
dbData = await super.insert(workflowRun);
|
|
171
|
-
}
|
|
172
|
-
if (lockedForThisSave) {
|
|
173
|
-
await (0, data_locks_1.DataLocks)().releaseLock(lock);
|
|
174
|
-
}
|
|
175
|
-
return dbData;
|
|
176
|
-
}
|
|
177
|
-
async acquireLock(workflowRunId, timeoutMs, lockTimeMs) {
|
|
178
|
-
const workflowDbData = await this.get(workflowRunId);
|
|
179
|
-
if (!workflowDbData)
|
|
180
|
-
return undefined;
|
|
181
|
-
if (workflowDbData.inErrorState) {
|
|
182
|
-
console.warn(`Workflow run ${workflowRunId} is in an error state - not acquiring lock`);
|
|
183
|
-
return undefined;
|
|
184
|
-
}
|
|
185
|
-
return await (0, data_locks_1.DataLocks)().acquireLock(workflowRunId, timeoutMs, lockTimeMs);
|
|
186
|
-
}
|
|
187
|
-
async saveAndRelease(workflowRun, lock) {
|
|
188
|
-
const workflowDbData = await this.get(workflowRun.workflowRunId);
|
|
189
|
-
if (!workflowDbData)
|
|
190
|
-
return;
|
|
191
|
-
const currentLock = await (0, data_locks_1.DataLocks)().getCurrentLock(workflowRun.workflowRunId);
|
|
192
|
-
if (!currentLock || currentLock.dataLockId !== lock.dataLockId) {
|
|
193
|
-
console.warn(`Workflow run ${workflowRun.workflowRunId} is not locked by ${lock.dataLockId} - aborting save`);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
if (workflowDbData.inErrorState) {
|
|
197
|
-
console.warn(`Workflow run ${workflowRun.workflowRunId} is in an error state - aborting save`);
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
await super.update(workflowRun);
|
|
201
|
-
await (0, data_locks_1.DataLocks)().releaseLock(lock);
|
|
202
|
-
}
|
|
203
126
|
async haltRun(workflowRunId) {
|
|
204
127
|
const run = await super.get(workflowRunId);
|
|
205
128
|
if (!run) {
|
|
@@ -212,16 +135,6 @@ let WorkflowRunTable = (() => {
|
|
|
212
135
|
if (!run.completedAt) {
|
|
213
136
|
run.inErrorState = true;
|
|
214
137
|
await super.update(run);
|
|
215
|
-
// Release any existing lock
|
|
216
|
-
while (true) {
|
|
217
|
-
const currentLock = await (0, data_locks_1.DataLocks)().getCurrentLock(workflowRunId);
|
|
218
|
-
if (currentLock) {
|
|
219
|
-
await (0, data_locks_1.DataLocks)().releaseLock(currentLock);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
break; // No lock to release
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
138
|
// also halt any child runs
|
|
226
139
|
this.list({ parentWorkflowRunId: workflowRunId })
|
|
227
140
|
.then(async (childRuns) => {
|
|
@@ -241,10 +154,7 @@ let WorkflowRunTable = (() => {
|
|
|
241
154
|
logger(runHaltMessage);
|
|
242
155
|
}
|
|
243
156
|
}
|
|
244
|
-
/**
|
|
245
|
-
* Removes the error state from a workflow run and locks it so
|
|
246
|
-
* additional editing can be done before it is re-run.
|
|
247
|
-
*/
|
|
157
|
+
/** Removes the error state from a workflow run so it can be re-run. */
|
|
248
158
|
async clearErrorState(workflowRunId) {
|
|
249
159
|
const run = await super.get(workflowRunId);
|
|
250
160
|
if (!run) {
|
|
@@ -254,14 +164,9 @@ let WorkflowRunTable = (() => {
|
|
|
254
164
|
console.warn(`Workflow run ${workflowRunId} is not in an error state - not clearing`);
|
|
255
165
|
return false;
|
|
256
166
|
}
|
|
257
|
-
const lock = await (0, data_locks_1.DataLocks)().acquireLock(workflowRunId);
|
|
258
|
-
if (!lock) {
|
|
259
|
-
console.warn(`Could not acquire lock for workflow run ${workflowRunId}`);
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
167
|
run.inErrorState = false;
|
|
263
168
|
await super.update(run);
|
|
264
|
-
return
|
|
169
|
+
return true;
|
|
265
170
|
}
|
|
266
171
|
constructor() {
|
|
267
172
|
super(...arguments);
|
package/dist/data/workflows.js
CHANGED
|
@@ -11,6 +11,7 @@ const metaData = {
|
|
|
11
11
|
primaryKeyName: "workflowId",
|
|
12
12
|
fields: (0, types_1.schemaToFields)(workflow_1.workflowSchema),
|
|
13
13
|
iconClassName: "bi bi-database-fill-gear",
|
|
14
|
+
localOnly: true,
|
|
14
15
|
indexes: [{ fields: ["name"] }],
|
|
15
16
|
};
|
|
16
17
|
(0, table_definitions_system_1.registerSystemTableDefinition)(metaData, workflow_1.workflowSchema);
|
package/dist/rpc-types.d.ts
CHANGED
|
@@ -62,6 +62,7 @@ export declare const rpcServerCalls: {
|
|
|
62
62
|
resetChangeTracking: () => Promise<void>;
|
|
63
63
|
deleteLocalDatabase: () => Promise<void>;
|
|
64
64
|
importGroupShare: (groupShareJson: string) => Promise<string>;
|
|
65
|
+
registerWithPeersServices: () => Promise<string>;
|
|
65
66
|
appQuit: () => Promise<void>;
|
|
66
67
|
appRestart: () => Promise<void>;
|
|
67
68
|
dbQuery: (query: string, dataContextId?: string) => Promise<{
|
package/dist/rpc-types.js
CHANGED
|
@@ -35,6 +35,7 @@ exports.rpcServerCalls = {
|
|
|
35
35
|
resetChangeTracking: rpcStub("resetChangeTracking"),
|
|
36
36
|
deleteLocalDatabase: rpcStub("deleteLocalDatabase"),
|
|
37
37
|
importGroupShare: rpcStub("importGroupShare"),
|
|
38
|
+
registerWithPeersServices: rpcStub("registerWithPeersServices"),
|
|
38
39
|
// App lifecycle commands (for CLI control)
|
|
39
40
|
appQuit: rpcStub("appQuit"),
|
|
40
41
|
appRestart: rpcStub("appRestart"),
|
|
@@ -2,8 +2,10 @@ import type { IToolInstance } from "../data/tools";
|
|
|
2
2
|
import type { IWorkflowRun } from "../data/workflow-runs";
|
|
3
3
|
import type { IAssistantRunnerArgs } from "./assistant-runner-args";
|
|
4
4
|
import type { IWorkflowLogger } from "./workflow-logger";
|
|
5
|
+
/** Runtime context passed to tools while processing a workflow run instruction. */
|
|
5
6
|
export interface IWorkflowRunContext {
|
|
6
7
|
contextId: string;
|
|
8
|
+
dataContextId: string;
|
|
7
9
|
workflowRun: IWorkflowRun;
|
|
8
10
|
logger: IWorkflowLogger;
|
|
9
11
|
getAssistantRunnerArgs: (assistantId: string) => Promise<IAssistantRunnerArgs>;
|
package/package.json
CHANGED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { IPeerDevice } from "../types/peer-device";
|
|
3
|
-
import { type ITableDependencies, type ITableMetaData, Table } from "./orm";
|
|
4
|
-
export declare const dataLockSchema: z.ZodObject<{
|
|
5
|
-
dataLockId: z.ZodEffects<z.ZodString, string, string>;
|
|
6
|
-
recordId: z.ZodString;
|
|
7
|
-
lockedUntil: z.ZodNumber;
|
|
8
|
-
acknowledged: z.ZodOptional<z.ZodNumber>;
|
|
9
|
-
}, "strip", z.ZodTypeAny, {
|
|
10
|
-
recordId: string;
|
|
11
|
-
dataLockId: string;
|
|
12
|
-
lockedUntil: number;
|
|
13
|
-
acknowledged?: number | undefined;
|
|
14
|
-
}, {
|
|
15
|
-
recordId: string;
|
|
16
|
-
dataLockId: string;
|
|
17
|
-
lockedUntil: number;
|
|
18
|
-
acknowledged?: number | undefined;
|
|
19
|
-
}>;
|
|
20
|
-
export type IDataLock = z.infer<typeof dataLockSchema>;
|
|
21
|
-
export declare class DataLocksTable extends Table<IDataLock> {
|
|
22
|
-
readonly DEFAULT_LOCK_TIME_MS = 300000;
|
|
23
|
-
readonly DEFAULT_TIMEOUT_MS = 20000;
|
|
24
|
-
readonly DEAD_PERIOD_MS = 10000;
|
|
25
|
-
peerDevice: IPeerDevice | undefined;
|
|
26
|
-
constructor(metaData: ITableMetaData, deps: ITableDependencies);
|
|
27
|
-
getCurrentLock(recordId: string): Promise<IDataLock | undefined>;
|
|
28
|
-
releaseLock(dataLock: IDataLock): Promise<void>;
|
|
29
|
-
createLock(recordId: string, lockTimeMs?: number): Promise<IDataLock>;
|
|
30
|
-
acquireLock(lock: IDataLock, timeoutMs?: number): Promise<IDataLock | undefined>;
|
|
31
|
-
acquireLock(recordId: string, timeoutMs?: number, lockTimeMs?: number): Promise<IDataLock | undefined>;
|
|
32
|
-
renewLock(dataLock: IDataLock, lockTimeMs?: number): Promise<IDataLock | undefined>;
|
|
33
|
-
private countLockAcknowledgements;
|
|
34
|
-
private lockStatus;
|
|
35
|
-
private waitForLockConfirmedAndCurrent;
|
|
36
|
-
}
|
|
37
|
-
export declare function DataLocks(dataContext?: any): DataLocksTable;
|
package/dist/data/data-locks.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DataLocksTable = exports.dataLockSchema = void 0;
|
|
4
|
-
exports.DataLocks = DataLocks;
|
|
5
|
-
const lodash_1 = require("lodash");
|
|
6
|
-
const zod_1 = require("zod");
|
|
7
|
-
const context_1 = require("../context");
|
|
8
|
-
const zod_types_1 = require("../types/zod-types");
|
|
9
|
-
const utils_1 = require("../utils");
|
|
10
|
-
const orm_1 = require("./orm");
|
|
11
|
-
const table_definitions_system_1 = require("./orm/table-definitions.system");
|
|
12
|
-
/*
|
|
13
|
-
This works by waiting for a majority of peers to acknowledge the lock.
|
|
14
|
-
Once that happens it is considered "well propagated" and it also assumes older
|
|
15
|
-
locks from other devices have all been synced but that's not actually guaranteed.
|
|
16
|
-
|
|
17
|
-
This system works okay for a few peers but it won't scale well when _all_ the peers are trying to
|
|
18
|
-
do something like emit a scheduled event at the same time. They'll all try to acquire the lock at the same time
|
|
19
|
-
and even if this works, it'll create a ton of traffic and contention on the locks table.
|
|
20
|
-
|
|
21
|
-
The issue with emitting scheduled events might be an edge case but it still feels like the locking problem
|
|
22
|
-
should be solved in a more robust and scalable way.
|
|
23
|
-
|
|
24
|
-
We already have an election system for preferred connections. This feels like it could be solved in a similar way,
|
|
25
|
-
maybe by just using the existing elections. I also suspect there are going to be other problems that would
|
|
26
|
-
benefit greatly from utilizing an elected coordinator for these kinds of problems that require coordination
|
|
27
|
-
|
|
28
|
-
*/
|
|
29
|
-
exports.dataLockSchema = zod_1.z.object({
|
|
30
|
-
dataLockId: zod_types_1.zodPeerId,
|
|
31
|
-
recordId: zod_1.z.string(),
|
|
32
|
-
lockedUntil: zod_1.z.number(),
|
|
33
|
-
acknowledged: zod_1.z.number().optional(), // timestamp of acknowledgment
|
|
34
|
-
});
|
|
35
|
-
const dataLocksTableMetaData = {
|
|
36
|
-
name: "DataLocks",
|
|
37
|
-
primaryKeyName: "dataLockId",
|
|
38
|
-
description: "Data locks table to track exclusive write access to records in other tables.",
|
|
39
|
-
fields: (0, orm_1.schemaToFields)(exports.dataLockSchema),
|
|
40
|
-
indexes: [{ fields: ["recordId"] }],
|
|
41
|
-
};
|
|
42
|
-
class DataLocksTable extends orm_1.Table {
|
|
43
|
-
DEFAULT_LOCK_TIME_MS = 300_000; // 5 minutes
|
|
44
|
-
DEFAULT_TIMEOUT_MS = 20_000; // 20 seconds
|
|
45
|
-
DEAD_PERIOD_MS = 10_000; // The cannot be renewed in it's last 10 seconds to avoid races
|
|
46
|
-
peerDevice;
|
|
47
|
-
constructor(metaData, deps) {
|
|
48
|
-
super(metaData, deps);
|
|
49
|
-
// signal to other peers when we become aware of a new data lock
|
|
50
|
-
this.dataChanged.subscribe(async (evt) => {
|
|
51
|
-
const dataLock = evt.dataObject;
|
|
52
|
-
if (evt.op === "insert") {
|
|
53
|
-
dataLock.acknowledged = (0, utils_1.getTimestamp)();
|
|
54
|
-
this.dataSource.update(dataLock);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
async getCurrentLock(recordId) {
|
|
59
|
-
const locks = await this.list({
|
|
60
|
-
recordId,
|
|
61
|
-
lockedUntil: { $gt: Date.now() }, // Only consider locks that are still valid
|
|
62
|
-
});
|
|
63
|
-
return (0, lodash_1.sortBy)(locks, "dataLockId")[0];
|
|
64
|
-
}
|
|
65
|
-
async releaseLock(dataLock) {
|
|
66
|
-
// Remove the lock from the database
|
|
67
|
-
await this.delete(dataLock);
|
|
68
|
-
}
|
|
69
|
-
async createLock(recordId, lockTimeMs = this.DEFAULT_LOCK_TIME_MS) {
|
|
70
|
-
const newLock = {
|
|
71
|
-
dataLockId: (0, utils_1.newid)(),
|
|
72
|
-
recordId,
|
|
73
|
-
lockedUntil: Date.now() + lockTimeMs,
|
|
74
|
-
};
|
|
75
|
-
return await this.insert(newLock);
|
|
76
|
-
}
|
|
77
|
-
async acquireLock(recordIdOrLock, timeoutMs = this.DEFAULT_TIMEOUT_MS, lockTimeMs = this.DEFAULT_LOCK_TIME_MS) {
|
|
78
|
-
const lock = typeof recordIdOrLock === "string"
|
|
79
|
-
? await this.createLock(recordIdOrLock, lockTimeMs)
|
|
80
|
-
: recordIdOrLock;
|
|
81
|
-
const recordId = lock.recordId;
|
|
82
|
-
const confirmedLock = await this.waitForLockConfirmedAndCurrent(lock, timeoutMs);
|
|
83
|
-
if (confirmedLock) {
|
|
84
|
-
await this.renewLock(confirmedLock, lockTimeMs);
|
|
85
|
-
const currentLock = await this.getCurrentLock(recordId);
|
|
86
|
-
if (!currentLock || currentLock.dataLockId !== confirmedLock.dataLockId) {
|
|
87
|
-
console.warn(`Lock acquisition failed for ${recordId}`);
|
|
88
|
-
await this.releaseLock(confirmedLock); // clean up unconfirmed lock
|
|
89
|
-
return undefined; // Return undefined if the lock could not be renewed
|
|
90
|
-
}
|
|
91
|
-
return currentLock;
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
await this.releaseLock(lock); // clean up unconfirmed lock
|
|
95
|
-
return undefined; // Return undefined if the lock could not be acquired
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
async renewLock(dataLock, lockTimeMs = this.DEFAULT_LOCK_TIME_MS) {
|
|
99
|
-
if (dataLock.lockedUntil < Date.now() - this.DEAD_PERIOD_MS) {
|
|
100
|
-
console.warn(`Lock renewal rejected for expired lock ${dataLock.dataLockId} for ${dataLock.recordId}`);
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
dataLock.lockedUntil = Date.now() + lockTimeMs;
|
|
104
|
-
const renewedLock = await this.update(dataLock);
|
|
105
|
-
const currentLock = await this.getCurrentLock(dataLock.recordId);
|
|
106
|
-
if (!currentLock || currentLock.dataLockId !== dataLock.dataLockId) {
|
|
107
|
-
// currently we don't allow renewing a lock if it is not the current lock - this could change in the future if needed
|
|
108
|
-
console.warn(`Lock renewal failed for ${dataLock.dataLockId} on ${dataLock.recordId}`);
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
return renewedLock;
|
|
112
|
-
}
|
|
113
|
-
async countLockAcknowledgements(lock) {
|
|
114
|
-
if (!this.peerDevice)
|
|
115
|
-
throw new Error(`Peer device not set for DataLocksTable`);
|
|
116
|
-
const changes = (await this.peerDevice.listChanges({
|
|
117
|
-
tableName: dataLocksTableMetaData.name,
|
|
118
|
-
recordId: lock.dataLockId,
|
|
119
|
-
op: "set", // V2 uses 'set' for updates
|
|
120
|
-
}));
|
|
121
|
-
// In V2, look for changes at path "/acknowledged" or full record changes at path "/"
|
|
122
|
-
const acknowledgedUpdates = changes
|
|
123
|
-
.map((change) => {
|
|
124
|
-
if (change.path === "/acknowledged") {
|
|
125
|
-
return change.value;
|
|
126
|
-
}
|
|
127
|
-
else if (change.path === "/" && change.value && typeof change.value === "object") {
|
|
128
|
-
return change.value.acknowledged;
|
|
129
|
-
}
|
|
130
|
-
return undefined;
|
|
131
|
-
})
|
|
132
|
-
.filter((a) => a !== undefined);
|
|
133
|
-
// NOTE this could potentially miscount if two peers acknowledge the lock with the same timestamp (very unlikely)
|
|
134
|
-
return (0, lodash_1.uniq)(acknowledgedUpdates).length;
|
|
135
|
-
}
|
|
136
|
-
async lockStatus(lock) {
|
|
137
|
-
if (!this.peerDevice)
|
|
138
|
-
throw new Error(`Peer device not set for DataLocksTable`);
|
|
139
|
-
const connectionCount = (await this.peerDevice.getNetworkInfo()).connections.length;
|
|
140
|
-
const ackCnt = await this.countLockAcknowledgements(lock);
|
|
141
|
-
const acksNeeded = Math.ceil(connectionCount * 0.7 + 1); // 70% of the connections and +1 for the local device
|
|
142
|
-
if (ackCnt >= acksNeeded) {
|
|
143
|
-
return "confirmed";
|
|
144
|
-
}
|
|
145
|
-
return "pending";
|
|
146
|
-
}
|
|
147
|
-
async waitForLockConfirmedAndCurrent(lock, timeoutMs = this.DEFAULT_TIMEOUT_MS) {
|
|
148
|
-
const startTime = Date.now();
|
|
149
|
-
let resolveLock;
|
|
150
|
-
const confirmPromise = new Promise((resolve) => {
|
|
151
|
-
resolveLock = resolve;
|
|
152
|
-
});
|
|
153
|
-
const subscription = this.dataChanged.subscribe(async (evt) => {
|
|
154
|
-
if (evt.dataObject.recordId === lock.recordId) {
|
|
155
|
-
checkLockStatus();
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
const checkLockStatus = async () => {
|
|
159
|
-
const status = await this.lockStatus(lock);
|
|
160
|
-
if (status === "confirmed") {
|
|
161
|
-
const currentLock = await this.getCurrentLock(lock.recordId);
|
|
162
|
-
if (currentLock?.dataLockId === lock.dataLockId) {
|
|
163
|
-
subscription.unsubscribe();
|
|
164
|
-
clearTimeout(timeoutId);
|
|
165
|
-
resolveLock(lock);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
console.debug(`Lock confirmed but not current for ${lock.recordId}`);
|
|
169
|
-
}
|
|
170
|
-
if (Date.now() - startTime > timeoutMs) {
|
|
171
|
-
console.warn(`Lock confirmation timed out for ${lock.recordId}`);
|
|
172
|
-
subscription.unsubscribe();
|
|
173
|
-
clearTimeout(timeoutId);
|
|
174
|
-
resolveLock(undefined);
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
const timeoutId = setTimeout(() => {
|
|
178
|
-
checkLockStatus(); // Check lock status after timeout
|
|
179
|
-
}, timeoutMs + 1);
|
|
180
|
-
checkLockStatus(); // Initial check for lock status
|
|
181
|
-
return confirmPromise;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
exports.DataLocksTable = DataLocksTable;
|
|
185
|
-
(0, table_definitions_system_1.registerSystemTableDefinition)(dataLocksTableMetaData, exports.dataLockSchema, DataLocksTable);
|
|
186
|
-
function DataLocks(dataContext) {
|
|
187
|
-
const tableFactory = (0, context_1.getTableContainer)(dataContext);
|
|
188
|
-
return tableFactory.getTable(dataLocksTableMetaData);
|
|
189
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const SQLiteDB = require("better-sqlite3");
|
|
4
|
-
const utils_1 = require("../utils");
|
|
5
|
-
const data_locks_1 = require("./data-locks");
|
|
6
|
-
const group_member_roles_1 = require("./group-member-roles");
|
|
7
|
-
const sql_data_source_1 = require("./orm/sql.data-source");
|
|
8
|
-
const types_1 = require("./orm/types");
|
|
9
|
-
class DBHarness {
|
|
10
|
-
_db = null;
|
|
11
|
-
get database() {
|
|
12
|
-
if (!this._db) {
|
|
13
|
-
this._db = new SQLiteDB(":memory:");
|
|
14
|
-
this._db.pragma("journal_mode = WAL");
|
|
15
|
-
}
|
|
16
|
-
return this._db;
|
|
17
|
-
}
|
|
18
|
-
async get(sql, params = []) {
|
|
19
|
-
return this.database.prepare(sql).get(params);
|
|
20
|
-
}
|
|
21
|
-
async all(sql, params = []) {
|
|
22
|
-
return this.database.prepare(sql).all(params);
|
|
23
|
-
}
|
|
24
|
-
async exec(sql, params = []) {
|
|
25
|
-
const result = this.database.prepare(sql).run(params);
|
|
26
|
-
return { changes: result.changes };
|
|
27
|
-
}
|
|
28
|
-
async close() {
|
|
29
|
-
this._db?.close();
|
|
30
|
-
this._db = null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// Mock peer device for testing
|
|
34
|
-
class MockPeerDevice {
|
|
35
|
-
deviceId = "mock-device";
|
|
36
|
-
userId = "mock-user";
|
|
37
|
-
role = group_member_roles_1.GroupMemberRole.Owner;
|
|
38
|
-
connections = [];
|
|
39
|
-
changes = [];
|
|
40
|
-
constructor(connectionCount = 3) {
|
|
41
|
-
this.connections = Array.from({ length: connectionCount }, (_, i) => ({
|
|
42
|
-
deviceId: `peer-${i}`,
|
|
43
|
-
latencyMs: 50,
|
|
44
|
-
errorRate: 0,
|
|
45
|
-
timestampLastApplied: Date.now(),
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
48
|
-
async getNetworkInfo() {
|
|
49
|
-
return {
|
|
50
|
-
deviceId: this.deviceId,
|
|
51
|
-
timestampLastApplied: Date.now(),
|
|
52
|
-
connections: this.connections,
|
|
53
|
-
preferredDeviceIds: [],
|
|
54
|
-
cpuPercent: 10,
|
|
55
|
-
memPercent: 10,
|
|
56
|
-
connectionSlotsAvailable: 10,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
async listChanges(query) {
|
|
60
|
-
if (!query)
|
|
61
|
-
return this.changes;
|
|
62
|
-
return this.changes.filter((change) => change.tableName === query.tableName &&
|
|
63
|
-
change.recordId === query.recordId &&
|
|
64
|
-
change.op === query.op);
|
|
65
|
-
}
|
|
66
|
-
async notifyOfChanges(_deviceId, _timestampLastApplied) {
|
|
67
|
-
// Mock implementation
|
|
68
|
-
}
|
|
69
|
-
async downloadFileChunk(_chunkHash) {
|
|
70
|
-
// Mock implementation - return null to indicate chunk not found
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
async getFileChunkInfo(_chunkHash) {
|
|
74
|
-
// Mock implementation - return that we don't have the chunk
|
|
75
|
-
return { hasChunk: false };
|
|
76
|
-
}
|
|
77
|
-
addAcknowledgment(recordId, acknowledged) {
|
|
78
|
-
this.changes.push({
|
|
79
|
-
changeId: (0, utils_1.newid)(),
|
|
80
|
-
tableName: "DataLocks",
|
|
81
|
-
recordId,
|
|
82
|
-
op: "set",
|
|
83
|
-
path: "/acknowledged",
|
|
84
|
-
value: acknowledged,
|
|
85
|
-
createdAt: Date.now(),
|
|
86
|
-
appliedAt: Date.now(),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
setConnectionCount(count) {
|
|
90
|
-
this.connections = Array.from({ length: count }, (_, i) => ({
|
|
91
|
-
deviceId: `peer-${i}`,
|
|
92
|
-
latencyMs: 50,
|
|
93
|
-
errorRate: 0,
|
|
94
|
-
timestampLastApplied: Date.now(),
|
|
95
|
-
}));
|
|
96
|
-
}
|
|
97
|
-
sendDeviceMessage(_message) {
|
|
98
|
-
throw new Error("Method not implemented.");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
describe("data-locks.ts", () => {
|
|
102
|
-
const fiveMinutesMs = 5 * 60 * 1000;
|
|
103
|
-
jest.setTimeout(fiveMinutesMs);
|
|
104
|
-
let db;
|
|
105
|
-
let dataSource;
|
|
106
|
-
let dataLocksTable;
|
|
107
|
-
let mockPeerDevice;
|
|
108
|
-
const dataLocksTableMetaData = {
|
|
109
|
-
name: "DataLocks",
|
|
110
|
-
primaryKeyName: "dataLockId",
|
|
111
|
-
description: "Data locks table to track exclusive write access to records in other tables.",
|
|
112
|
-
fields: (0, types_1.schemaToFields)(data_locks_1.dataLockSchema),
|
|
113
|
-
indexes: [{ fields: ["recordId"] }],
|
|
114
|
-
};
|
|
115
|
-
beforeAll(async () => {
|
|
116
|
-
db = new DBHarness();
|
|
117
|
-
dataSource = new sql_data_source_1.SQLDataSource(db, dataLocksTableMetaData);
|
|
118
|
-
const mockDataContextForRegistry = {
|
|
119
|
-
dataContextId: "data-locks-test",
|
|
120
|
-
};
|
|
121
|
-
const { EventRegistry } = require("./orm/event-registry");
|
|
122
|
-
const eventRegistry = new EventRegistry(mockDataContextForRegistry);
|
|
123
|
-
const _mockDataContext = {
|
|
124
|
-
dataSourceFactory: () => dataSource,
|
|
125
|
-
eventRegistry: eventRegistry,
|
|
126
|
-
};
|
|
127
|
-
const deps = {
|
|
128
|
-
dataSource,
|
|
129
|
-
eventRegistry,
|
|
130
|
-
schema: data_locks_1.dataLockSchema,
|
|
131
|
-
};
|
|
132
|
-
dataLocksTable = new data_locks_1.DataLocksTable(dataLocksTableMetaData, deps);
|
|
133
|
-
mockPeerDevice = new MockPeerDevice();
|
|
134
|
-
dataLocksTable.peerDevice = mockPeerDevice;
|
|
135
|
-
await dataSource.dropTableIfExists();
|
|
136
|
-
});
|
|
137
|
-
afterAll(async () => {
|
|
138
|
-
await db.close();
|
|
139
|
-
});
|
|
140
|
-
beforeEach(async () => {
|
|
141
|
-
// Clean up any existing locks before each test
|
|
142
|
-
const allLocks = await dataLocksTable.list();
|
|
143
|
-
for (const lock of allLocks) {
|
|
144
|
-
await dataLocksTable.delete(lock);
|
|
145
|
-
}
|
|
146
|
-
mockPeerDevice.setConnectionCount(3);
|
|
147
|
-
});
|
|
148
|
-
describe("DataLocksTable", () => {
|
|
149
|
-
describe("constructor", () => {
|
|
150
|
-
it("should create a DataLocksTable instance with default constants", () => {
|
|
151
|
-
expect(dataLocksTable.DEFAULT_LOCK_TIME_MS).toBe(300_000); // 5 minutes
|
|
152
|
-
expect(dataLocksTable.DEFAULT_TIMEOUT_MS).toBe(20_000); // 20 seconds
|
|
153
|
-
expect(dataLocksTable.DEAD_PERIOD_MS).toBe(10_000); // 10 seconds
|
|
154
|
-
});
|
|
155
|
-
it("should have peerDevice property", () => {
|
|
156
|
-
expect(dataLocksTable.peerDevice).toBeDefined();
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
describe("getCurrentLock", () => {
|
|
160
|
-
it("should return undefined when no locks exist for recordId", async () => {
|
|
161
|
-
const result = await dataLocksTable.getCurrentLock("nonexistent-record");
|
|
162
|
-
expect(result).toBeUndefined();
|
|
163
|
-
});
|
|
164
|
-
it("should return undefined when only expired locks exist", async () => {
|
|
165
|
-
const recordId = (0, utils_1.newid)();
|
|
166
|
-
const expiredLock = {
|
|
167
|
-
dataLockId: (0, utils_1.newid)(),
|
|
168
|
-
recordId,
|
|
169
|
-
lockedUntil: Date.now() - 1000, // Expired 1 second ago
|
|
170
|
-
};
|
|
171
|
-
await dataLocksTable.insert(expiredLock);
|
|
172
|
-
const result = await dataLocksTable.getCurrentLock(recordId);
|
|
173
|
-
expect(result).toBeUndefined();
|
|
174
|
-
});
|
|
175
|
-
it("should return the current valid lock", async () => {
|
|
176
|
-
const recordId = (0, utils_1.newid)();
|
|
177
|
-
const validLock = {
|
|
178
|
-
dataLockId: (0, utils_1.newid)(),
|
|
179
|
-
recordId,
|
|
180
|
-
lockedUntil: Date.now() + 60000, // Expires in 1 minute
|
|
181
|
-
};
|
|
182
|
-
await dataLocksTable.insert(validLock);
|
|
183
|
-
const result = await dataLocksTable.getCurrentLock(recordId);
|
|
184
|
-
expect(result).toEqual(validLock);
|
|
185
|
-
});
|
|
186
|
-
it("should return the earliest lock when multiple valid locks exist", async () => {
|
|
187
|
-
const recordId = (0, utils_1.newid)();
|
|
188
|
-
const lock1 = {
|
|
189
|
-
dataLockId: (0, utils_1.newid)(),
|
|
190
|
-
recordId,
|
|
191
|
-
lockedUntil: Date.now() + 60000,
|
|
192
|
-
};
|
|
193
|
-
const lock2 = {
|
|
194
|
-
dataLockId: (0, utils_1.newid)(),
|
|
195
|
-
recordId,
|
|
196
|
-
lockedUntil: Date.now() + 60000,
|
|
197
|
-
};
|
|
198
|
-
// Insert them in order so we can predict which should be first
|
|
199
|
-
const firstLock = lock1.dataLockId < lock2.dataLockId ? lock1 : lock2;
|
|
200
|
-
const secondLock = lock1.dataLockId < lock2.dataLockId ? lock2 : lock1;
|
|
201
|
-
await dataLocksTable.insert(secondLock);
|
|
202
|
-
await dataLocksTable.insert(firstLock);
|
|
203
|
-
const result = await dataLocksTable.getCurrentLock(recordId);
|
|
204
|
-
expect(result?.dataLockId).toBe(firstLock.dataLockId);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
describe("releaseLock", () => {
|
|
208
|
-
it("should delete the lock from the database", async () => {
|
|
209
|
-
const lock = {
|
|
210
|
-
dataLockId: (0, utils_1.newid)(),
|
|
211
|
-
recordId: (0, utils_1.newid)(),
|
|
212
|
-
lockedUntil: Date.now() + 60000,
|
|
213
|
-
};
|
|
214
|
-
await dataLocksTable.insert(lock);
|
|
215
|
-
expect(await dataLocksTable.get(lock.dataLockId)).toEqual(lock);
|
|
216
|
-
await dataLocksTable.releaseLock(lock);
|
|
217
|
-
expect(await dataLocksTable.get(lock.dataLockId)).toBeUndefined();
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
describe("acquireLock", () => {
|
|
221
|
-
it("should acquire a lock when no existing locks", async () => {
|
|
222
|
-
const recordId = (0, utils_1.newid)();
|
|
223
|
-
// Mock sufficient acknowledgments for lock confirmation
|
|
224
|
-
const mockAcquireLock = jest.spyOn(dataLocksTable, "acquireLock");
|
|
225
|
-
mockAcquireLock.mockImplementation(async () => {
|
|
226
|
-
const lock = {
|
|
227
|
-
dataLockId: (0, utils_1.newid)(),
|
|
228
|
-
recordId,
|
|
229
|
-
lockedUntil: Date.now() + dataLocksTable.DEFAULT_LOCK_TIME_MS,
|
|
230
|
-
};
|
|
231
|
-
await dataLocksTable.insert(lock);
|
|
232
|
-
return lock;
|
|
233
|
-
});
|
|
234
|
-
const result = await dataLocksTable.acquireLock(recordId);
|
|
235
|
-
expect(result).toBeDefined();
|
|
236
|
-
expect(result?.recordId).toBe(recordId);
|
|
237
|
-
expect(result?.lockedUntil).toBeGreaterThan(Date.now());
|
|
238
|
-
mockAcquireLock.mockRestore();
|
|
239
|
-
});
|
|
240
|
-
it("should use custom timeout and lock time", async () => {
|
|
241
|
-
const recordId = (0, utils_1.newid)();
|
|
242
|
-
const customTimeout = 5000;
|
|
243
|
-
const customLockTime = 120000;
|
|
244
|
-
const mockAcquireLock = jest.spyOn(dataLocksTable, "acquireLock");
|
|
245
|
-
mockAcquireLock.mockImplementation(async (recordId, timeoutMs, lockTimeMs) => {
|
|
246
|
-
expect(timeoutMs).toBe(customTimeout);
|
|
247
|
-
expect(lockTimeMs).toBe(customLockTime);
|
|
248
|
-
return {
|
|
249
|
-
dataLockId: (0, utils_1.newid)(),
|
|
250
|
-
recordId,
|
|
251
|
-
lockedUntil: Date.now() + (lockTimeMs || dataLocksTable.DEFAULT_LOCK_TIME_MS),
|
|
252
|
-
};
|
|
253
|
-
});
|
|
254
|
-
await dataLocksTable.acquireLock(recordId, customTimeout, customLockTime);
|
|
255
|
-
mockAcquireLock.mockRestore();
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
describe("renewLock", () => {
|
|
259
|
-
it("should renew a valid lock", async () => {
|
|
260
|
-
const lock = {
|
|
261
|
-
dataLockId: (0, utils_1.newid)(),
|
|
262
|
-
recordId: (0, utils_1.newid)(),
|
|
263
|
-
lockedUntil: Date.now() + 60000, // 1 minute from now
|
|
264
|
-
};
|
|
265
|
-
await dataLocksTable.insert(lock);
|
|
266
|
-
const originalLockedUntil = lock.lockedUntil;
|
|
267
|
-
// Small delay to ensure time difference
|
|
268
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
269
|
-
await sleep(10);
|
|
270
|
-
const renewedLock = await dataLocksTable.renewLock(lock, 120000);
|
|
271
|
-
expect(renewedLock).toBeDefined();
|
|
272
|
-
expect(renewedLock?.lockedUntil).toBeGreaterThan(originalLockedUntil);
|
|
273
|
-
});
|
|
274
|
-
it("should reject renewal of expired lock", async () => {
|
|
275
|
-
const lock = {
|
|
276
|
-
dataLockId: (0, utils_1.newid)(),
|
|
277
|
-
recordId: (0, utils_1.newid)(),
|
|
278
|
-
lockedUntil: Date.now() - 20000, // Expired 20 seconds ago (beyond DEAD_PERIOD_MS)
|
|
279
|
-
};
|
|
280
|
-
await dataLocksTable.insert(lock);
|
|
281
|
-
const result = await dataLocksTable.renewLock(lock);
|
|
282
|
-
expect(result).toBeUndefined();
|
|
283
|
-
});
|
|
284
|
-
it("should reject renewal when lock is no longer current", async () => {
|
|
285
|
-
const recordId = (0, utils_1.newid)();
|
|
286
|
-
// getCurrentLock returns the lock with the lexicographically first dataLockId,
|
|
287
|
-
// so ensure newLockId sorts before oldLockId
|
|
288
|
-
const [newLockId, oldLockId] = [(0, utils_1.newid)(), (0, utils_1.newid)()].sort();
|
|
289
|
-
const oldLock = {
|
|
290
|
-
dataLockId: oldLockId,
|
|
291
|
-
recordId,
|
|
292
|
-
lockedUntil: Date.now() + 60000,
|
|
293
|
-
};
|
|
294
|
-
const newLock = {
|
|
295
|
-
dataLockId: newLockId,
|
|
296
|
-
recordId,
|
|
297
|
-
lockedUntil: Date.now() + 60000,
|
|
298
|
-
};
|
|
299
|
-
await dataLocksTable.insert(oldLock);
|
|
300
|
-
await dataLocksTable.insert(newLock);
|
|
301
|
-
// Try to renew the old lock - should fail because new lock is current (sorts first)
|
|
302
|
-
const result = await dataLocksTable.renewLock(oldLock);
|
|
303
|
-
expect(result).toBeUndefined();
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
describe("lockStatus", () => {
|
|
307
|
-
it("should return confirmed when sufficient acknowledgments", async () => {
|
|
308
|
-
const lock = {
|
|
309
|
-
dataLockId: (0, utils_1.newid)(),
|
|
310
|
-
recordId: (0, utils_1.newid)(),
|
|
311
|
-
lockedUntil: Date.now() + 60000,
|
|
312
|
-
};
|
|
313
|
-
// Add sufficient acknowledgments (need 4 for 3 connections: Math.ceil(3 * 0.7) + 1 = 4)
|
|
314
|
-
mockPeerDevice.addAcknowledgment(lock.dataLockId, Date.now());
|
|
315
|
-
mockPeerDevice.addAcknowledgment(lock.dataLockId, Date.now() + 1);
|
|
316
|
-
mockPeerDevice.addAcknowledgment(lock.dataLockId, Date.now() + 2);
|
|
317
|
-
mockPeerDevice.addAcknowledgment(lock.dataLockId, Date.now() + 3);
|
|
318
|
-
const status = await dataLocksTable.lockStatus(lock);
|
|
319
|
-
expect(status).toBe("confirmed");
|
|
320
|
-
});
|
|
321
|
-
it("should return pending when insufficient acknowledgments", async () => {
|
|
322
|
-
const lock = {
|
|
323
|
-
dataLockId: (0, utils_1.newid)(),
|
|
324
|
-
recordId: (0, utils_1.newid)(),
|
|
325
|
-
lockedUntil: Date.now() + 60000,
|
|
326
|
-
};
|
|
327
|
-
// Add insufficient acknowledgments (only 1, need 4)
|
|
328
|
-
mockPeerDevice.addAcknowledgment(lock.dataLockId, Date.now());
|
|
329
|
-
const status = await dataLocksTable.lockStatus(lock);
|
|
330
|
-
expect(status).toBe("pending");
|
|
331
|
-
});
|
|
332
|
-
it("should calculate acknowledgments needed correctly for different network sizes", async () => {
|
|
333
|
-
const lock = {
|
|
334
|
-
dataLockId: (0, utils_1.newid)(),
|
|
335
|
-
recordId: (0, utils_1.newid)(),
|
|
336
|
-
lockedUntil: Date.now() + 60000,
|
|
337
|
-
};
|
|
338
|
-
// Test with 1 connection (need Math.ceil(1 * 0.7) + 1 = 2 acknowledgments)
|
|
339
|
-
mockPeerDevice.setConnectionCount(1);
|
|
340
|
-
mockPeerDevice.addAcknowledgment(lock.dataLockId, Date.now());
|
|
341
|
-
mockPeerDevice.addAcknowledgment(lock.dataLockId, Date.now() + 1);
|
|
342
|
-
let status = await dataLocksTable.lockStatus(lock);
|
|
343
|
-
expect(status).toBe("confirmed");
|
|
344
|
-
// Test with 5 connections (need Math.ceil(5 * 0.7) + 1 = 5 acknowledgments)
|
|
345
|
-
mockPeerDevice.setConnectionCount(5);
|
|
346
|
-
status = await dataLocksTable.lockStatus(lock);
|
|
347
|
-
expect(status).toBe("pending"); // Still only 2 acknowledgments, need 5
|
|
348
|
-
});
|
|
349
|
-
it("should throw error when peerDevice is not set", async () => {
|
|
350
|
-
const lock = {
|
|
351
|
-
dataLockId: (0, utils_1.newid)(),
|
|
352
|
-
recordId: (0, utils_1.newid)(),
|
|
353
|
-
lockedUntil: Date.now() + 60000,
|
|
354
|
-
};
|
|
355
|
-
const originalPeerDevice = dataLocksTable.peerDevice;
|
|
356
|
-
dataLocksTable.peerDevice = undefined;
|
|
357
|
-
await expect(dataLocksTable.lockStatus(lock)).rejects.toThrow("Peer device not set for DataLocksTable");
|
|
358
|
-
dataLocksTable.peerDevice = originalPeerDevice;
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
describe("dataLockSchema", () => {
|
|
363
|
-
it("should validate valid lock data", () => {
|
|
364
|
-
const validLock = {
|
|
365
|
-
dataLockId: (0, utils_1.newid)(),
|
|
366
|
-
recordId: "test-record",
|
|
367
|
-
lockedUntil: Date.now() + 60000,
|
|
368
|
-
};
|
|
369
|
-
const result = data_locks_1.dataLockSchema.safeParse(validLock);
|
|
370
|
-
expect(result.success).toBe(true);
|
|
371
|
-
});
|
|
372
|
-
it("should validate lock with acknowledgment", () => {
|
|
373
|
-
const validLock = {
|
|
374
|
-
dataLockId: (0, utils_1.newid)(),
|
|
375
|
-
recordId: "test-record",
|
|
376
|
-
lockedUntil: Date.now() + 60000,
|
|
377
|
-
acknowledged: Date.now(),
|
|
378
|
-
};
|
|
379
|
-
const result = data_locks_1.dataLockSchema.safeParse(validLock);
|
|
380
|
-
expect(result.success).toBe(true);
|
|
381
|
-
});
|
|
382
|
-
it("should reject invalid lock data", () => {
|
|
383
|
-
const invalidLock = {
|
|
384
|
-
dataLockId: 123, // Should be string
|
|
385
|
-
recordId: "test-record",
|
|
386
|
-
lockedUntil: "invalid", // Should be number
|
|
387
|
-
};
|
|
388
|
-
const result = data_locks_1.dataLockSchema.safeParse(invalidLock);
|
|
389
|
-
expect(result.success).toBe(false);
|
|
390
|
-
});
|
|
391
|
-
it("should reject missing required fields", () => {
|
|
392
|
-
const incompleteLock = {
|
|
393
|
-
dataLockId: (0, utils_1.newid)(),
|
|
394
|
-
// Missing recordId and lockedUntil
|
|
395
|
-
};
|
|
396
|
-
const result = data_locks_1.dataLockSchema.safeParse(incompleteLock);
|
|
397
|
-
expect(result.success).toBe(false);
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
describe("DataLocks factory function", () => {
|
|
401
|
-
it("should return a DataLocksTable instance", () => {
|
|
402
|
-
// This is a simplified test - in practice you'd need to set up the factory properly
|
|
403
|
-
expect(typeof data_locks_1.DataLocks).toBe("function");
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
describe("integration scenarios", () => {
|
|
407
|
-
it("should handle concurrent lock attempts", async () => {
|
|
408
|
-
const recordId = (0, utils_1.newid)();
|
|
409
|
-
// This is a simplified version - real concurrent testing would require more complex setup
|
|
410
|
-
const mockInsert = jest.spyOn(dataLocksTable, "insert");
|
|
411
|
-
let insertCount = 0;
|
|
412
|
-
mockInsert.mockImplementation(async (lock) => {
|
|
413
|
-
insertCount++;
|
|
414
|
-
return { ...lock, dataLockId: `lock-${insertCount}` };
|
|
415
|
-
});
|
|
416
|
-
const promises = [
|
|
417
|
-
dataLocksTable.insert({ dataLockId: (0, utils_1.newid)(), recordId, lockedUntil: Date.now() + 60000 }),
|
|
418
|
-
dataLocksTable.insert({ dataLockId: (0, utils_1.newid)(), recordId, lockedUntil: Date.now() + 60000 }),
|
|
419
|
-
dataLocksTable.insert({ dataLockId: (0, utils_1.newid)(), recordId, lockedUntil: Date.now() + 60000 }),
|
|
420
|
-
];
|
|
421
|
-
const results = await Promise.all(promises);
|
|
422
|
-
expect(results).toHaveLength(3);
|
|
423
|
-
expect(insertCount).toBe(3);
|
|
424
|
-
mockInsert.mockRestore();
|
|
425
|
-
});
|
|
426
|
-
it("should handle lock expiration cleanup", async () => {
|
|
427
|
-
const recordId = (0, utils_1.newid)();
|
|
428
|
-
const expiredLock = {
|
|
429
|
-
dataLockId: (0, utils_1.newid)(),
|
|
430
|
-
recordId,
|
|
431
|
-
lockedUntil: Date.now() - 1000, // Expired
|
|
432
|
-
};
|
|
433
|
-
const validLock = {
|
|
434
|
-
dataLockId: (0, utils_1.newid)(),
|
|
435
|
-
recordId,
|
|
436
|
-
lockedUntil: Date.now() + 60000, // Valid
|
|
437
|
-
};
|
|
438
|
-
await dataLocksTable.insert(expiredLock);
|
|
439
|
-
await dataLocksTable.insert(validLock);
|
|
440
|
-
const currentLock = await dataLocksTable.getCurrentLock(recordId);
|
|
441
|
-
expect(currentLock?.dataLockId).toBe(validLock.dataLockId);
|
|
442
|
-
});
|
|
443
|
-
it("should handle multiple records with independent locks", async () => {
|
|
444
|
-
const record1 = (0, utils_1.newid)();
|
|
445
|
-
const record2 = (0, utils_1.newid)();
|
|
446
|
-
const lock1 = {
|
|
447
|
-
dataLockId: (0, utils_1.newid)(),
|
|
448
|
-
recordId: record1,
|
|
449
|
-
lockedUntil: Date.now() + 60000,
|
|
450
|
-
};
|
|
451
|
-
const lock2 = {
|
|
452
|
-
dataLockId: (0, utils_1.newid)(),
|
|
453
|
-
recordId: record2,
|
|
454
|
-
lockedUntil: Date.now() + 60000,
|
|
455
|
-
};
|
|
456
|
-
await dataLocksTable.insert(lock1);
|
|
457
|
-
await dataLocksTable.insert(lock2);
|
|
458
|
-
const currentLock1 = await dataLocksTable.getCurrentLock(record1);
|
|
459
|
-
const currentLock2 = await dataLocksTable.getCurrentLock(record2);
|
|
460
|
-
expect(currentLock1?.dataLockId).toBe(lock1.dataLockId);
|
|
461
|
-
expect(currentLock2?.dataLockId).toBe(lock2.dataLockId);
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
});
|