trekoon 0.1.6 → 0.1.8

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.
@@ -278,11 +278,58 @@ function recordMigration(db: Database, migration: Migration): void {
278
278
  );
279
279
  }
280
280
 
281
+ function isSchemaCurrentFastPath(db: Database, latestVersion: number): boolean {
282
+ if (latestVersion === 0 || !migrationTableExists(db) || !hasMigrationVersionColumn(db)) {
283
+ return false;
284
+ }
285
+
286
+ const row = db
287
+ .query(
288
+ `
289
+ SELECT
290
+ COALESCE(MIN(version), 0) AS min_version,
291
+ COALESCE(MAX(version), 0) AS max_version,
292
+ COUNT(DISTINCT version) AS distinct_versions,
293
+ SUM(CASE WHEN version IS NULL THEN 1 ELSE 0 END) AS null_versions
294
+ FROM schema_migrations;
295
+ `,
296
+ )
297
+ .get() as
298
+ | {
299
+ min_version: number;
300
+ max_version: number;
301
+ distinct_versions: number;
302
+ null_versions: number;
303
+ }
304
+ | null;
305
+
306
+ if (!row) {
307
+ return false;
308
+ }
309
+
310
+ return (
311
+ row.null_versions === 0 &&
312
+ row.min_version === 1 &&
313
+ row.max_version === latestVersion &&
314
+ row.distinct_versions === latestVersion
315
+ );
316
+ }
317
+
281
318
  export function migrateDatabase(db: Database): void {
319
+ validateMigrationPlan();
320
+
321
+ const latestVersion: number = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
322
+
323
+ // Fast path: avoid BEGIN EXCLUSIVE when schema is already current.
324
+ // This reduces startup lock contention while keeping the explicit
325
+ // transactional migration path for non-current/legacy schemas.
326
+ if (isSchemaCurrentFastPath(db, latestVersion)) {
327
+ return;
328
+ }
329
+
282
330
  runExclusive(db, (): void => {
283
331
  ensureMigrationTable(db);
284
332
  ensureMigrationVersionColumn(db);
285
- validateMigrationPlan();
286
333
 
287
334
  const version: number = currentVersion(db);
288
335
 
@@ -225,17 +225,55 @@ function countBehind(remoteDb: Database, cursorToken: string): number {
225
225
  return row?.count ?? 0;
226
226
  }
227
227
 
228
- function listRemoteEventIds(remoteDb: Database): Set<string> {
229
- const rows = remoteDb.query("SELECT id FROM events;").all() as Array<{ id: string }>;
230
- return new Set(rows.map((row) => row.id));
228
+ function countAhead(localDb: Database, currentBranch: string | null, remoteDbPath: string): number {
229
+ if (!currentBranch) {
230
+ return 0;
231
+ }
232
+
233
+ localDb.query("ATTACH DATABASE ? AS sync_remote;").run(remoteDbPath);
234
+
235
+ try {
236
+ const row = localDb
237
+ .query(
238
+ `
239
+ SELECT COUNT(*) AS count
240
+ FROM events AS local_events
241
+ WHERE local_events.git_branch = @branch
242
+ AND NOT EXISTS (
243
+ SELECT 1
244
+ FROM sync_remote.events AS remote_events
245
+ WHERE remote_events.id = local_events.id
246
+ );
247
+ `,
248
+ )
249
+ .get({ "@branch": currentBranch }) as { count: number } | null;
250
+
251
+ return row?.count ?? 0;
252
+ } finally {
253
+ localDb.query("DETACH DATABASE sync_remote;").run();
254
+ }
231
255
  }
232
256
 
233
- function countAhead(localDb: Database, currentBranch: string | null, remoteEventIds: Set<string>): number {
234
- const rows = localDb
235
- .query("SELECT id, git_branch FROM events WHERE git_branch = ?;")
236
- .all(currentBranch) as Array<{ id: string; git_branch: string | null }>;
257
+ function buildSyncErrorHints(diagnostics: {
258
+ malformedPayloadEvents: number;
259
+ applyRejectedEvents: number;
260
+ conflictEvents: number;
261
+ }): string[] {
262
+ const hints: string[] = [];
263
+
264
+ if (diagnostics.malformedPayloadEvents > 0) {
265
+ hints.push("Malformed event payloads were quarantined; inspect sync conflicts with field '__payload__'.");
266
+ }
267
+
268
+ if (diagnostics.applyRejectedEvents > 0) {
269
+ hints.push("Some events were quarantined as invalid; inspect sync conflicts with field '__apply__'.");
270
+ }
271
+
272
+ if (diagnostics.conflictEvents > 0) {
273
+ hints.push("Field-level conflicts detected; run 'trekoon sync conflicts list' and resolve pending entries.");
274
+ }
237
275
 
238
- return rows.filter((row) => !remoteEventIds.has(row.id)).length;
276
+ return hints;
239
277
  }
240
278
 
241
279
  function readFieldValue(payload: EventPayload, field: string): unknown {
@@ -631,7 +669,7 @@ export function syncStatus(cwd: string, sourceBranch: string): SyncStatusSummary
631
669
  try {
632
670
  return {
633
671
  sourceBranch,
634
- ahead: countAhead(storage.db, git.branchName, listRemoteEventIds(remote.db)),
672
+ ahead: countAhead(storage.db, git.branchName, remote.path),
635
673
  behind: countBehind(remote.db, cursorToken),
636
674
  pendingConflicts: countPendingConflicts(storage.db),
637
675
  git,
@@ -658,6 +696,10 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
658
696
 
659
697
  let appliedEvents = 0;
660
698
  let createdConflicts = 0;
699
+ let malformedPayloadEvents = 0;
700
+ let applyRejectedEvents = 0;
701
+ let quarantinedEvents = 0;
702
+ let conflictEvents = 0;
661
703
  let lastToken: string | null = null;
662
704
  let lastEventAt: number | null = cursor?.last_event_at ?? null;
663
705
 
@@ -666,6 +708,8 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
666
708
  const payloadValidation = parsePayload(incoming.payload);
667
709
 
668
710
  if (!payloadValidation.ok) {
711
+ malformedPayloadEvents += 1;
712
+ quarantinedEvents += 1;
669
713
  createConflict(
670
714
  storage.db,
671
715
  incoming,
@@ -688,6 +732,7 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
688
732
  const conflict = entityFieldConflict(storage.db, sourceBranch, incoming, fieldName, value);
689
733
 
690
734
  if (conflict) {
735
+ conflictEvents += 1;
691
736
  createConflict(storage.db, incoming, fieldName, conflict.oursValue, conflict.theirsValue);
692
737
  createdConflicts += 1;
693
738
  continue;
@@ -699,6 +744,8 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
699
744
  if (applyEntityFields(storage.db, incoming, fieldsToApply)) {
700
745
  appliedEvents += 1;
701
746
  } else if (incoming.operation !== "resolve_conflict") {
747
+ applyRejectedEvents += 1;
748
+ quarantinedEvents += 1;
702
749
  createConflict(
703
750
  storage.db,
704
751
  incoming,
@@ -726,6 +773,17 @@ export function syncPull(cwd: string, sourceBranch: string): PullSummary {
726
773
  appliedEvents,
727
774
  createdConflicts,
728
775
  cursorToken: lastToken,
776
+ diagnostics: {
777
+ malformedPayloadEvents,
778
+ applyRejectedEvents,
779
+ quarantinedEvents,
780
+ conflictEvents,
781
+ errorHints: buildSyncErrorHints({
782
+ malformedPayloadEvents,
783
+ applyRejectedEvents,
784
+ conflictEvents,
785
+ }),
786
+ },
729
787
  };
730
788
  } finally {
731
789
  remote.close();
package/src/sync/types.ts CHANGED
@@ -21,6 +21,15 @@ export interface PullSummary {
21
21
  readonly appliedEvents: number;
22
22
  readonly createdConflicts: number;
23
23
  readonly cursorToken: string | null;
24
+ readonly diagnostics: SyncPullDiagnostics;
25
+ }
26
+
27
+ export interface SyncPullDiagnostics {
28
+ readonly malformedPayloadEvents: number;
29
+ readonly applyRejectedEvents: number;
30
+ readonly quarantinedEvents: number;
31
+ readonly conflictEvents: number;
32
+ readonly errorHints: readonly string[];
24
33
  }
25
34
 
26
35
  export interface ResolveSummary {