@hasna/todos 0.15.7 → 0.15.9
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/cli/cloud-router.d.ts +13 -1
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2300 -1123
- package/dist/contracts.js +35 -1
- package/dist/db/task-lists.d.ts +5 -0
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/index.js +489 -35
- package/dist/lib/assignee-validation.d.ts +44 -0
- package/dist/lib/assignee-validation.d.ts.map +1 -1
- package/dist/lib/project-task-list-ensure.d.ts +19 -0
- package/dist/lib/project-task-list-ensure.d.ts.map +1 -0
- package/dist/mcp/index.js +1085 -5
- package/dist/mcp.js +1 -1
- package/dist/project-registration/sqlite.d.ts.map +1 -1
- package/dist/project-registration.js +491 -35
- package/dist/registry.js +35 -1
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +21 -0
- package/dist/sdk/v1.generated.d.ts +43 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1179 -99
- package/dist/server/openapi.d.ts +311 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +11 -0
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage.js +36 -1
- package/dist/types/index.d.ts +29 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/contracts.js
CHANGED
|
@@ -6747,6 +6747,40 @@ function deleteTaskList(id, db) {
|
|
|
6747
6747
|
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
6748
6748
|
})();
|
|
6749
6749
|
}
|
|
6750
|
+
function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
|
|
6751
|
+
const d = db || getDatabase();
|
|
6752
|
+
return d.transaction(() => {
|
|
6753
|
+
const current = getTaskList(id, d);
|
|
6754
|
+
if (!current) {
|
|
6755
|
+
return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
|
|
6756
|
+
}
|
|
6757
|
+
const changed = current.project_id !== expected.project_id || current.slug !== expected.slug || current.name !== expected.name || current.description !== expected.description || current.updated_at !== expected.updated_at || JSON.stringify(current.metadata) !== JSON.stringify(expected.metadata);
|
|
6758
|
+
if (changed) {
|
|
6759
|
+
return { status: "changed", task_dependents: 0, plan_dependents: 0 };
|
|
6760
|
+
}
|
|
6761
|
+
const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
|
|
6762
|
+
const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
|
|
6763
|
+
if (taskDependents > 0 || planDependents > 0) {
|
|
6764
|
+
return {
|
|
6765
|
+
status: "has_dependents",
|
|
6766
|
+
task_dependents: taskDependents,
|
|
6767
|
+
plan_dependents: planDependents
|
|
6768
|
+
};
|
|
6769
|
+
}
|
|
6770
|
+
recordStorageTombstone({
|
|
6771
|
+
object_type: "task_lists",
|
|
6772
|
+
object_id: id,
|
|
6773
|
+
payload: current
|
|
6774
|
+
}, d);
|
|
6775
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
6776
|
+
const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
6777
|
+
return {
|
|
6778
|
+
status: deleted ? "deleted" : "not_found",
|
|
6779
|
+
task_dependents: 0,
|
|
6780
|
+
plan_dependents: 0
|
|
6781
|
+
};
|
|
6782
|
+
})();
|
|
6783
|
+
}
|
|
6750
6784
|
function ensureTaskList(name, slug, projectId, db) {
|
|
6751
6785
|
const d = db || getDatabase();
|
|
6752
6786
|
const existing = getTaskListBySlug(slug, projectId, d);
|
|
@@ -12250,7 +12284,7 @@ var init_tasks = __esm(() => {
|
|
|
12250
12284
|
// package.json
|
|
12251
12285
|
var package_default = {
|
|
12252
12286
|
name: "@hasna/todos",
|
|
12253
|
-
version: "0.15.
|
|
12287
|
+
version: "0.15.9",
|
|
12254
12288
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12255
12289
|
type: "module",
|
|
12256
12290
|
main: "dist/index.js",
|
package/dist/db/task-lists.d.ts
CHANGED
|
@@ -6,5 +6,10 @@ export declare function getTaskListBySlug(slug: string, projectId?: string, db?:
|
|
|
6
6
|
export declare function listTaskLists(projectId?: string, db?: Database): TaskList[];
|
|
7
7
|
export declare function updateTaskList(id: string, input: UpdateTaskListInput, db?: Database): TaskList;
|
|
8
8
|
export declare function deleteTaskList(id: string, db?: Database): boolean;
|
|
9
|
+
export declare function deleteTaskListIfUnchangedAndUnused(id: string, expected: Pick<TaskList, "project_id" | "slug" | "name" | "description" | "metadata" | "updated_at">, db?: Database): {
|
|
10
|
+
status: "deleted" | "not_found" | "changed" | "has_dependents";
|
|
11
|
+
task_dependents: number;
|
|
12
|
+
plan_dependents: number;
|
|
13
|
+
};
|
|
9
14
|
export declare function ensureTaskList(name: string, slug: string, projectId?: string, db?: Database): TaskList;
|
|
10
15
|
//# sourceMappingURL=task-lists.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-lists.d.ts","sourceRoot":"","sources":["../../src/db/task-lists.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAe,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAezG,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAwBlF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAItE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CASlG;AAED,wBAAgB,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAM3E;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CA4C9F;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAajE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAKtG"}
|
|
1
|
+
{"version":3,"file":"task-lists.d.ts","sourceRoot":"","sources":["../../src/db/task-lists.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,QAAQ,EAAe,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAezG,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAwBlF;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAItE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CASlG;AAED,wBAAgB,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,EAAE,CAM3E;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CA4C9F;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAajE;AAED,wBAAgB,kCAAkC,CAChD,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,UAAU,GAAG,YAAY,CAAC,EACpG,EAAE,CAAC,EAAE,QAAQ,GACZ;IACD,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,gBAAgB,CAAC;IAC/D,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACzB,CA8CA;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAKtG"}
|
package/dist/index.js
CHANGED
|
@@ -6747,6 +6747,40 @@ function deleteTaskList(id, db) {
|
|
|
6747
6747
|
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
6748
6748
|
})();
|
|
6749
6749
|
}
|
|
6750
|
+
function deleteTaskListIfUnchangedAndUnused(id, expected, db) {
|
|
6751
|
+
const d = db || getDatabase();
|
|
6752
|
+
return d.transaction(() => {
|
|
6753
|
+
const current = getTaskList(id, d);
|
|
6754
|
+
if (!current) {
|
|
6755
|
+
return { status: "not_found", task_dependents: 0, plan_dependents: 0 };
|
|
6756
|
+
}
|
|
6757
|
+
const changed = current.project_id !== expected.project_id || current.slug !== expected.slug || current.name !== expected.name || current.description !== expected.description || current.updated_at !== expected.updated_at || JSON.stringify(current.metadata) !== JSON.stringify(expected.metadata);
|
|
6758
|
+
if (changed) {
|
|
6759
|
+
return { status: "changed", task_dependents: 0, plan_dependents: 0 };
|
|
6760
|
+
}
|
|
6761
|
+
const taskDependents = Number(d.query("SELECT COUNT(*) AS count FROM tasks WHERE task_list_id = ?").get(id).count);
|
|
6762
|
+
const planDependents = Number(d.query("SELECT COUNT(*) AS count FROM plans WHERE task_list_id = ?").get(id).count);
|
|
6763
|
+
if (taskDependents > 0 || planDependents > 0) {
|
|
6764
|
+
return {
|
|
6765
|
+
status: "has_dependents",
|
|
6766
|
+
task_dependents: taskDependents,
|
|
6767
|
+
plan_dependents: planDependents
|
|
6768
|
+
};
|
|
6769
|
+
}
|
|
6770
|
+
recordStorageTombstone({
|
|
6771
|
+
object_type: "task_lists",
|
|
6772
|
+
object_id: id,
|
|
6773
|
+
payload: current
|
|
6774
|
+
}, d);
|
|
6775
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
6776
|
+
const deleted = d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
6777
|
+
return {
|
|
6778
|
+
status: deleted ? "deleted" : "not_found",
|
|
6779
|
+
task_dependents: 0,
|
|
6780
|
+
plan_dependents: 0
|
|
6781
|
+
};
|
|
6782
|
+
})();
|
|
6783
|
+
}
|
|
6750
6784
|
function ensureTaskList(name, slug, projectId, db) {
|
|
6751
6785
|
const d = db || getDatabase();
|
|
6752
6786
|
const existing = getTaskListBySlug(slug, projectId, d);
|
|
@@ -12363,7 +12397,7 @@ var init_dispatches = __esm(() => {
|
|
|
12363
12397
|
// package.json
|
|
12364
12398
|
var package_default = {
|
|
12365
12399
|
name: "@hasna/todos",
|
|
12366
|
-
version: "0.15.
|
|
12400
|
+
version: "0.15.9",
|
|
12367
12401
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12368
12402
|
type: "module",
|
|
12369
12403
|
main: "dist/index.js",
|
|
@@ -25696,7 +25730,8 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
25696
25730
|
getBySlug: (slug, projectId) => getTaskListBySlug(slug, projectId, database()),
|
|
25697
25731
|
list: (projectId) => listTaskLists(projectId, database()),
|
|
25698
25732
|
update: (id, input) => updateTaskList(id, input, database()),
|
|
25699
|
-
delete: (id) => deleteTaskList(id, database())
|
|
25733
|
+
delete: (id) => deleteTaskList(id, database()),
|
|
25734
|
+
deleteIfUnchangedAndUnused: (id, expected) => deleteTaskListIfUnchangedAndUnused(id, expected, database())
|
|
25700
25735
|
},
|
|
25701
25736
|
templates: {
|
|
25702
25737
|
create: (input) => createTemplate(input, database()),
|
|
@@ -32511,6 +32546,8 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
32511
32546
|
}
|
|
32512
32547
|
|
|
32513
32548
|
// src/project-registration/sqlite.ts
|
|
32549
|
+
init_database();
|
|
32550
|
+
init_storage_tombstones();
|
|
32514
32551
|
var sqliteTransactionTails2 = new WeakMap;
|
|
32515
32552
|
var PROJECT_REFERENCE_COLUMNS = new Set([
|
|
32516
32553
|
"project_id",
|
|
@@ -32519,9 +32556,75 @@ var PROJECT_REFERENCE_COLUMNS = new Set([
|
|
|
32519
32556
|
"external_project_id"
|
|
32520
32557
|
]);
|
|
32521
32558
|
var TASK_LIST_REFERENCE_COLUMNS = new Set(["task_list_id"]);
|
|
32559
|
+
var SQLITE_TRANSACTION_RETRY_LIMIT = 8;
|
|
32560
|
+
|
|
32561
|
+
class SqliteRegistrationOptimisticConflict extends Error {
|
|
32562
|
+
constructor(message, options = {}) {
|
|
32563
|
+
super(message, options);
|
|
32564
|
+
this.name = "SqliteRegistrationOptimisticConflict";
|
|
32565
|
+
}
|
|
32566
|
+
}
|
|
32567
|
+
function sameSqliteValue(left, right) {
|
|
32568
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
32569
|
+
}
|
|
32570
|
+
function taskListFromRow(row) {
|
|
32571
|
+
return {
|
|
32572
|
+
...row,
|
|
32573
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
32574
|
+
};
|
|
32575
|
+
}
|
|
32576
|
+
function selectProject(db, id) {
|
|
32577
|
+
return db.query("SELECT * FROM projects WHERE id = ? LIMIT 1").get(id);
|
|
32578
|
+
}
|
|
32579
|
+
function selectTaskList(db, id) {
|
|
32580
|
+
const row = db.query("SELECT * FROM task_lists WHERE id = ? LIMIT 1").get(id);
|
|
32581
|
+
return row ? taskListFromRow(row) : null;
|
|
32582
|
+
}
|
|
32583
|
+
function selectProjectConflict(db, path, taskListSlug) {
|
|
32584
|
+
return db.query(`
|
|
32585
|
+
SELECT * FROM projects
|
|
32586
|
+
WHERE path = ? OR task_list_id = ?
|
|
32587
|
+
ORDER BY created_at ASC, id ASC
|
|
32588
|
+
LIMIT 1
|
|
32589
|
+
`).get(path, taskListSlug);
|
|
32590
|
+
}
|
|
32591
|
+
function selectTaskListConflict(db, projectId, slug) {
|
|
32592
|
+
const row = db.query(`
|
|
32593
|
+
SELECT * FROM task_lists
|
|
32594
|
+
WHERE project_id = ? AND slug = ?
|
|
32595
|
+
LIMIT 1
|
|
32596
|
+
`).get(projectId, slug);
|
|
32597
|
+
return row ? taskListFromRow(row) : null;
|
|
32598
|
+
}
|
|
32522
32599
|
function quoteSqliteIdentifier(value) {
|
|
32523
32600
|
return `"${value.replaceAll('"', '""')}"`;
|
|
32524
32601
|
}
|
|
32602
|
+
function hasSqliteDependents(db, resourceKind, targetId) {
|
|
32603
|
+
const targetTable = resourceKind === "project" ? "projects" : "task_lists";
|
|
32604
|
+
const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
|
|
32605
|
+
const tables = db.query(`
|
|
32606
|
+
SELECT name FROM sqlite_schema
|
|
32607
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
32608
|
+
ORDER BY name
|
|
32609
|
+
`).all();
|
|
32610
|
+
for (const { name: tableName } of tables) {
|
|
32611
|
+
const quotedTable = quoteSqliteIdentifier(tableName);
|
|
32612
|
+
const columns = db.query(`PRAGMA table_info(${quotedTable})`).all();
|
|
32613
|
+
const foreignKeys = db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
|
|
32614
|
+
const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
|
|
32615
|
+
for (const columnName of referenceColumns) {
|
|
32616
|
+
const row = db.query(`
|
|
32617
|
+
SELECT 1 AS found
|
|
32618
|
+
FROM ${quotedTable}
|
|
32619
|
+
WHERE ${quoteSqliteIdentifier(columnName)} = ?
|
|
32620
|
+
LIMIT 1
|
|
32621
|
+
`).get(targetId);
|
|
32622
|
+
if (row)
|
|
32623
|
+
return true;
|
|
32624
|
+
}
|
|
32625
|
+
}
|
|
32626
|
+
return false;
|
|
32627
|
+
}
|
|
32525
32628
|
function receiptFromRow2(row) {
|
|
32526
32629
|
return {
|
|
32527
32630
|
...row,
|
|
@@ -32664,30 +32767,7 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
32664
32767
|
}
|
|
32665
32768
|
async lockCompensationWrites() {}
|
|
32666
32769
|
async hasDependents(resourceKind, targetId) {
|
|
32667
|
-
|
|
32668
|
-
const semanticColumns = resourceKind === "project" ? PROJECT_REFERENCE_COLUMNS : TASK_LIST_REFERENCE_COLUMNS;
|
|
32669
|
-
const tables = this.db.query(`
|
|
32670
|
-
SELECT name FROM sqlite_schema
|
|
32671
|
-
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
32672
|
-
ORDER BY name
|
|
32673
|
-
`).all();
|
|
32674
|
-
for (const { name: tableName } of tables) {
|
|
32675
|
-
const quotedTable = quoteSqliteIdentifier(tableName);
|
|
32676
|
-
const columns = this.db.query(`PRAGMA table_info(${quotedTable})`).all();
|
|
32677
|
-
const foreignKeys = this.db.query(`PRAGMA foreign_key_list(${quotedTable})`).all();
|
|
32678
|
-
const referenceColumns = columns.map((column) => column.name).filter((columnName) => semanticColumns.has(columnName) || foreignKeys.some((foreignKey) => foreignKey.from === columnName && foreignKey.table === targetTable));
|
|
32679
|
-
for (const columnName of referenceColumns) {
|
|
32680
|
-
const row = this.db.query(`
|
|
32681
|
-
SELECT 1 AS found
|
|
32682
|
-
FROM ${quotedTable}
|
|
32683
|
-
WHERE ${quoteSqliteIdentifier(columnName)} = ?
|
|
32684
|
-
LIMIT 1
|
|
32685
|
-
`).get(targetId);
|
|
32686
|
-
if (row)
|
|
32687
|
-
return true;
|
|
32688
|
-
}
|
|
32689
|
-
}
|
|
32690
|
-
return false;
|
|
32770
|
+
return hasSqliteDependents(this.db, resourceKind, targetId);
|
|
32691
32771
|
}
|
|
32692
32772
|
async deleteProject(id) {
|
|
32693
32773
|
return await this.storage.projects.delete(id);
|
|
@@ -32697,6 +32777,376 @@ class SqliteTodosProjectRegistrationTransaction {
|
|
|
32697
32777
|
}
|
|
32698
32778
|
}
|
|
32699
32779
|
|
|
32780
|
+
class StagedSqliteTodosProjectRegistrationTransaction {
|
|
32781
|
+
db;
|
|
32782
|
+
direct;
|
|
32783
|
+
validators = [];
|
|
32784
|
+
mutations = [];
|
|
32785
|
+
receipts = new Map;
|
|
32786
|
+
bindings = new Map;
|
|
32787
|
+
projects = new Map;
|
|
32788
|
+
taskLists = new Map;
|
|
32789
|
+
constructor(db) {
|
|
32790
|
+
this.db = db;
|
|
32791
|
+
this.direct = new SqliteTodosProjectRegistrationTransaction(db);
|
|
32792
|
+
}
|
|
32793
|
+
commit() {
|
|
32794
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
32795
|
+
try {
|
|
32796
|
+
for (const validate of this.validators) {
|
|
32797
|
+
if (!validate()) {
|
|
32798
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration input changed before SQLite commit");
|
|
32799
|
+
}
|
|
32800
|
+
}
|
|
32801
|
+
for (const mutate of this.mutations)
|
|
32802
|
+
mutate();
|
|
32803
|
+
this.db.exec("COMMIT");
|
|
32804
|
+
} catch (error) {
|
|
32805
|
+
try {
|
|
32806
|
+
this.db.exec("ROLLBACK");
|
|
32807
|
+
} catch {}
|
|
32808
|
+
throw error;
|
|
32809
|
+
}
|
|
32810
|
+
}
|
|
32811
|
+
async lockStep(_identity) {}
|
|
32812
|
+
async getReceiptForLookup(identity) {
|
|
32813
|
+
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
|
|
32814
|
+
const stored = await this.direct.getReceiptForLookup(identity);
|
|
32815
|
+
if (stored)
|
|
32816
|
+
staged.push(stored);
|
|
32817
|
+
const outcomeRank = (receipt) => receipt.outcome === "terminal_nonacceptance" ? 0 : receipt.outcome === "duplicate_of_accepted" ? 1 : 2;
|
|
32818
|
+
return staged.sort((left, right) => outcomeRank(left) - outcomeRank(right) || right.created_at.localeCompare(left.created_at) || right.receipt_id.localeCompare(left.receipt_id))[0] ?? null;
|
|
32819
|
+
}
|
|
32820
|
+
async getReceiptById(receiptId) {
|
|
32821
|
+
return this.receipts.get(receiptId) ?? this.direct.getReceiptById(receiptId);
|
|
32822
|
+
}
|
|
32823
|
+
async getAcceptedReceiptForStep(identity) {
|
|
32824
|
+
const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.outcome === "accepted");
|
|
32825
|
+
const stored = await this.direct.getAcceptedReceiptForStep(identity);
|
|
32826
|
+
if (stored)
|
|
32827
|
+
staged.push(stored);
|
|
32828
|
+
return staged.sort((left, right) => left.created_at.localeCompare(right.created_at) || left.receipt_id.localeCompare(right.receipt_id))[0] ?? null;
|
|
32829
|
+
}
|
|
32830
|
+
async insertReceipt(receipt) {
|
|
32831
|
+
if (this.receipts.has(receipt.receipt_id))
|
|
32832
|
+
return false;
|
|
32833
|
+
if (await this.direct.getReceiptById(receipt.receipt_id))
|
|
32834
|
+
return false;
|
|
32835
|
+
const planned = { ...receipt };
|
|
32836
|
+
this.receipts.set(planned.receipt_id, planned);
|
|
32837
|
+
this.mutations.push(() => {
|
|
32838
|
+
try {
|
|
32839
|
+
const result = this.db.query(`
|
|
32840
|
+
INSERT OR IGNORE INTO todos_project_registration_receipts (
|
|
32841
|
+
receipt_id, authority, route, package_version, authority_id, tenant_id,
|
|
32842
|
+
corpus_id, operation_id, step_id, resource_kind, direction,
|
|
32843
|
+
target_selector, idempotency_key, request_digest, precondition_digest,
|
|
32844
|
+
normalized_call_digest, outcome, reason, target_id, result_revision,
|
|
32845
|
+
result_digest, duplicate_of_receipt_id, accepted_receipt_id,
|
|
32846
|
+
created_by_operation, created_at
|
|
32847
|
+
) VALUES (
|
|
32848
|
+
?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?
|
|
32849
|
+
)
|
|
32850
|
+
`).run(planned.receipt_id, planned.authority, planned.route, planned.package_version, planned.authority_id, planned.tenant_id, planned.corpus_id, planned.operation_id, planned.step_id, planned.resource_kind, planned.direction, planned.target_selector, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.outcome, planned.reason, planned.target_id, planned.result_revision, planned.result_digest, planned.duplicate_of_receipt_id, planned.accepted_receipt_id, planned.created_by_operation ? 1 : 0, planned.created_at);
|
|
32851
|
+
if (result.changes !== 1) {
|
|
32852
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt changed before SQLite commit");
|
|
32853
|
+
}
|
|
32854
|
+
} catch (error) {
|
|
32855
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
32856
|
+
throw error;
|
|
32857
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration receipt conflicted at SQLite commit", { cause: error });
|
|
32858
|
+
}
|
|
32859
|
+
});
|
|
32860
|
+
return true;
|
|
32861
|
+
}
|
|
32862
|
+
async getBinding(scope, resourceKind, targetSelector) {
|
|
32863
|
+
const key = this.bindingKey(scope, resourceKind, targetSelector);
|
|
32864
|
+
return this.bindings.get(key) ?? this.direct.getBinding(scope, resourceKind, targetSelector);
|
|
32865
|
+
}
|
|
32866
|
+
async claimBinding(binding) {
|
|
32867
|
+
const key = this.bindingKey(binding, binding.resource_kind, binding.target_selector);
|
|
32868
|
+
if (this.bindings.has(key))
|
|
32869
|
+
return false;
|
|
32870
|
+
if (await this.direct.getBinding(binding, binding.resource_kind, binding.target_selector)) {
|
|
32871
|
+
return false;
|
|
32872
|
+
}
|
|
32873
|
+
const planned = { ...binding };
|
|
32874
|
+
this.bindings.set(key, planned);
|
|
32875
|
+
this.mutations.push(() => {
|
|
32876
|
+
try {
|
|
32877
|
+
const result = this.db.query(`
|
|
32878
|
+
INSERT OR IGNORE INTO todos_project_registration_bindings (
|
|
32879
|
+
authority_id, tenant_id, corpus_id, resource_kind, target_selector,
|
|
32880
|
+
operation_id, step_id, direction, idempotency_key, request_digest,
|
|
32881
|
+
precondition_digest, normalized_call_digest, state, target_id,
|
|
32882
|
+
accepted_receipt_id, result_revision, result_digest, removed_receipt_id,
|
|
32883
|
+
created_at, updated_at
|
|
32884
|
+
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
32885
|
+
`).run(planned.authority_id, planned.tenant_id, planned.corpus_id, planned.resource_kind, planned.target_selector, planned.operation_id, planned.step_id, planned.direction, planned.idempotency_key, planned.request_digest, planned.precondition_digest, planned.normalized_call_digest, planned.state, planned.target_id, planned.accepted_receipt_id, planned.result_revision, planned.result_digest, planned.removed_receipt_id, planned.created_at, planned.updated_at);
|
|
32886
|
+
if (result.changes !== 1) {
|
|
32887
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding changed before SQLite commit");
|
|
32888
|
+
}
|
|
32889
|
+
} catch (error) {
|
|
32890
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
32891
|
+
throw error;
|
|
32892
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding conflicted at SQLite commit", { cause: error });
|
|
32893
|
+
}
|
|
32894
|
+
});
|
|
32895
|
+
return true;
|
|
32896
|
+
}
|
|
32897
|
+
async setBindingAccepted(scope, resourceKind, targetSelector, update) {
|
|
32898
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
32899
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
32900
|
+
...binding,
|
|
32901
|
+
state: "accepted",
|
|
32902
|
+
target_id: update.target_id,
|
|
32903
|
+
accepted_receipt_id: update.accepted_receipt_id,
|
|
32904
|
+
result_revision: update.result_revision,
|
|
32905
|
+
result_digest: update.result_digest,
|
|
32906
|
+
updated_at: update.updated_at
|
|
32907
|
+
});
|
|
32908
|
+
this.mutations.push(() => {
|
|
32909
|
+
const result = this.db.query(`
|
|
32910
|
+
UPDATE todos_project_registration_bindings
|
|
32911
|
+
SET state = 'accepted', target_id = ?, accepted_receipt_id = ?,
|
|
32912
|
+
result_revision = ?, result_digest = ?, updated_at = ?
|
|
32913
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32914
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
32915
|
+
`).run(update.target_id, update.accepted_receipt_id, update.result_revision, update.result_digest, update.updated_at, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
32916
|
+
if (result.changes !== 1) {
|
|
32917
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
32918
|
+
}
|
|
32919
|
+
});
|
|
32920
|
+
}
|
|
32921
|
+
async setBindingTerminal(scope, resourceKind, targetSelector, updatedAt) {
|
|
32922
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "pending");
|
|
32923
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
32924
|
+
...binding,
|
|
32925
|
+
state: "terminal_nonacceptance",
|
|
32926
|
+
updated_at: updatedAt
|
|
32927
|
+
});
|
|
32928
|
+
this.mutations.push(() => {
|
|
32929
|
+
const result = this.db.query(`
|
|
32930
|
+
UPDATE todos_project_registration_bindings
|
|
32931
|
+
SET state = 'terminal_nonacceptance', updated_at = ?
|
|
32932
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32933
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'pending'
|
|
32934
|
+
`).run(updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
32935
|
+
if (result.changes !== 1) {
|
|
32936
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer pending at SQLite commit");
|
|
32937
|
+
}
|
|
32938
|
+
});
|
|
32939
|
+
}
|
|
32940
|
+
async setBindingRemoved(scope, resourceKind, targetSelector, removedReceiptId, updatedAt) {
|
|
32941
|
+
const binding = await this.requireBinding(scope, resourceKind, targetSelector, "accepted");
|
|
32942
|
+
this.bindings.set(this.bindingKey(scope, resourceKind, targetSelector), {
|
|
32943
|
+
...binding,
|
|
32944
|
+
state: "removed",
|
|
32945
|
+
removed_receipt_id: removedReceiptId,
|
|
32946
|
+
updated_at: updatedAt
|
|
32947
|
+
});
|
|
32948
|
+
this.mutations.push(() => {
|
|
32949
|
+
const result = this.db.query(`
|
|
32950
|
+
UPDATE todos_project_registration_bindings
|
|
32951
|
+
SET state = 'removed', removed_receipt_id = ?, updated_at = ?
|
|
32952
|
+
WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
|
|
32953
|
+
AND resource_kind = ? AND target_selector = ? AND state = 'accepted'
|
|
32954
|
+
`).run(removedReceiptId, updatedAt, scope.authority_id, scope.tenant_id, scope.corpus_id, resourceKind, targetSelector);
|
|
32955
|
+
if (result.changes !== 1) {
|
|
32956
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration binding was no longer accepted at SQLite commit");
|
|
32957
|
+
}
|
|
32958
|
+
});
|
|
32959
|
+
}
|
|
32960
|
+
async findProjectConflict(path, taskListSlug) {
|
|
32961
|
+
const planned = [...this.projects.values()].find((project) => project?.path === path || project?.task_list_id === taskListSlug);
|
|
32962
|
+
if (planned)
|
|
32963
|
+
return planned;
|
|
32964
|
+
const observed = selectProjectConflict(this.db, path, taskListSlug);
|
|
32965
|
+
this.validators.push(() => sameSqliteValue(selectProjectConflict(this.db, path, taskListSlug), observed));
|
|
32966
|
+
return observed;
|
|
32967
|
+
}
|
|
32968
|
+
async findTaskListConflict(projectId, slug) {
|
|
32969
|
+
const planned = [...this.taskLists.values()].find((taskList) => taskList?.project_id === projectId && taskList.slug === slug);
|
|
32970
|
+
if (planned)
|
|
32971
|
+
return planned;
|
|
32972
|
+
const observed = selectTaskListConflict(this.db, projectId, slug);
|
|
32973
|
+
this.validators.push(() => sameSqliteValue(selectTaskListConflict(this.db, projectId, slug), observed));
|
|
32974
|
+
return observed;
|
|
32975
|
+
}
|
|
32976
|
+
async createProject(input) {
|
|
32977
|
+
const derivedSlug = normalizeSlug(input.name);
|
|
32978
|
+
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : normalizeSlug(input.task_list_id);
|
|
32979
|
+
if (!derivedSlug || !taskListId) {
|
|
32980
|
+
throw new Error("Project name and task-list slug must be non-empty");
|
|
32981
|
+
}
|
|
32982
|
+
const project = {
|
|
32983
|
+
id: uuid(),
|
|
32984
|
+
name: input.name,
|
|
32985
|
+
path: input.path,
|
|
32986
|
+
description: input.description || null,
|
|
32987
|
+
task_list_id: taskListId,
|
|
32988
|
+
task_prefix: input.task_prefix ?? this.availableProjectPrefix(input.name),
|
|
32989
|
+
task_counter: 0,
|
|
32990
|
+
created_at: now(),
|
|
32991
|
+
updated_at: now(),
|
|
32992
|
+
machine_id: currentStorageMachineId(this.db)
|
|
32993
|
+
};
|
|
32994
|
+
project.updated_at = project.created_at;
|
|
32995
|
+
this.projects.set(project.id, project);
|
|
32996
|
+
this.mutations.push(() => {
|
|
32997
|
+
try {
|
|
32998
|
+
const result = this.db.run(`INSERT INTO projects (
|
|
32999
|
+
id, name, path, description, task_list_id, task_prefix,
|
|
33000
|
+
task_counter, created_at, updated_at, machine_id
|
|
33001
|
+
) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [
|
|
33002
|
+
project.id,
|
|
33003
|
+
project.name,
|
|
33004
|
+
project.path,
|
|
33005
|
+
project.description,
|
|
33006
|
+
project.task_list_id,
|
|
33007
|
+
project.task_prefix,
|
|
33008
|
+
project.created_at,
|
|
33009
|
+
project.updated_at,
|
|
33010
|
+
project.machine_id ?? null
|
|
33011
|
+
]);
|
|
33012
|
+
if (result.changes < 1) {
|
|
33013
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite registration commit");
|
|
33014
|
+
}
|
|
33015
|
+
} catch (error) {
|
|
33016
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
33017
|
+
throw error;
|
|
33018
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project conflicted at SQLite registration commit", { cause: error });
|
|
33019
|
+
}
|
|
33020
|
+
});
|
|
33021
|
+
return project;
|
|
33022
|
+
}
|
|
33023
|
+
async createTaskList(input) {
|
|
33024
|
+
const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
|
|
33025
|
+
if (!slug)
|
|
33026
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
33027
|
+
const taskList = {
|
|
33028
|
+
id: uuid(),
|
|
33029
|
+
project_id: input.project_id || null,
|
|
33030
|
+
slug,
|
|
33031
|
+
name: input.name,
|
|
33032
|
+
description: input.description || null,
|
|
33033
|
+
metadata: input.metadata ?? {},
|
|
33034
|
+
created_at: now(),
|
|
33035
|
+
updated_at: now(),
|
|
33036
|
+
machine_id: currentStorageMachineId(this.db)
|
|
33037
|
+
};
|
|
33038
|
+
taskList.updated_at = taskList.created_at;
|
|
33039
|
+
this.taskLists.set(taskList.id, taskList);
|
|
33040
|
+
this.mutations.push(() => {
|
|
33041
|
+
try {
|
|
33042
|
+
const result = this.db.run(`INSERT INTO task_lists (
|
|
33043
|
+
id, project_id, slug, name, description, metadata,
|
|
33044
|
+
created_at, updated_at, machine_id
|
|
33045
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
33046
|
+
taskList.id,
|
|
33047
|
+
taskList.project_id,
|
|
33048
|
+
taskList.slug,
|
|
33049
|
+
taskList.name,
|
|
33050
|
+
taskList.description,
|
|
33051
|
+
JSON.stringify(taskList.metadata),
|
|
33052
|
+
taskList.created_at,
|
|
33053
|
+
taskList.updated_at,
|
|
33054
|
+
taskList.machine_id ?? null
|
|
33055
|
+
]);
|
|
33056
|
+
if (result.changes < 1) {
|
|
33057
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite registration commit");
|
|
33058
|
+
}
|
|
33059
|
+
} catch (error) {
|
|
33060
|
+
if (error instanceof SqliteRegistrationOptimisticConflict)
|
|
33061
|
+
throw error;
|
|
33062
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list conflicted at SQLite registration commit", { cause: error });
|
|
33063
|
+
}
|
|
33064
|
+
});
|
|
33065
|
+
return taskList;
|
|
33066
|
+
}
|
|
33067
|
+
async getProject(id) {
|
|
33068
|
+
if (this.projects.has(id))
|
|
33069
|
+
return this.projects.get(id) ?? null;
|
|
33070
|
+
const observed = selectProject(this.db, id);
|
|
33071
|
+
this.validators.push(() => sameSqliteValue(selectProject(this.db, id), observed));
|
|
33072
|
+
return observed;
|
|
33073
|
+
}
|
|
33074
|
+
async getTaskList(id) {
|
|
33075
|
+
if (this.taskLists.has(id))
|
|
33076
|
+
return this.taskLists.get(id) ?? null;
|
|
33077
|
+
const observed = selectTaskList(this.db, id);
|
|
33078
|
+
this.validators.push(() => sameSqliteValue(selectTaskList(this.db, id), observed));
|
|
33079
|
+
return observed;
|
|
33080
|
+
}
|
|
33081
|
+
async lockCompensationWrites() {}
|
|
33082
|
+
async hasDependents(resourceKind, targetId) {
|
|
33083
|
+
const observed = hasSqliteDependents(this.db, resourceKind, targetId);
|
|
33084
|
+
this.validators.push(() => hasSqliteDependents(this.db, resourceKind, targetId) === observed);
|
|
33085
|
+
return observed;
|
|
33086
|
+
}
|
|
33087
|
+
async deleteProject(id) {
|
|
33088
|
+
const project = await this.getProject(id);
|
|
33089
|
+
if (!project)
|
|
33090
|
+
return false;
|
|
33091
|
+
this.projects.set(id, null);
|
|
33092
|
+
this.mutations.push(() => {
|
|
33093
|
+
recordStorageTombstone({
|
|
33094
|
+
object_type: "projects",
|
|
33095
|
+
object_id: id,
|
|
33096
|
+
payload: project
|
|
33097
|
+
}, this.db);
|
|
33098
|
+
if (this.db.run("DELETE FROM projects WHERE id = ?", [id]).changes < 1) {
|
|
33099
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project changed before SQLite compensation commit");
|
|
33100
|
+
}
|
|
33101
|
+
});
|
|
33102
|
+
return true;
|
|
33103
|
+
}
|
|
33104
|
+
async deleteTaskList(id) {
|
|
33105
|
+
const taskList = await this.getTaskList(id);
|
|
33106
|
+
if (!taskList)
|
|
33107
|
+
return false;
|
|
33108
|
+
this.taskLists.set(id, null);
|
|
33109
|
+
this.mutations.push(() => {
|
|
33110
|
+
recordStorageTombstone({
|
|
33111
|
+
object_type: "task_lists",
|
|
33112
|
+
object_id: id,
|
|
33113
|
+
payload: taskList
|
|
33114
|
+
}, this.db);
|
|
33115
|
+
if (this.db.run("DELETE FROM task_lists WHERE id = ?", [id]).changes < 1) {
|
|
33116
|
+
throw new SqliteRegistrationOptimisticConflict("Todos task list changed before SQLite compensation commit");
|
|
33117
|
+
}
|
|
33118
|
+
});
|
|
33119
|
+
return true;
|
|
33120
|
+
}
|
|
33121
|
+
bindingKey(scope, resourceKind, targetSelector) {
|
|
33122
|
+
return JSON.stringify([
|
|
33123
|
+
scope.authority_id,
|
|
33124
|
+
scope.tenant_id,
|
|
33125
|
+
scope.corpus_id,
|
|
33126
|
+
resourceKind,
|
|
33127
|
+
targetSelector
|
|
33128
|
+
]);
|
|
33129
|
+
}
|
|
33130
|
+
async requireBinding(scope, resourceKind, targetSelector, state) {
|
|
33131
|
+
const binding = await this.getBinding(scope, resourceKind, targetSelector);
|
|
33132
|
+
if (!binding || binding.state !== state) {
|
|
33133
|
+
throw new Error(`Todos project registration binding was not ${state}`);
|
|
33134
|
+
}
|
|
33135
|
+
return binding;
|
|
33136
|
+
}
|
|
33137
|
+
availableProjectPrefix(name) {
|
|
33138
|
+
const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
33139
|
+
const prefix = words.length >= 3 ? words.slice(0, 3).map((word) => word[0].toUpperCase()).join("") : words.length === 2 ? (words[0].slice(0, 2) + words[1][0]).toUpperCase() : words[0].slice(0, 3).toUpperCase();
|
|
33140
|
+
let candidate = prefix;
|
|
33141
|
+
let suffix = 1;
|
|
33142
|
+
while (this.db.query("SELECT id FROM projects WHERE task_prefix = ? LIMIT 1").get(candidate) || [...this.projects.values()].some((project) => project?.task_prefix === candidate)) {
|
|
33143
|
+
suffix += 1;
|
|
33144
|
+
candidate = `${prefix}${suffix}`;
|
|
33145
|
+
}
|
|
33146
|
+
return candidate;
|
|
33147
|
+
}
|
|
33148
|
+
}
|
|
33149
|
+
|
|
32700
33150
|
class SqliteTodosProjectRegistrationBackend {
|
|
32701
33151
|
db;
|
|
32702
33152
|
kind = "sqlite";
|
|
@@ -32715,15 +33165,19 @@ class SqliteTodosProjectRegistrationBackend {
|
|
|
32715
33165
|
sqliteTransactionTails2.set(this.db, current);
|
|
32716
33166
|
await previous;
|
|
32717
33167
|
try {
|
|
32718
|
-
|
|
32719
|
-
|
|
32720
|
-
|
|
32721
|
-
|
|
32722
|
-
|
|
32723
|
-
|
|
32724
|
-
|
|
32725
|
-
|
|
32726
|
-
|
|
33168
|
+
for (let attempt = 0;attempt < SQLITE_TRANSACTION_RETRY_LIMIT; attempt += 1) {
|
|
33169
|
+
const transaction = new StagedSqliteTodosProjectRegistrationTransaction(this.db);
|
|
33170
|
+
const result = await fn(transaction);
|
|
33171
|
+
try {
|
|
33172
|
+
transaction.commit();
|
|
33173
|
+
return result;
|
|
33174
|
+
} catch (error) {
|
|
33175
|
+
if (!(error instanceof SqliteRegistrationOptimisticConflict) || attempt === SQLITE_TRANSACTION_RETRY_LIMIT - 1) {
|
|
33176
|
+
throw error;
|
|
33177
|
+
}
|
|
33178
|
+
}
|
|
33179
|
+
}
|
|
33180
|
+
throw new SqliteRegistrationOptimisticConflict("Todos project registration exhausted SQLite optimistic retries");
|
|
32727
33181
|
} finally {
|
|
32728
33182
|
release();
|
|
32729
33183
|
if (sqliteTransactionTails2.get(this.db) === current) {
|