@interocitor/core 0.0.0-beta.3 → 0.0.0-beta.5

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.
Files changed (98) hide show
  1. package/README.md +446 -91
  2. package/dist/adapters/cloudflare.d.ts +8 -9
  3. package/dist/adapters/cloudflare.d.ts.map +1 -1
  4. package/dist/adapters/cloudflare.js +38 -12
  5. package/dist/adapters/google-drive.d.ts +1 -1
  6. package/dist/adapters/google-drive.js +1 -2
  7. package/dist/adapters/memory.d.ts +4 -1
  8. package/dist/adapters/memory.d.ts.map +1 -1
  9. package/dist/adapters/memory.js +13 -2
  10. package/dist/adapters/webdav.d.ts +5 -0
  11. package/dist/adapters/webdav.d.ts.map +1 -1
  12. package/dist/adapters/webdav.js +18 -1
  13. package/dist/core/codec.d.ts +1 -1
  14. package/dist/core/codec.d.ts.map +1 -1
  15. package/dist/core/codec.js +39 -3
  16. package/dist/core/compaction.d.ts +9 -1
  17. package/dist/core/compaction.d.ts.map +1 -1
  18. package/dist/core/compaction.js +63 -7
  19. package/dist/core/crdt.d.ts +6 -3
  20. package/dist/core/crdt.d.ts.map +1 -1
  21. package/dist/core/crdt.js +53 -67
  22. package/dist/core/errors.d.ts +47 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +61 -0
  25. package/dist/core/flush.d.ts +3 -3
  26. package/dist/core/flush.d.ts.map +1 -1
  27. package/dist/core/flush.js +64 -7
  28. package/dist/core/hlc.js +0 -1
  29. package/dist/core/ids.d.ts +49 -0
  30. package/dist/core/ids.d.ts.map +1 -0
  31. package/dist/core/ids.js +132 -0
  32. package/dist/core/internals.d.ts +10 -2
  33. package/dist/core/internals.d.ts.map +1 -1
  34. package/dist/core/internals.js +27 -9
  35. package/dist/core/manifest.d.ts +24 -5
  36. package/dist/core/manifest.d.ts.map +1 -1
  37. package/dist/core/manifest.js +80 -13
  38. package/dist/core/pull.d.ts +1 -1
  39. package/dist/core/pull.d.ts.map +1 -1
  40. package/dist/core/pull.js +21 -6
  41. package/dist/core/row-id.js +0 -1
  42. package/dist/core/schema-types.d.ts +22 -11
  43. package/dist/core/schema-types.d.ts.map +1 -1
  44. package/dist/core/schema-types.js +18 -9
  45. package/dist/core/schema-types.type-test.js +59 -5
  46. package/dist/core/sync-engine.d.ts +166 -12
  47. package/dist/core/sync-engine.d.ts.map +1 -1
  48. package/dist/core/sync-engine.js +1615 -219
  49. package/dist/core/table.d.ts +217 -17
  50. package/dist/core/table.d.ts.map +1 -1
  51. package/dist/core/table.js +376 -24
  52. package/dist/core/types.d.ts +413 -17
  53. package/dist/core/types.d.ts.map +1 -1
  54. package/dist/core/types.js +0 -1
  55. package/dist/crypto/encryption.d.ts.map +1 -1
  56. package/dist/crypto/encryption.js +6 -1
  57. package/dist/crypto/keys.js +0 -1
  58. package/dist/handshake/channel.js +0 -1
  59. package/dist/handshake/index.d.ts +5 -2
  60. package/dist/handshake/index.d.ts.map +1 -1
  61. package/dist/handshake/index.js +19 -2
  62. package/dist/handshake/qr.js +0 -1
  63. package/dist/index.d.ts +9 -7
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +8 -6
  66. package/dist/storage/credential-store.d.ts +25 -2
  67. package/dist/storage/credential-store.d.ts.map +1 -1
  68. package/dist/storage/credential-store.js +55 -8
  69. package/dist/storage/local-store.d.ts +4 -1
  70. package/dist/storage/local-store.d.ts.map +1 -1
  71. package/dist/storage/local-store.js +37 -21
  72. package/package.json +3 -3
  73. package/dist/adapters/cloudflare.js.map +0 -1
  74. package/dist/adapters/google-drive.js.map +0 -1
  75. package/dist/adapters/memory.js.map +0 -1
  76. package/dist/adapters/webdav.js.map +0 -1
  77. package/dist/core/codec.js.map +0 -1
  78. package/dist/core/compaction.js.map +0 -1
  79. package/dist/core/crdt.js.map +0 -1
  80. package/dist/core/flush.js.map +0 -1
  81. package/dist/core/hlc.js.map +0 -1
  82. package/dist/core/internals.js.map +0 -1
  83. package/dist/core/manifest.js.map +0 -1
  84. package/dist/core/pull.js.map +0 -1
  85. package/dist/core/row-id.js.map +0 -1
  86. package/dist/core/schema-types.js.map +0 -1
  87. package/dist/core/schema-types.type-test.js.map +0 -1
  88. package/dist/core/sync-engine.js.map +0 -1
  89. package/dist/core/table.js.map +0 -1
  90. package/dist/core/types.js.map +0 -1
  91. package/dist/crypto/encryption.js.map +0 -1
  92. package/dist/crypto/keys.js.map +0 -1
  93. package/dist/handshake/channel.js.map +0 -1
  94. package/dist/handshake/index.js.map +0 -1
  95. package/dist/handshake/qr.js.map +0 -1
  96. package/dist/index.js.map +0 -1
  97. package/dist/storage/credential-store.js.map +0 -1
  98. package/dist/storage/local-store.js.map +0 -1
@@ -18,31 +18,50 @@ export function paths(root) {
18
18
  }
19
19
  // ─── Logger ──────────────────────────────────────────────────────────
20
20
  const LOG_PREFIX = '[interocitor]';
21
- export function log(level, ...args) {
21
+ const LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
22
+ function shouldLog(currentLevel, messageLevel) {
23
+ return LOG_LEVELS.indexOf(messageLevel) >= LOG_LEVELS.indexOf(currentLevel);
24
+ }
25
+ export function logAtLevel(currentLevel, level, ...args) {
26
+ if (!shouldLog(currentLevel, level))
27
+ return;
22
28
  // eslint-disable-next-line no-console
23
29
  console[level](LOG_PREFIX, ...args);
24
30
  }
31
+ export function log(level, ...args) {
32
+ logAtLevel('debug', level, ...args);
33
+ }
34
+ export function normalizeLogLevel(level) {
35
+ return LOG_LEVELS.includes(level ?? '') ? level : 'info';
36
+ }
37
+ export { LOG_LEVELS };
25
38
  // ─── ID generation ───────────────────────────────────────────────────
39
+ import { uuidv7, createDeviceId } from "./ids.js";
40
+ /**
41
+ * Generate a prefixed ID for internal use (change entries, snapshots, etc).
42
+ * Uses UUIDv7 for sortability.
43
+ */
26
44
  export function generateId(prefix) {
27
- const rand = crypto.getRandomValues(new Uint8Array(8));
28
- const hex = Array.from(rand).map(b => b.toString(16).padStart(2, '0')).join('');
29
- return `${prefix}_${hex}`;
45
+ return `${prefix}_${uuidv7()}`;
30
46
  }
31
47
  export function getDeviceId(override) {
32
48
  if (override)
33
49
  return override;
34
50
  const KEY = 'interocitor-device-id';
35
- let id = localStorage.getItem(KEY);
51
+ const storage = typeof localStorage !== 'undefined' ? localStorage : null;
52
+ let id = storage?.getItem(KEY) ?? null;
36
53
  if (!id) {
37
- id = generateId('dev');
38
- localStorage.setItem(KEY, id);
54
+ id = createDeviceId();
55
+ try {
56
+ storage?.setItem(KEY, id);
57
+ }
58
+ catch { /* ok */ }
39
59
  }
40
60
  return id;
41
61
  }
42
62
  // ─── Encoding / Hashing ─────────────────────────────────────────────
43
63
  export const textEncoder = new TextEncoder();
44
64
  export const textDecoder = new TextDecoder();
45
- export const ROW_META_KEYS = new Set(['_table', '_rowId', '_deleted', '_deletedHlc', '_schemaVersion']);
46
65
  export function hexFromBytes(bytes) {
47
66
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
48
67
  }
@@ -51,4 +70,3 @@ export async function computeContentHash(payload) {
51
70
  const digest = await crypto.subtle.digest('SHA-256', textEncoder.encode(json));
52
71
  return `sha256:${hexFromBytes(new Uint8Array(digest))}`;
53
72
  }
54
- //# sourceMappingURL=internals.js.map
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Manifest — reading, writing, creating, and validating cloud manifests.
3
3
  *
4
- * Extracted from SyncEngine. Not part of the public API.
4
+ * Extracted from Interocitor. Not part of the public API.
5
5
  */
6
- import type { StorageAdapter, Manifest, DatabaseSchemaDefinition, SyncEvent } from './types.ts';
6
+ import type { StorageAdapter, Manifest, ManifestPointer, DatabaseSchemaDefinition, SyncEvent } from './types.ts';
7
7
  import type { CodecState } from './codec.ts';
8
8
  export interface ManifestContext {
9
9
  adapter: StorageAdapter;
@@ -25,7 +25,26 @@ export declare function validateManifestHash(manifest: {
25
25
  [key: string]: unknown;
26
26
  }): Promise<void>;
27
27
  export declare function writeJson(adapter: StorageAdapter, path: string, value: unknown): Promise<void>;
28
- export declare function createBootstrapManifest(ctx: ManifestContext): Promise<void>;
29
- export declare function loadOrCreateManifest(ctx: ManifestContext, codecState: CodecState, local: import('./types.ts').LocalStoreAdapter, poisonRemote: (error: unknown, path?: string) => Promise<Error>): Promise<Manifest>;
30
- export declare function upsertDeviceMetadata(adapter: StorageAdapter, remotePath: string, deviceId: string): Promise<void>;
28
+ export declare function createBootstrapManifest(ctx: ManifestContext, meshId?: string): Promise<{
29
+ pointer: ManifestPointer;
30
+ manifest: Manifest;
31
+ }>;
32
+ export declare function loadOrCreateManifest(ctx: ManifestContext, codecState: CodecState, local: import('./types.ts').LocalStoreAdapter, poisonRemote: (error: unknown, path?: string) => Promise<Error>, reason?: string): Promise<{
33
+ manifest: Manifest;
34
+ bootstrapped: boolean;
35
+ }>;
36
+ export declare function upsertDeviceMetadata(adapter: StorageAdapter, remotePath: string, deviceId: string, opts?: {
37
+ displayName?: string;
38
+ deviceType?: import('./types.ts').DeviceType;
39
+ observedManifestGeneration?: number;
40
+ observedEpoch?: number;
41
+ observedWatermarkHlc?: string;
42
+ observedGcFloorHlc?: string;
43
+ /**
44
+ * When true, skip the read-merge step. Use only when the caller knows
45
+ * no prior device record exists (e.g. immediately after bootstrap of
46
+ * a fresh mesh). Saves one round-trip per connect on first run.
47
+ */
48
+ bootstrap?: boolean;
49
+ }): Promise<void>;
31
50
  //# sourceMappingURL=manifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/core/manifest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,QAAQ,EAGR,wBAAwB,EACxB,SAAS,EACV,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CAClC;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGnF;AAED,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAMlG;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxF;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACxD,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAEpG;AAED,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCjF;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,OAAO,YAAY,EAAE,iBAAiB,EAC7C,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,GAC9D,OAAO,CAAC,QAAQ,CAAC,CA8BnB;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAaf"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/core/manifest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,QAAQ,EACR,eAAe,EAEf,wBAAwB,EACxB,SAAS,EACV,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,cAAc,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CAClC;AAED,wBAAsB,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAGnF;AAED,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAMlG;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxF;AAED,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACxD,OAAO,CAAC,IAAI,CAAC,CAMf;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAGpG;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,eAAe,EACpB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,OAAO,EAAE,eAAe,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,CAsD3D;AAED,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,OAAO,YAAY,EAAE,iBAAiB,EAC7C,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,EAC/D,MAAM,GAAE,MAAkB,GACzB,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,YAAY,EAAE,OAAO,CAAA;CAAE,CAAC,CA2DxD;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IACL,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,YAAY,EAAE,UAAU,CAAC;IAC7C,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GACA,OAAO,CAAC,IAAI,CAAC,CAmCf"}
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Manifest — reading, writing, creating, and validating cloud manifests.
3
3
  *
4
- * Extracted from SyncEngine. Not part of the public API.
4
+ * Extracted from Interocitor. Not part of the public API.
5
5
  */
6
6
  import { paths, textEncoder, textDecoder, generateId, computeContentHash } from "./internals.js";
7
7
  import { assertExpectedMeshId } from "./codec.js";
8
+ import { MeshEncryptionMismatchError } from "./errors.js";
8
9
  export async function readJson(adapter, path) {
9
10
  const data = await adapter.readFile(path);
10
11
  return JSON.parse(textDecoder.decode(data));
@@ -30,9 +31,10 @@ export async function validateManifestHash(manifest) {
30
31
  }
31
32
  }
32
33
  export async function writeJson(adapter, path, value) {
34
+ console.log('[interocitor:write] manifest.writeJson', { path, kind: path.endsWith('/manifest.json') ? 'pointer' : path.includes('/manifest-') ? 'manifest' : path.includes('/devices/') ? 'device' : path.includes('/changes/') ? 'changes' : 'other' });
33
35
  await adapter.writeFile(path, textEncoder.encode(JSON.stringify(value, null, 2)));
34
36
  }
35
- export async function createBootstrapManifest(ctx) {
37
+ export async function createBootstrapManifest(ctx, meshId) {
36
38
  const p = paths(ctx.remotePath);
37
39
  const now = new Date().toISOString();
38
40
  const payload = {
@@ -41,7 +43,7 @@ export async function createBootstrapManifest(ctx) {
41
43
  writtenBy: ctx.serverId,
42
44
  writtenAt: now,
43
45
  version: 3,
44
- meshId: generateId('mesh'),
46
+ meshId: meshId || generateId('mesh'),
45
47
  schema: ctx.schema?.version ?? 1,
46
48
  encrypted: ctx.encrypted,
47
49
  server: {
@@ -60,21 +62,52 @@ export async function createBootstrapManifest(ctx) {
60
62
  contentHash: await computeContentHash(payload),
61
63
  };
62
64
  const manifestFile = `manifest-${manifest.generation}.json`;
63
- await writeJson(ctx.adapter, p.manifestFile(manifest.generation), manifest);
64
- await writeJson(ctx.adapter, p.manifestPointer, {
65
+ const pointer = {
65
66
  currentGeneration: manifest.generation,
66
67
  file: manifestFile,
68
+ };
69
+ await writeJson(ctx.adapter, p.manifestFile(manifest.generation), manifest);
70
+ ctx.emit({
71
+ type: 'trace:manifest',
72
+ op: 'write',
73
+ reason: 'bootstrap',
74
+ generation: manifest.generation,
75
+ path: p.manifestFile(manifest.generation),
67
76
  });
77
+ await writeJson(ctx.adapter, p.manifestPointer, pointer);
78
+ ctx.emit({
79
+ type: 'trace:manifest',
80
+ op: 'write',
81
+ reason: 'bootstrap-pointer',
82
+ generation: manifest.generation,
83
+ path: p.manifestPointer,
84
+ });
85
+ return { pointer, manifest };
68
86
  }
69
- export async function loadOrCreateManifest(ctx, codecState, local, poisonRemote) {
87
+ export async function loadOrCreateManifest(ctx, codecState, local, poisonRemote, reason = 'unknown') {
70
88
  const p = paths(ctx.remotePath);
89
+ ctx.emit({ type: 'trace:manifest', op: 'read', reason, path: p.manifestPointer });
71
90
  const globalPointer = await readJsonIfExists(ctx.adapter, p.manifestPointer);
91
+ let pointer;
92
+ let manifest;
93
+ let bootstrapped = false;
72
94
  if (!globalPointer) {
73
- await createBootstrapManifest(ctx);
95
+ bootstrapped = true;
96
+ ctx.emit({ type: 'trace:manifest', op: 'bootstrap-create', reason, path: p.manifestPointer });
97
+ const existingMeshId = await local.getMeta('meshId');
98
+ const bootstrap = await createBootstrapManifest(ctx, typeof existingMeshId === 'string' ? existingMeshId : undefined);
99
+ // Skip the read-after-write — we just minted both files in this process,
100
+ // they are exactly what's on disk. No GETs needed.
101
+ pointer = bootstrap.pointer;
102
+ manifest = bootstrap.manifest;
103
+ }
104
+ else {
105
+ pointer = globalPointer;
106
+ const manifestPath = `${ctx.remotePath}/${pointer.file}`;
107
+ ctx.emit({ type: 'trace:manifest', op: 'read', reason, path: manifestPath, generation: pointer.currentGeneration });
108
+ manifest = await readJson(ctx.adapter, manifestPath);
74
109
  }
75
- const pointer = await readJson(ctx.adapter, p.manifestPointer);
76
110
  const manifestPath = `${ctx.remotePath}/${pointer.file}`;
77
- const manifest = await readJson(ctx.adapter, manifestPath);
78
111
  await validateManifestHash(manifest);
79
112
  try {
80
113
  await assertExpectedMeshId(local, codecState.manifest, manifest.meshId);
@@ -92,20 +125,54 @@ export async function loadOrCreateManifest(ctx, codecState, local, poisonRemote)
92
125
  if (manifest.server.managed) {
93
126
  assertServerAuth(manifest, ctx.serverId);
94
127
  }
95
- return manifest;
128
+ // Encryption-mode parity check.
129
+ //
130
+ // The remote manifest pins the mesh's encryption mode at bootstrap.
131
+ // If the engine reconnects with a different `encrypted` flag (typical
132
+ // app bug: passphrase loaded asynchronously, so the first session
133
+ // wrote plaintext and the next session derives a key and tries to
134
+ // decrypt), every change file would fail decode and poison the remote.
135
+ //
136
+ // Surface this as an actionable error *before* any decode runs and
137
+ // *without* poisoning. The remote is not corrupt — the local config
138
+ // is wrong.
139
+ if (typeof manifest.encrypted === 'boolean' && manifest.encrypted !== ctx.encrypted) {
140
+ throw new MeshEncryptionMismatchError(manifest.encrypted, ctx.encrypted);
141
+ }
142
+ return { manifest, bootstrapped };
96
143
  }
97
- export async function upsertDeviceMetadata(adapter, remotePath, deviceId) {
144
+ export async function upsertDeviceMetadata(adapter, remotePath, deviceId, opts) {
98
145
  const p = paths(remotePath);
99
146
  const now = new Date().toISOString();
100
- const existing = await readJsonIfExists(adapter, p.deviceFile(deviceId));
147
+ // Bootstrap fast-path: caller asserts no prior record. Skip the GET.
148
+ // Worst case if caller is wrong: we clobber displayName/deviceType the
149
+ // user set on a different device — which would itself indicate the
150
+ // bootstrap flag was misused. Sync engine only sets bootstrap=true
151
+ // when it just minted the manifest in this same connect cycle.
152
+ const existing = opts?.bootstrap
153
+ ? null
154
+ : await readJsonIfExists(adapter, p.deviceFile(deviceId));
101
155
  const next = {
102
156
  deviceId,
103
157
  registeredAt: existing?.registeredAt ?? now,
104
158
  lastSeenAt: now,
105
159
  userId: existing?.userId,
106
160
  name: existing?.name,
161
+ displayName: opts?.displayName ?? existing?.displayName,
162
+ deviceType: opts?.deviceType ?? existing?.deviceType,
107
163
  retired: existing?.retired,
164
+ observedManifestGeneration: opts?.observedManifestGeneration ?? existing?.observedManifestGeneration,
165
+ observedEpoch: opts?.observedEpoch ?? existing?.observedEpoch,
166
+ observedWatermarkHlc: opts?.observedWatermarkHlc ?? existing?.observedWatermarkHlc,
167
+ observedGcFloorHlc: opts?.observedGcFloorHlc ?? existing?.observedGcFloorHlc,
168
+ observedAt: opts?.observedManifestGeneration !== undefined
169
+ || opts?.observedEpoch !== undefined
170
+ || opts?.observedWatermarkHlc !== undefined
171
+ || opts?.observedGcFloorHlc !== undefined
172
+ ? now
173
+ : existing?.observedAt,
174
+ cutOffAt: existing?.cutOffAt,
175
+ cutOffReason: existing?.cutOffReason,
108
176
  };
109
177
  await writeJson(adapter, p.deviceFile(deviceId), next);
110
178
  }
111
- //# sourceMappingURL=manifest.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pull — download remote changes and merge into local state.
3
3
  *
4
- * Extracted from SyncEngine. Not part of the public API.
4
+ * Extracted from Interocitor. Not part of the public API.
5
5
  */
6
6
  import type { StorageAdapter, LocalStoreAdapter, Row, Op, SyncEvent, DatabaseSchemaDefinition } from './types.ts';
7
7
  import type { HLC } from './types.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/core/pull.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,GAAG,EACH,EAAE,EAEF,SAAS,EACT,wBAAwB,EACzB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAKtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5C,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACjC,gBAAgB,EAAE,CAAC,GAAG,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAChE,oBAAoB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C;AAiBD,0CAA0C;AAC1C,wBAAsB,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAiFzD"}
1
+ {"version":3,"file":"pull.d.ts","sourceRoot":"","sources":["../../src/core/pull.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,GAAG,EACH,EAAE,EAEF,SAAS,EACT,wBAAwB,EACzB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAKtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAC5C,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,CAAC,EAAE,wBAAwB,CAAC;IAClC,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACjC,gBAAgB,EAAE,CAAC,GAAG,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAChE,oBAAoB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C;AAiBD,0CAA0C;AAC1C,wBAAsB,IAAI,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,CAiGzD"}
package/dist/core/pull.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Pull — download remote changes and merge into local state.
3
3
  *
4
- * Extracted from SyncEngine. Not part of the public API.
4
+ * Extracted from Interocitor. Not part of the public API.
5
5
  */
6
6
  import { hlcParse, hlcReceive, hlcCompareStr, hlcSerialize } from "./hlc.js";
7
7
  import { applyChangeEntry } from "./crdt.js";
@@ -10,12 +10,12 @@ import { decodeChangePayload } from "./codec.js";
10
10
  import { readJsonIfExists } from "./manifest.js";
11
11
  function emitAffectedRows(affected, knownTables, emit) {
12
12
  for (const row of affected) {
13
- knownTables.add(row._table);
14
- if (row._deleted) {
15
- emit({ type: 'delete', table: row._table, rowId: row._rowId });
13
+ knownTables.add(row._meta.table);
14
+ if (row._meta.deleted) {
15
+ emit({ type: 'delete', table: row._meta.table, rowId: row._meta.rowId });
16
16
  }
17
17
  else {
18
- emit({ type: 'change', table: row._table, rowId: row._rowId, row });
18
+ emit({ type: 'change', table: row._meta.table, rowId: row._meta.rowId, row });
19
19
  }
20
20
  }
21
21
  }
@@ -32,8 +32,23 @@ export async function pull(ctx) {
32
32
  const cursor = typeof cursorRaw === 'string' ? cursorRaw : '';
33
33
  // Fast path: if global head hasn't advanced past cursor, skip listing.
34
34
  const head = await readJsonIfExists(adapter, p.changesHead);
35
+ emit({
36
+ type: 'trace:head',
37
+ op: 'read',
38
+ reason: 'pull-fast-path',
39
+ path: p.changesHead,
40
+ priorHlc: head?.latestHlc ?? null,
41
+ });
35
42
  if (head?.latestHlc && cursor && hlcCompareStr(head.latestHlc, cursor) <= 0) {
36
43
  log('debug', 'pull() — head unchanged, skipping');
44
+ emit({
45
+ type: 'trace:head',
46
+ op: 'skip-no-change',
47
+ reason: 'pull-fast-path',
48
+ path: p.changesHead,
49
+ priorHlc: head.latestHlc,
50
+ nextHlc: cursor,
51
+ });
37
52
  emit({ type: 'sync:complete', entriesMerged: 0 });
38
53
  return hlc;
39
54
  }
@@ -78,6 +93,7 @@ export async function pull(ctx) {
78
93
  }
79
94
  }
80
95
  catch (err) {
96
+ emit({ type: 'decode:error', error: err instanceof Error ? err : new Error(String(err)), path: file.path, context: { stage: 'pull', name: file.name } });
81
97
  throw await ctx.poisonRemote(err, file.path);
82
98
  }
83
99
  }
@@ -95,4 +111,3 @@ export async function pull(ctx) {
95
111
  throw err;
96
112
  }
97
113
  }
98
- //# sourceMappingURL=pull.js.map
@@ -9,4 +9,3 @@ export function createRowId(options = {}) {
9
9
  : `${randomHex(8)}-${randomHex(4)}-${randomHex(4)}-${randomHex(4)}-${randomHex(12)}`;
10
10
  return options.prefix ? `${options.prefix}_${base}` : base;
11
11
  }
12
- //# sourceMappingURL=row-id.js.map
@@ -1,15 +1,26 @@
1
- import type { IndexableSchemaField, SchemaField } from './types.ts';
1
+ import type { IndexableSchemaField, OptionalSchemaField, SchemaField } from './types.ts';
2
+ type BaseField<T, K extends import('./types.ts').SchemaFieldKind> = SchemaField<T, K> & {
3
+ readonly optional: OptionalSchemaField<T, K>;
4
+ };
5
+ type BaseIndexableField<T> = IndexableSchemaField<T> & {
6
+ readonly optional: OptionalSchemaField<T, import('./types.ts').IndexableSchemaFieldKind>;
7
+ };
2
8
  export declare const types: {
3
- string: IndexableSchemaField<string>;
4
- number: IndexableSchemaField<number>;
5
- boolean: IndexableSchemaField<boolean>;
6
- date: IndexableSchemaField<Date>;
7
- Date: IndexableSchemaField<Date>;
8
- json: SchemaField<unknown>;
9
+ string: BaseIndexableField<string>;
10
+ number: BaseIndexableField<number>;
11
+ boolean: BaseIndexableField<boolean>;
12
+ date: BaseIndexableField<Date>;
13
+ Date: BaseIndexableField<Date>;
14
+ json: BaseField<unknown, "json">;
9
15
  /** Type a JSON field explicitly: `types.typed<MyType[]>('json')` */
10
- typed<T>(kind: "json"): SchemaField<T>;
11
- enum<const T extends string>(..._values: T[]): IndexableSchemaField<T>;
12
- index<T>(field: IndexableSchemaField<T>): IndexableSchemaField<T>;
13
- unique<T>(field: IndexableSchemaField<T>): IndexableSchemaField<T>;
16
+ typed<T>(kind: "json"): BaseField<T, "json">;
17
+ enum<const T extends string>(..._values: T[]): BaseIndexableField<T>;
18
+ index<T>(field: IndexableSchemaField<T> & {
19
+ __optional?: never;
20
+ }): IndexableSchemaField<T>;
21
+ unique<T>(field: IndexableSchemaField<T> & {
22
+ __optional?: never;
23
+ }): IndexableSchemaField<T>;
14
24
  };
25
+ export {};
15
26
  //# sourceMappingURL=schema-types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema-types.d.ts","sourceRoot":"","sources":["../../src/core/schema-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEpE,eAAO,MAAM,KAAK;YACgB,oBAAoB,CAAC,MAAM,CAAC;YAC5B,oBAAoB,CAAC,MAAM,CAAC;aAC5B,oBAAoB,CAAC,OAAO,CAAC;UAC7B,oBAAoB,CAAC,IAAI,CAAC;UAC1B,oBAAoB,CAAC,IAAI,CAAC;UAC1B,WAAW,CAAC,OAAO,CAAC;IAEpD,oEAAoE;UAC9D,CAAC,QAAQ,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC;eAI3B,CAAC,SAAS,MAAM,cAAc,CAAC,EAAE,GAAG,oBAAoB,CAAC,CAAC,CAAC;UAIhE,CAAC,SAAS,oBAAoB,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC;WAI1D,CAAC,SAAS,oBAAoB,CAAC,CAAC,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC;CAGnE,CAAC"}
1
+ {"version":3,"file":"schema-types.d.ts","sourceRoot":"","sources":["../../src/core/schema-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzF,KAAK,SAAS,CAAC,CAAC,EAAE,CAAC,SAAS,OAAO,YAAY,EAAE,eAAe,IAAI,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IACtF,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC9C,CAAC;AAEF,KAAK,kBAAkB,CAAC,CAAC,IAAI,oBAAoB,CAAC,CAAC,CAAC,GAAG;IACrD,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC,EAAE,OAAO,YAAY,EAAE,wBAAwB,CAAC,CAAC;CAC1F,CAAC;AAaF,eAAO,MAAM,KAAK;YAC8D,kBAAkB,CAAC,MAAM,CAAC;YAC1B,kBAAkB,CAAC,MAAM,CAAC;aACzB,kBAAkB,CAAC,OAAO,CAAC;UAC9B,kBAAkB,CAAC,IAAI,CAAC;UACxB,kBAAkB,CAAC,IAAI,CAAC;UAC9B,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;IAEhG,oEAAoE;UAC9D,CAAC,QAAQ,MAAM,GAAG,SAAS,CAAC,CAAC,EAAE,MAAM,CAAC;eAIjC,CAAC,SAAS,MAAM,cAAc,CAAC,EAAE,GAAG,kBAAkB,CAAC,CAAC,CAAC;UAI9D,CAAC,SAAS,oBAAoB,CAAC,CAAC,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,KAAK,CAAA;KAAE,GAAG,oBAAoB,CAAC,CAAC,CAAC;WAInF,CAAC,SAAS,oBAAoB,CAAC,CAAC,CAAC,GAAG;QAAE,UAAU,CAAC,EAAE,KAAK,CAAA;KAAE,GAAG,oBAAoB,CAAC,CAAC,CAAC;CAG5F,CAAC"}
@@ -1,16 +1,26 @@
1
+ function withOptional(field) {
2
+ const base = field;
3
+ Object.defineProperty(base, 'optional', {
4
+ value: { ...field, optional: true, __optional: true },
5
+ enumerable: true,
6
+ configurable: false,
7
+ writable: false,
8
+ });
9
+ return base;
10
+ }
1
11
  export const types = {
2
- string: { kind: 'string' },
3
- number: { kind: 'number' },
4
- boolean: { kind: 'boolean' },
5
- date: { kind: 'date' },
6
- Date: { kind: 'date' },
7
- json: { kind: 'json' },
12
+ string: withOptional({ kind: 'string' }),
13
+ number: withOptional({ kind: 'number' }),
14
+ boolean: withOptional({ kind: 'boolean' }),
15
+ date: withOptional({ kind: 'date' }),
16
+ Date: withOptional({ kind: 'date' }),
17
+ json: withOptional({ kind: 'json' }),
8
18
  /** Type a JSON field explicitly: `types.typed<MyType[]>('json')` */
9
19
  typed(kind) {
10
- return { kind };
20
+ return withOptional({ kind });
11
21
  },
12
22
  enum(..._values) {
13
- return { kind: 'enum' };
23
+ return withOptional({ kind: 'enum' });
14
24
  },
15
25
  index(field) {
16
26
  return { ...field, index: true };
@@ -19,4 +29,3 @@ export const types = {
19
29
  return { ...field, index: true, unique: true };
20
30
  },
21
31
  };
22
- //# sourceMappingURL=schema-types.js.map
@@ -139,11 +139,18 @@ const _badConfig = {
139
139
  },
140
140
  };
141
141
  void _badConfig;
142
- // table() with known key → Table<{ title: string; status: 'open'|'done'; priority: number }>
143
142
  const tasksTable = typedEngine.table('tasks');
144
- // _TasksGet should be { title: string; ... } | undefined — not Record<string,unknown>
145
- const _checkGet = { title: 'x', status: 'open', priority: 1 };
146
- void _checkGet;
143
+ const _addOk = { title: 'x', status: 'open', priority: 1 };
144
+ // @ts-expect-error missing required field status
145
+ const _addBad = { title: 'x', priority: 1 };
146
+ const _replaceOk = { title: 'x', status: 'open', priority: 1 };
147
+ // @ts-expect-error — missing required field priority
148
+ const _replaceBad = { title: 'x', status: 'open' };
149
+ void _addOk;
150
+ void _replaceOk;
151
+ // _TasksRow should be { title: string; ... } | undefined — not Record<string,unknown>
152
+ const _checkRow = { title: 'x', status: 'open', priority: 1 };
153
+ void _checkRow;
147
154
  // @ts-expect-error — 'nonexistent' is not keyof DB (no fallback overload)
148
155
  typedEngine.table('nonexistent');
149
156
  // ─── Regression: satisfies DatabaseSchemaDefinition infers correctly ─
@@ -173,4 +180,51 @@ void _r;
173
180
  // @ts-expect-error — number not assignable to string
174
181
  const _badWp = { weekId: 42, plan: {}, createdAt: new Date() };
175
182
  void _badWp;
176
- //# sourceMappingURL=schema-types.type-test.js.map
183
+ // ─── _type phantom: no undefined bleeding into field types ───────────
184
+ const _weekId = 'hello'; // string not string|undefined
185
+ const _date = new Date(); // Date not Date|undefined
186
+ const _num = 42; // number
187
+ // @ts-expect-error — string is not number
188
+ const _badNum = 'x';
189
+ // ─── Optional fields ─────────────────────────────────────────────────
190
+ const _optionalStringField = types.string.optional;
191
+ const _optionalJsonField = types.typed('json').optional;
192
+ // @ts-expect-error — optional fields cannot be indexed
193
+ const _optionalIndexedString = types.index(types.string.optional);
194
+ // @ts-expect-error — optional fields cannot be unique
195
+ const _optionalUniqueString = types.unique(types.string.optional);
196
+ const optionalSchema = {
197
+ version: 1,
198
+ tables: {
199
+ tasks: {
200
+ fields: {
201
+ title: types.string,
202
+ note: types.string.optional,
203
+ payload: types.typed('json').optional,
204
+ },
205
+ },
206
+ },
207
+ };
208
+ const _optionalOk1 = { title: 'x' };
209
+ const _noteRead = _optRow.note;
210
+ // @ts-expect-error — optional field is string | undefined, not string
211
+ const _noteStrict = _optRow.note;
212
+ void _noteRead;
213
+ void _noteStrict;
214
+ const _optionalOk2 = { title: 'x', note: 'hello', payload: { foo: 'bar' } };
215
+ // @ts-expect-error — title required
216
+ const _optionalBad1 = { note: 'hello' };
217
+ // @ts-expect-error — note must be string when present
218
+ const _optionalBad2 = { title: 'x', note: 42 };
219
+ // @ts-expect-error — payload must match typed JSON payload
220
+ const _optionalBad3 = { title: 'x', payload: { foo: 42 } };
221
+ void _optionalStringField;
222
+ void _optionalJsonField;
223
+ void _optionalIndexedString;
224
+ void _optionalUniqueString;
225
+ void _optionalOk1;
226
+ void _optionalOk2;
227
+ void _weekId;
228
+ void _date;
229
+ void _num;
230
+ void _badNum;