hzl-core 1.6.0 → 1.7.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/__tests__/backup/backup-restore.test.js +13 -12
- package/dist/__tests__/backup/backup-restore.test.js.map +1 -1
- package/dist/__tests__/backup/import-export.test.js +41 -39
- package/dist/__tests__/backup/import-export.test.js.map +1 -1
- package/dist/__tests__/concurrency/stress.test.js +2 -2
- package/dist/__tests__/concurrency/stress.test.js.map +1 -1
- package/dist/__tests__/concurrency/worker.js +3 -2
- package/dist/__tests__/concurrency/worker.js.map +1 -1
- package/dist/__tests__/migrations/upgrade.test.js +132 -117
- package/dist/__tests__/migrations/upgrade.test.js.map +1 -1
- package/dist/__tests__/projections/rebuild-equivalence.test.js +2 -2
- package/dist/__tests__/projections/rebuild-equivalence.test.js.map +1 -1
- package/dist/__tests__/properties/invariants.test.js +2 -2
- package/dist/__tests__/properties/invariants.test.js.map +1 -1
- package/dist/db/__tests__/append-only.test.d.ts +2 -0
- package/dist/db/__tests__/append-only.test.d.ts.map +1 -0
- package/dist/db/__tests__/append-only.test.js +46 -0
- package/dist/db/__tests__/append-only.test.js.map +1 -0
- package/dist/db/__tests__/datastore.test.d.ts +2 -0
- package/dist/db/__tests__/datastore.test.d.ts.map +1 -0
- package/dist/db/__tests__/datastore.test.js +68 -0
- package/dist/db/__tests__/datastore.test.js.map +1 -0
- package/dist/db/__tests__/libsql-compat.test.d.ts +2 -0
- package/dist/db/__tests__/libsql-compat.test.d.ts.map +1 -0
- package/dist/db/__tests__/libsql-compat.test.js +56 -0
- package/dist/db/__tests__/libsql-compat.test.js.map +1 -0
- package/dist/db/__tests__/lock.test.d.ts +2 -0
- package/dist/db/__tests__/lock.test.d.ts.map +1 -0
- package/dist/db/__tests__/lock.test.js +77 -0
- package/dist/db/__tests__/lock.test.js.map +1 -0
- package/dist/db/__tests__/meta.test.d.ts +2 -0
- package/dist/db/__tests__/meta.test.d.ts.map +1 -0
- package/dist/db/__tests__/meta.test.js +50 -0
- package/dist/db/__tests__/meta.test.js.map +1 -0
- package/dist/db/__tests__/migrations.test.d.ts +2 -0
- package/dist/db/__tests__/migrations.test.d.ts.map +1 -0
- package/dist/db/__tests__/migrations.test.js +57 -0
- package/dist/db/__tests__/migrations.test.js.map +1 -0
- package/dist/db/__tests__/sync-policy.test.d.ts +2 -0
- package/dist/db/__tests__/sync-policy.test.d.ts.map +1 -0
- package/dist/db/__tests__/sync-policy.test.js +118 -0
- package/dist/db/__tests__/sync-policy.test.js.map +1 -0
- package/dist/db/__tests__/types.test.d.ts +2 -0
- package/dist/db/__tests__/types.test.d.ts.map +1 -0
- package/dist/db/__tests__/types.test.js +91 -0
- package/dist/db/__tests__/types.test.js.map +1 -0
- package/dist/db/datastore.d.ts +16 -0
- package/dist/db/datastore.d.ts.map +1 -0
- package/dist/db/datastore.js +159 -0
- package/dist/db/datastore.js.map +1 -0
- package/dist/db/lock.d.ts +38 -0
- package/dist/db/lock.d.ts.map +1 -0
- package/dist/db/lock.js +114 -0
- package/dist/db/lock.js.map +1 -0
- package/dist/db/meta.d.ts +28 -0
- package/dist/db/meta.d.ts.map +1 -0
- package/dist/db/meta.js +95 -0
- package/dist/db/meta.js.map +1 -0
- package/dist/db/migrations.d.ts +23 -3
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +69 -99
- package/dist/db/migrations.js.map +1 -1
- package/dist/db/migrations.test.js +40 -105
- package/dist/db/migrations.test.js.map +1 -1
- package/dist/db/schema.d.ts +2 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +52 -10
- package/dist/db/schema.js.map +1 -1
- package/dist/db/sync-policy.d.ts +21 -0
- package/dist/db/sync-policy.d.ts.map +1 -0
- package/dist/db/sync-policy.js +62 -0
- package/dist/db/sync-policy.js.map +1 -0
- package/dist/db/test-utils.d.ts +33 -0
- package/dist/db/test-utils.d.ts.map +1 -0
- package/dist/db/test-utils.js +58 -0
- package/dist/db/test-utils.js.map +1 -0
- package/dist/db/{connection.d.ts → transaction.d.ts} +2 -4
- package/dist/db/transaction.d.ts.map +1 -0
- package/dist/db/{connection.js → transaction.js} +1 -23
- package/dist/db/transaction.js.map +1 -0
- package/dist/db/types.d.ts +199 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +43 -0
- package/dist/db/types.js.map +1 -0
- package/dist/events/store.d.ts +1 -1
- package/dist/events/store.d.ts.map +1 -1
- package/dist/events/store.test.js +2 -4
- package/dist/events/store.test.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +13 -11
- package/dist/index.test.js.map +1 -1
- package/dist/projections/comments-checkpoints.d.ts +1 -1
- package/dist/projections/comments-checkpoints.d.ts.map +1 -1
- package/dist/projections/comments-checkpoints.test.js +3 -4
- package/dist/projections/comments-checkpoints.test.js.map +1 -1
- package/dist/projections/dependencies.d.ts +1 -1
- package/dist/projections/dependencies.d.ts.map +1 -1
- package/dist/projections/dependencies.test.js +3 -4
- package/dist/projections/dependencies.test.js.map +1 -1
- package/dist/projections/engine.d.ts +9 -2
- package/dist/projections/engine.d.ts.map +1 -1
- package/dist/projections/engine.js +22 -6
- package/dist/projections/engine.js.map +1 -1
- package/dist/projections/engine.test.js +3 -4
- package/dist/projections/engine.test.js.map +1 -1
- package/dist/projections/projects.d.ts +1 -1
- package/dist/projections/projects.d.ts.map +1 -1
- package/dist/projections/projects.test.js +2 -20
- package/dist/projections/projects.test.js.map +1 -1
- package/dist/projections/rebuild.d.ts +1 -1
- package/dist/projections/rebuild.d.ts.map +1 -1
- package/dist/projections/rebuild.test.js +3 -4
- package/dist/projections/rebuild.test.js.map +1 -1
- package/dist/projections/search.d.ts +1 -1
- package/dist/projections/search.d.ts.map +1 -1
- package/dist/projections/search.test.js +3 -4
- package/dist/projections/search.test.js.map +1 -1
- package/dist/projections/tags.d.ts +1 -1
- package/dist/projections/tags.d.ts.map +1 -1
- package/dist/projections/tags.test.js +3 -4
- package/dist/projections/tags.test.js.map +1 -1
- package/dist/projections/tasks-current.d.ts +1 -1
- package/dist/projections/tasks-current.d.ts.map +1 -1
- package/dist/projections/tasks-current.test.js +3 -4
- package/dist/projections/tasks-current.test.js.map +1 -1
- package/dist/projections/types.d.ts +1 -1
- package/dist/projections/types.d.ts.map +1 -1
- package/dist/services/backup-service.d.ts +2 -2
- package/dist/services/backup-service.d.ts.map +1 -1
- package/dist/services/backup-service.js +13 -3
- package/dist/services/backup-service.js.map +1 -1
- package/dist/services/project-service.d.ts +1 -1
- package/dist/services/project-service.d.ts.map +1 -1
- package/dist/services/project-service.js +1 -1
- package/dist/services/project-service.js.map +1 -1
- package/dist/services/project-service.test.js +2 -13
- package/dist/services/project-service.test.js.map +1 -1
- package/dist/services/search-service.d.ts +1 -1
- package/dist/services/search-service.d.ts.map +1 -1
- package/dist/services/search-service.test.js +3 -4
- package/dist/services/search-service.test.js.map +1 -1
- package/dist/services/task-service.d.ts +1 -1
- package/dist/services/task-service.d.ts.map +1 -1
- package/dist/services/task-service.js +1 -1
- package/dist/services/task-service.js.map +1 -1
- package/dist/services/task-service.test.js +3 -4
- package/dist/services/task-service.test.js.map +1 -1
- package/dist/services/validation-service.d.ts +1 -1
- package/dist/services/validation-service.d.ts.map +1 -1
- package/dist/services/validation-service.test.js +2 -4
- package/dist/services/validation-service.test.js.map +1 -1
- package/package.json +4 -4
- package/dist/db/connection.d.ts.map +0 -1
- package/dist/db/connection.js.map +0 -1
- package/dist/db/connection.test.d.ts +0 -2
- package/dist/db/connection.test.d.ts.map +0 -1
- package/dist/db/connection.test.js +0 -63
- package/dist/db/connection.test.js.map +0 -1
package/dist/db/migrations.js
CHANGED
|
@@ -1,113 +1,83 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
.prepare('SELECT version FROM schema_migrations')
|
|
11
|
-
.all();
|
|
12
|
-
let maxVersion = 0;
|
|
13
|
-
for (const row of rows) {
|
|
14
|
-
const parsed = Number(row.version);
|
|
15
|
-
if (Number.isFinite(parsed) && parsed > maxVersion) {
|
|
16
|
-
maxVersion = parsed;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return maxVersion;
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return 0;
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
export class MigrationError extends Error {
|
|
3
|
+
failedMigration;
|
|
4
|
+
rolledBack;
|
|
5
|
+
constructor(message, failedMigration, rolledBack) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.failedMigration = failedMigration;
|
|
8
|
+
this.rolledBack = rolledBack;
|
|
9
|
+
this.name = 'MigrationError';
|
|
23
10
|
}
|
|
24
11
|
}
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
function computeChecksum(sql) {
|
|
13
|
+
return crypto.createHash('sha256').update(sql).digest('hex').slice(0, 16);
|
|
14
|
+
}
|
|
15
|
+
function ensureMigrationsTable(db) {
|
|
27
16
|
db.exec(`
|
|
28
17
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
migration_id TEXT PRIMARY KEY,
|
|
19
|
+
applied_at_ms INTEGER NOT NULL,
|
|
20
|
+
checksum TEXT NOT NULL
|
|
31
21
|
)
|
|
32
22
|
`);
|
|
33
|
-
const currentVersion = getCurrentVersion(db);
|
|
34
|
-
const versions = Object.keys(MIGRATIONS)
|
|
35
|
-
.map(Number)
|
|
36
|
-
.sort((a, b) => a - b);
|
|
37
|
-
for (const version of versions) {
|
|
38
|
-
if (version > currentVersion) {
|
|
39
|
-
db.transaction(() => {
|
|
40
|
-
db.exec(MIGRATIONS[version]);
|
|
41
|
-
db.prepare('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)').run(version, new Date().toISOString());
|
|
42
|
-
})();
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
migrateToProjectsTable(db);
|
|
46
|
-
db.exec(SCHEMA_V1);
|
|
47
23
|
}
|
|
48
|
-
function
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return;
|
|
24
|
+
function getAppliedMigrations(db) {
|
|
25
|
+
const rows = db.prepare('SELECT migration_id FROM schema_migrations').all();
|
|
26
|
+
return new Set(rows.map(r => r.migration_id));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Run migrations with atomic rollback on failure.
|
|
30
|
+
* All pending migrations are run in a single transaction.
|
|
31
|
+
* If any migration fails, ALL changes are rolled back.
|
|
32
|
+
*/
|
|
33
|
+
export function runMigrationsWithRollback(db, migrations) {
|
|
34
|
+
ensureMigrationsTable(db);
|
|
35
|
+
const applied = [];
|
|
36
|
+
const skipped = [];
|
|
37
|
+
const alreadyApplied = getAppliedMigrations(db);
|
|
38
|
+
// Filter to pending migrations
|
|
39
|
+
const pendingMigrations = migrations.filter(m => {
|
|
40
|
+
if (alreadyApplied.has(m.id)) {
|
|
41
|
+
skipped.push(m.id);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
if (pendingMigrations.length === 0) {
|
|
47
|
+
return { success: true, applied, skipped };
|
|
69
48
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
49
|
+
// Run all pending migrations in a single transaction for atomic rollback
|
|
50
|
+
try {
|
|
51
|
+
db.exec('BEGIN IMMEDIATE');
|
|
52
|
+
for (const migration of pendingMigrations) {
|
|
53
|
+
try {
|
|
54
|
+
db.exec(migration.up);
|
|
55
|
+
// Record migration
|
|
56
|
+
db.prepare('INSERT INTO schema_migrations (migration_id, applied_at_ms, checksum) VALUES (?, ?, ?)').run(migration.id, Date.now(), computeChecksum(migration.up));
|
|
57
|
+
applied.push(migration.id);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// Rollback the entire transaction
|
|
61
|
+
db.exec('ROLLBACK');
|
|
62
|
+
throw new MigrationError(`Migration ${migration.id} failed: ${err instanceof Error ? err.message : String(err)}`, migration.id, applied // These were applied before failure but are now rolled back
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
db.exec('COMMIT');
|
|
67
|
+
return { success: true, applied, skipped };
|
|
85
68
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
VALUES (?, ?, ?, ?, ?)
|
|
90
|
-
`);
|
|
91
|
-
const insertProject = db.prepare(`
|
|
92
|
-
INSERT OR IGNORE INTO projects (name, description, is_protected, created_at, last_event_id)
|
|
93
|
-
VALUES (?, NULL, ?, ?, ?)
|
|
94
|
-
`);
|
|
95
|
-
db.transaction(() => {
|
|
96
|
-
for (const { project } of projectsToCreate) {
|
|
97
|
-
const eventId = generateId();
|
|
98
|
-
const data = JSON.stringify({ name: project });
|
|
99
|
-
const result = insertEvent.run(eventId, PROJECT_EVENT_TASK_ID, EventType.ProjectCreated, data, timestamp);
|
|
100
|
-
insertProject.run(project, 0, timestamp, result.lastInsertRowid);
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (err instanceof MigrationError) {
|
|
71
|
+
throw err;
|
|
101
72
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const result = insertEvent.run(eventId, PROJECT_EVENT_TASK_ID, EventType.ProjectCreated, data, timestamp);
|
|
106
|
-
insertProject.run('inbox', 1, timestamp, result.lastInsertRowid);
|
|
73
|
+
// Unexpected error - ensure rollback
|
|
74
|
+
try {
|
|
75
|
+
db.exec('ROLLBACK');
|
|
107
76
|
}
|
|
108
|
-
|
|
109
|
-
|
|
77
|
+
catch {
|
|
78
|
+
// Ignore rollback errors
|
|
110
79
|
}
|
|
111
|
-
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
112
82
|
}
|
|
113
83
|
//# sourceMappingURL=migrations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/db/migrations.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/db/migrations.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,115 +1,50 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
describe('migrations', () => {
|
|
2
|
+
import { runMigrationsWithRollback, MigrationError } from './migrations.js';
|
|
3
|
+
import { createTestDb } from './test-utils.js';
|
|
4
|
+
describe('migrations with rollback', () => {
|
|
6
5
|
let db;
|
|
7
6
|
beforeEach(() => {
|
|
8
|
-
db =
|
|
7
|
+
db = createTestDb();
|
|
9
8
|
});
|
|
10
9
|
afterEach(() => {
|
|
11
10
|
db.close();
|
|
12
11
|
});
|
|
13
|
-
it('
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
expect(
|
|
25
|
-
expect(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
expect(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
expect(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
expect(
|
|
48
|
-
|
|
49
|
-
it('creates task_tags table for fast tag filtering', () => {
|
|
50
|
-
runMigrations(db);
|
|
51
|
-
const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='task_tags'").get();
|
|
52
|
-
expect(table).toBeDefined();
|
|
53
|
-
});
|
|
54
|
-
it('creates task_comments table', () => {
|
|
55
|
-
runMigrations(db);
|
|
56
|
-
const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='task_comments'").get();
|
|
57
|
-
expect(table).toBeDefined();
|
|
58
|
-
});
|
|
59
|
-
it('creates task_checkpoints table', () => {
|
|
60
|
-
runMigrations(db);
|
|
61
|
-
const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='task_checkpoints'").get();
|
|
62
|
-
expect(table).toBeDefined();
|
|
63
|
-
});
|
|
64
|
-
it('creates task_search FTS5 table', () => {
|
|
65
|
-
runMigrations(db);
|
|
66
|
-
const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='task_search'").get();
|
|
67
|
-
expect(table).toBeDefined();
|
|
68
|
-
});
|
|
69
|
-
it('is idempotent', () => {
|
|
70
|
-
runMigrations(db);
|
|
71
|
-
runMigrations(db);
|
|
72
|
-
const version = getCurrentVersion(db);
|
|
73
|
-
expect(version).toBe(1);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
describe('projects table migration', () => {
|
|
77
|
-
it('should create projects table', () => {
|
|
78
|
-
const db = new Database(':memory:');
|
|
79
|
-
db.exec(SCHEMA_V1);
|
|
80
|
-
db.exec('DROP TABLE projects');
|
|
81
|
-
db.exec('DROP INDEX IF EXISTS idx_projects_protected');
|
|
82
|
-
db.prepare(`INSERT INTO tasks_current (task_id, title, project, status, created_at, updated_at, last_event_id)
|
|
83
|
-
VALUES ('t1', 'Task 1', 'projectA', 'ready', datetime('now'), datetime('now'), 1)`).run();
|
|
84
|
-
db.prepare(`INSERT INTO tasks_current (task_id, title, project, status, created_at, updated_at, last_event_id)
|
|
85
|
-
VALUES ('t2', 'Task 2', 'projectB', 'ready', datetime('now'), datetime('now'), 2)`).run();
|
|
86
|
-
runMigrations(db);
|
|
87
|
-
const projects = db.prepare('SELECT name FROM projects ORDER BY name').all();
|
|
88
|
-
expect(projects.map((p) => p.name)).toContain('inbox');
|
|
89
|
-
expect(projects.map((p) => p.name)).toContain('projectA');
|
|
90
|
-
expect(projects.map((p) => p.name)).toContain('projectB');
|
|
91
|
-
const inbox = db
|
|
92
|
-
.prepare('SELECT is_protected FROM projects WHERE name = ?')
|
|
93
|
-
.get('inbox');
|
|
94
|
-
expect(inbox.is_protected).toBe(1);
|
|
95
|
-
db.close();
|
|
96
|
-
});
|
|
97
|
-
it('should emit synthetic ProjectCreated events for existing projects', () => {
|
|
98
|
-
const db = new Database(':memory:');
|
|
99
|
-
db.exec(SCHEMA_V1);
|
|
100
|
-
db.exec('DROP TABLE projects');
|
|
101
|
-
db.exec('DROP INDEX IF EXISTS idx_projects_protected');
|
|
102
|
-
db.prepare(`INSERT INTO tasks_current (task_id, title, project, status, created_at, updated_at, last_event_id)
|
|
103
|
-
VALUES ('t1', 'Task 1', 'myproject', 'ready', datetime('now'), datetime('now'), 1)`).run();
|
|
104
|
-
runMigrations(db);
|
|
105
|
-
const events = db
|
|
106
|
-
.prepare(`SELECT * FROM events WHERE type = 'project_created'`)
|
|
107
|
-
.all();
|
|
108
|
-
expect(events.length).toBeGreaterThanOrEqual(2);
|
|
109
|
-
const projectNames = events.map((e) => JSON.parse(e.data).name);
|
|
110
|
-
expect(projectNames).toContain('inbox');
|
|
111
|
-
expect(projectNames).toContain('myproject');
|
|
112
|
-
db.close();
|
|
12
|
+
it('applies migrations successfully', () => {
|
|
13
|
+
const migrations = [
|
|
14
|
+
{ id: 'v001', up: 'CREATE TABLE test1 (id INTEGER PRIMARY KEY)' },
|
|
15
|
+
{ id: 'v002', up: 'CREATE TABLE test2 (id INTEGER PRIMARY KEY)' },
|
|
16
|
+
];
|
|
17
|
+
const result = runMigrationsWithRollback(db, migrations);
|
|
18
|
+
expect(result.success).toBe(true);
|
|
19
|
+
expect(result.applied).toEqual(['v001', 'v002']);
|
|
20
|
+
// Tables should exist
|
|
21
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
22
|
+
const tableNames = tables.map((t) => t.name);
|
|
23
|
+
expect(tableNames).toContain('test1');
|
|
24
|
+
expect(tableNames).toContain('test2');
|
|
25
|
+
});
|
|
26
|
+
it('rolls back on partial failure', () => {
|
|
27
|
+
const migrations = [
|
|
28
|
+
{ id: 'v001', up: 'CREATE TABLE test1 (id INTEGER PRIMARY KEY)' },
|
|
29
|
+
{ id: 'v002', up: 'INVALID SQL SYNTAX' }, // This will fail
|
|
30
|
+
];
|
|
31
|
+
expect(() => runMigrationsWithRollback(db, migrations)).toThrow(MigrationError);
|
|
32
|
+
// test1 should NOT exist due to rollback
|
|
33
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
|
|
34
|
+
const tableNames = tables.map((t) => t.name);
|
|
35
|
+
expect(tableNames).not.toContain('test1');
|
|
36
|
+
});
|
|
37
|
+
it('skips already applied migrations', () => {
|
|
38
|
+
const migrations = [
|
|
39
|
+
{ id: 'v001', up: 'CREATE TABLE test1 (id INTEGER PRIMARY KEY)' },
|
|
40
|
+
];
|
|
41
|
+
// Apply first time
|
|
42
|
+
runMigrationsWithRollback(db, migrations);
|
|
43
|
+
// Apply again - should skip
|
|
44
|
+
const result = runMigrationsWithRollback(db, migrations);
|
|
45
|
+
expect(result.success).toBe(true);
|
|
46
|
+
expect(result.applied).toEqual([]);
|
|
47
|
+
expect(result.skipped).toEqual(['v001']);
|
|
113
48
|
});
|
|
114
49
|
});
|
|
115
50
|
//# sourceMappingURL=migrations.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.test.js","sourceRoot":"","sources":["../../src/db/migrations.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"migrations.test.js","sourceRoot":"","sources":["../../src/db/migrations.test.ts"],"names":[],"mappings":"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"}
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export declare const
|
|
1
|
+
export declare const EVENTS_SCHEMA_V2 = "\n-- Append-only event store (source of truth)\nCREATE TABLE IF NOT EXISTS events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n event_id TEXT NOT NULL UNIQUE,\n task_id TEXT NOT NULL,\n type TEXT NOT NULL,\n data TEXT NOT NULL CHECK (json_valid(data)),\n author TEXT,\n agent_id TEXT,\n session_id TEXT,\n correlation_id TEXT,\n causation_id TEXT,\n timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))\n);\n\n-- Append-only enforcement: prevent UPDATE on events\nCREATE TRIGGER IF NOT EXISTS events_no_update\nBEFORE UPDATE ON events\nBEGIN\n SELECT RAISE(ABORT, 'Events table is append-only: cannot UPDATE');\nEND;\n\n-- Append-only enforcement: prevent DELETE on events\nCREATE TRIGGER IF NOT EXISTS events_no_delete\nBEFORE DELETE ON events\nBEGIN\n SELECT RAISE(ABORT, 'Events table is append-only: cannot DELETE');\nEND;\n\n-- Global metadata (synced, immutable after creation)\nCREATE TABLE IF NOT EXISTS hzl_global_meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\n-- Schema migrations tracking (append-only)\nCREATE TABLE IF NOT EXISTS schema_migrations (\n migration_id TEXT PRIMARY KEY,\n applied_at_ms INTEGER NOT NULL,\n checksum TEXT NOT NULL\n);\n\n-- Indexes for events\nCREATE INDEX IF NOT EXISTS idx_events_task_id ON events(task_id);\nCREATE INDEX IF NOT EXISTS idx_events_task_id_id ON events(task_id, id);\nCREATE INDEX IF NOT EXISTS idx_events_type ON events(type);\nCREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);\nCREATE INDEX IF NOT EXISTS idx_events_correlation_id ON events(correlation_id);\n";
|
|
2
|
+
export declare const CACHE_SCHEMA_V1 = "\n-- Local metadata (per-device sync state)\nCREATE TABLE IF NOT EXISTS hzl_local_meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\n-- Projection cursor (tracks last applied event)\nCREATE TABLE IF NOT EXISTS projection_cursor (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\n-- Track last applied event id per projection\nCREATE TABLE IF NOT EXISTS projection_state (\n name TEXT PRIMARY KEY,\n last_event_id INTEGER NOT NULL DEFAULT 0,\n updated_at TEXT NOT NULL\n);\n\n-- Current-state projection for fast reads (rebuildable from events)\nCREATE TABLE IF NOT EXISTS tasks_current (\n task_id TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n project TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('backlog','ready','in_progress','done','archived')),\n parent_id TEXT,\n description TEXT,\n links TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(links)),\n tags TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags)),\n priority INTEGER NOT NULL DEFAULT 0 CHECK (priority BETWEEN 0 AND 3),\n due_at TEXT,\n metadata TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(metadata)),\n claimed_at TEXT,\n claimed_by_author TEXT,\n claimed_by_agent_id TEXT,\n lease_until TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n last_event_id INTEGER NOT NULL\n);\n\n-- Dependency edges for fast availability checks (rebuildable)\nCREATE TABLE IF NOT EXISTS task_dependencies (\n task_id TEXT NOT NULL,\n depends_on_id TEXT NOT NULL,\n PRIMARY KEY (task_id, depends_on_id)\n);\n\n-- Fast tag filtering (rebuildable)\nCREATE TABLE IF NOT EXISTS task_tags (\n task_id TEXT NOT NULL,\n tag TEXT NOT NULL,\n PRIMARY KEY (task_id, tag)\n);\n\n-- Fast access for steering comments (rebuildable)\nCREATE TABLE IF NOT EXISTS task_comments (\n event_rowid INTEGER PRIMARY KEY,\n task_id TEXT NOT NULL,\n author TEXT,\n agent_id TEXT,\n text TEXT NOT NULL,\n timestamp TEXT NOT NULL\n);\n\n-- Fast access for checkpoints (rebuildable)\nCREATE TABLE IF NOT EXISTS task_checkpoints (\n event_rowid INTEGER PRIMARY KEY,\n task_id TEXT NOT NULL,\n name TEXT NOT NULL,\n data TEXT NOT NULL DEFAULT '{}' CHECK (json_valid(data)),\n timestamp TEXT NOT NULL\n);\n\n-- Full-text search over tasks (rebuildable)\nCREATE VIRTUAL TABLE IF NOT EXISTS task_search USING fts5(\n task_id UNINDEXED,\n title,\n description\n);\n\n-- Projects table (projection from events)\nCREATE TABLE IF NOT EXISTS projects (\n name TEXT PRIMARY KEY,\n description TEXT,\n is_protected INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n last_event_id INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_projects_protected ON projects(is_protected);\n\n-- Indexes for tasks_current\nCREATE INDEX IF NOT EXISTS idx_tasks_current_project_status ON tasks_current(project, status);\nCREATE INDEX IF NOT EXISTS idx_tasks_current_priority ON tasks_current(project, priority, created_at);\nCREATE INDEX IF NOT EXISTS idx_tasks_current_claim_next ON tasks_current(project, status, priority DESC, created_at ASC, task_id ASC);\nCREATE INDEX IF NOT EXISTS idx_tasks_current_stuck ON tasks_current(project, status, claimed_at);\nCREATE INDEX IF NOT EXISTS idx_tasks_current_parent ON tasks_current(parent_id);\n\n-- Indexes for dependencies\nCREATE INDEX IF NOT EXISTS idx_deps_depends_on ON task_dependencies(depends_on_id);\n\n-- Indexes for tags/comments/checkpoints\nCREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag, task_id);\nCREATE INDEX IF NOT EXISTS idx_task_comments_task ON task_comments(task_id, event_rowid);\nCREATE INDEX IF NOT EXISTS idx_task_checkpoints_task ON task_checkpoints(task_id, event_rowid);\n";
|
|
2
3
|
export declare const PRAGMAS = "\nPRAGMA journal_mode=WAL;\nPRAGMA synchronous=NORMAL;\nPRAGMA foreign_keys=ON;\nPRAGMA busy_timeout=5000;\n";
|
|
3
4
|
//# sourceMappingURL=schema.d.ts.map
|
package/dist/db/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,gBAAgB,srDAiD5B,CAAC;AAGF,eAAO,MAAM,eAAe,y4HA2G3B,CAAC;AAEF,eAAO,MAAM,OAAO,iHAKnB,CAAC"}
|
package/dist/db/schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// Schema for events.db (source of truth)
|
|
2
|
+
export const EVENTS_SCHEMA_V2 = `
|
|
2
3
|
-- Append-only event store (source of truth)
|
|
3
4
|
CREATE TABLE IF NOT EXISTS events (
|
|
4
5
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -14,7 +15,55 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
14
15
|
timestamp TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
|
|
15
16
|
);
|
|
16
17
|
|
|
17
|
-
--
|
|
18
|
+
-- Append-only enforcement: prevent UPDATE on events
|
|
19
|
+
CREATE TRIGGER IF NOT EXISTS events_no_update
|
|
20
|
+
BEFORE UPDATE ON events
|
|
21
|
+
BEGIN
|
|
22
|
+
SELECT RAISE(ABORT, 'Events table is append-only: cannot UPDATE');
|
|
23
|
+
END;
|
|
24
|
+
|
|
25
|
+
-- Append-only enforcement: prevent DELETE on events
|
|
26
|
+
CREATE TRIGGER IF NOT EXISTS events_no_delete
|
|
27
|
+
BEFORE DELETE ON events
|
|
28
|
+
BEGIN
|
|
29
|
+
SELECT RAISE(ABORT, 'Events table is append-only: cannot DELETE');
|
|
30
|
+
END;
|
|
31
|
+
|
|
32
|
+
-- Global metadata (synced, immutable after creation)
|
|
33
|
+
CREATE TABLE IF NOT EXISTS hzl_global_meta (
|
|
34
|
+
key TEXT PRIMARY KEY,
|
|
35
|
+
value TEXT NOT NULL
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
-- Schema migrations tracking (append-only)
|
|
39
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
40
|
+
migration_id TEXT PRIMARY KEY,
|
|
41
|
+
applied_at_ms INTEGER NOT NULL,
|
|
42
|
+
checksum TEXT NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
-- Indexes for events
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_events_task_id ON events(task_id);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_events_task_id_id ON events(task_id, id);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_events_correlation_id ON events(correlation_id);
|
|
51
|
+
`;
|
|
52
|
+
// Cache schema (local-only, rebuildable)
|
|
53
|
+
export const CACHE_SCHEMA_V1 = `
|
|
54
|
+
-- Local metadata (per-device sync state)
|
|
55
|
+
CREATE TABLE IF NOT EXISTS hzl_local_meta (
|
|
56
|
+
key TEXT PRIMARY KEY,
|
|
57
|
+
value TEXT NOT NULL
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
-- Projection cursor (tracks last applied event)
|
|
61
|
+
CREATE TABLE IF NOT EXISTS projection_cursor (
|
|
62
|
+
key TEXT PRIMARY KEY,
|
|
63
|
+
value TEXT NOT NULL
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
-- Track last applied event id per projection
|
|
18
67
|
CREATE TABLE IF NOT EXISTS projection_state (
|
|
19
68
|
name TEXT PRIMARY KEY,
|
|
20
69
|
last_event_id INTEGER NOT NULL DEFAULT 0,
|
|
@@ -43,7 +92,7 @@ CREATE TABLE IF NOT EXISTS tasks_current (
|
|
|
43
92
|
last_event_id INTEGER NOT NULL
|
|
44
93
|
);
|
|
45
94
|
|
|
46
|
-
-- Dependency edges for fast availability checks (rebuildable
|
|
95
|
+
-- Dependency edges for fast availability checks (rebuildable)
|
|
47
96
|
CREATE TABLE IF NOT EXISTS task_dependencies (
|
|
48
97
|
task_id TEXT NOT NULL,
|
|
49
98
|
depends_on_id TEXT NOT NULL,
|
|
@@ -94,13 +143,6 @@ CREATE TABLE IF NOT EXISTS projects (
|
|
|
94
143
|
|
|
95
144
|
CREATE INDEX IF NOT EXISTS idx_projects_protected ON projects(is_protected);
|
|
96
145
|
|
|
97
|
-
-- Indexes for events
|
|
98
|
-
CREATE INDEX IF NOT EXISTS idx_events_task_id ON events(task_id);
|
|
99
|
-
CREATE INDEX IF NOT EXISTS idx_events_task_id_id ON events(task_id, id);
|
|
100
|
-
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
101
|
-
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
|
|
102
|
-
CREATE INDEX IF NOT EXISTS idx_events_correlation_id ON events(correlation_id);
|
|
103
|
-
|
|
104
146
|
-- Indexes for tasks_current
|
|
105
147
|
CREATE INDEX IF NOT EXISTS idx_tasks_current_project_status ON tasks_current(project, status);
|
|
106
148
|
CREATE INDEX IF NOT EXISTS idx_tasks_current_priority ON tasks_current(project, priority, created_at);
|
package/dist/db/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiD/B,CAAC;AAEF,yCAAyC;AACzC,MAAM,CAAC,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2G9B,CAAC;AAEF,MAAM,CAAC,MAAM,OAAO,GAAG;;;;;CAKtB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SyncConfig, SyncPolicy as SyncPolicyType } from './types.js';
|
|
2
|
+
export interface SyncState {
|
|
3
|
+
lastSyncAt: number | null;
|
|
4
|
+
isDirty: boolean;
|
|
5
|
+
lastSyncAttemptAt?: number | null;
|
|
6
|
+
lastSyncError?: string | null;
|
|
7
|
+
}
|
|
8
|
+
export interface AfterWriteState {
|
|
9
|
+
isDirty: boolean;
|
|
10
|
+
lastSyncAttemptAt?: number | null;
|
|
11
|
+
}
|
|
12
|
+
export type SyncTrigger = 'stale' | 'dirty' | 'forced' | 'none';
|
|
13
|
+
export type SyncErrorAction = 'continue' | 'fail';
|
|
14
|
+
export interface SyncPolicy {
|
|
15
|
+
type: SyncPolicyType;
|
|
16
|
+
shouldSyncBefore(state: SyncState): boolean;
|
|
17
|
+
shouldSyncAfter(state: AfterWriteState): boolean;
|
|
18
|
+
onSyncError(error: Error): SyncErrorAction;
|
|
19
|
+
}
|
|
20
|
+
export declare function createSyncPolicy(config?: Partial<SyncConfig>): SyncPolicy;
|
|
21
|
+
//# sourceMappingURL=sync-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-policy.d.ts","sourceRoot":"","sources":["../../src/db/sync-policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,WAAW,SAAS;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;AAChE,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,MAAM,CAAC;AAElD,MAAM,WAAW,UAAU;IACvB,IAAI,EAAE,cAAc,CAAC;IACrB,gBAAgB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC;IAC5C,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC;IACjD,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAAC;CAC9C;AAMD,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,OAAO,CAAC,UAAU,CAAM,GAAG,UAAU,CAqE7E"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const DEFAULT_STALE_AFTER_MS = 60000;
|
|
2
|
+
const DEFAULT_MIN_INTERVAL_MS = 15000;
|
|
3
|
+
const DEFAULT_FAILURE_BACKOFF_MS = 60000;
|
|
4
|
+
export function createSyncPolicy(config = {}) {
|
|
5
|
+
const policyType = config.policy ?? 'opportunistic';
|
|
6
|
+
const staleAfterMs = config.staleAfterMs ?? DEFAULT_STALE_AFTER_MS;
|
|
7
|
+
const minIntervalMs = config.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS;
|
|
8
|
+
const failureBackoffMs = config.failureBackoffMs ?? DEFAULT_FAILURE_BACKOFF_MS;
|
|
9
|
+
if (policyType === 'manual') {
|
|
10
|
+
return {
|
|
11
|
+
type: 'manual',
|
|
12
|
+
shouldSyncBefore: () => false,
|
|
13
|
+
shouldSyncAfter: () => false,
|
|
14
|
+
onSyncError: () => 'continue',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (policyType === 'strict') {
|
|
18
|
+
return {
|
|
19
|
+
type: 'strict',
|
|
20
|
+
shouldSyncBefore: () => true,
|
|
21
|
+
shouldSyncAfter: () => true,
|
|
22
|
+
onSyncError: () => 'fail',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// Opportunistic policy
|
|
26
|
+
return {
|
|
27
|
+
type: 'opportunistic',
|
|
28
|
+
shouldSyncBefore(state) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
// Check for failure backoff
|
|
31
|
+
if (state.lastSyncError && state.lastSyncAttemptAt) {
|
|
32
|
+
const timeSinceAttempt = now - state.lastSyncAttemptAt;
|
|
33
|
+
if (timeSinceAttempt < failureBackoffMs) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Never synced - should sync
|
|
38
|
+
if (state.lastSyncAt === null) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
// Check if stale
|
|
42
|
+
const timeSinceSync = now - state.lastSyncAt;
|
|
43
|
+
return timeSinceSync > staleAfterMs;
|
|
44
|
+
},
|
|
45
|
+
shouldSyncAfter(state) {
|
|
46
|
+
if (!state.isDirty) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
// Check min interval
|
|
51
|
+
if (state.lastSyncAttemptAt) {
|
|
52
|
+
const timeSinceAttempt = now - state.lastSyncAttemptAt;
|
|
53
|
+
if (timeSinceAttempt < minIntervalMs) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
},
|
|
59
|
+
onSyncError: () => 'continue',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=sync-policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-policy.js","sourceRoot":"","sources":["../../src/db/sync-policy.ts"],"names":[],"mappings":"AAwBA,MAAM,sBAAsB,GAAG,KAAK,CAAC;AACrC,MAAM,uBAAuB,GAAG,KAAK,CAAC;AACtC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAEzC,MAAM,UAAU,gBAAgB,CAAC,SAA8B,EAAE;IAC7D,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,IAAI,eAAe,CAAC;IACpD,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,IAAI,sBAAsB,CAAC;IACnE,MAAM,aAAa,GAAG,MAAM,CAAC,aAAa,IAAI,uBAAuB,CAAC;IACtE,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,0BAA0B,CAAC;IAE/E,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO;YACH,IAAI,EAAE,QAAQ;YACd,gBAAgB,EAAE,GAAG,EAAE,CAAC,KAAK;YAC7B,eAAe,EAAE,GAAG,EAAE,CAAC,KAAK;YAC5B,WAAW,EAAE,GAAG,EAAE,CAAC,UAAU;SAChC,CAAC;IACN,CAAC;IAED,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO;YACH,IAAI,EAAE,QAAQ;YACd,gBAAgB,EAAE,GAAG,EAAE,CAAC,IAAI;YAC5B,eAAe,EAAE,GAAG,EAAE,CAAC,IAAI;YAC3B,WAAW,EAAE,GAAG,EAAE,CAAC,MAAM;SAC5B,CAAC;IACN,CAAC;IAED,uBAAuB;IACvB,OAAO;QACH,IAAI,EAAE,eAAe;QAErB,gBAAgB,CAAC,KAAgB;YAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAEvB,4BAA4B;YAC5B,IAAI,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,iBAAiB,EAAE,CAAC;gBACjD,MAAM,gBAAgB,GAAG,GAAG,GAAG,KAAK,CAAC,iBAAiB,CAAC;gBACvD,IAAI,gBAAgB,GAAG,gBAAgB,EAAE,CAAC;oBACtC,OAAO,KAAK,CAAC;gBACjB,CAAC;YACL,CAAC;YAED,6BAA6B;YAC7B,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;gBAC5B,OAAO,IAAI,CAAC;YAChB,CAAC;YAED,iBAAiB;YACjB,MAAM,aAAa,GAAG,GAAG,GAAG,KAAK,CAAC,UAAU,CAAC;YAC7C,OAAO,aAAa,GAAG,YAAY,CAAC;QACxC,CAAC;QAED,eAAe,CAAC,KAAsB;YAClC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC;gBACjB,OAAO,KAAK,CAAC;YACjB,CAAC;YAED,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAEvB,qBAAqB;YACrB,IAAI,KAAK,CAAC,iBAAiB,EAAE,CAAC;gBAC1B,MAAM,gBAAgB,GAAG,GAAG,GAAG,KAAK,CAAC,iBAAiB,CAAC;gBACvD,IAAI,gBAAgB,GAAG,aAAa,EAAE,CAAC;oBACnC,OAAO,KAAK,CAAC;gBACjB,CAAC;YACL,CAAC;YAED,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,WAAW,EAAE,GAAG,EAAE,CAAC,UAAU;KAChC,CAAC;AACN,CAAC"}
|