@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
@@ -14,6 +14,12 @@ export class CloudflareAdapter {
14
14
  constructor(config) {
15
15
  this.name = 'cloudflare';
16
16
  this.authenticated = false;
17
+ // Per-session cache of folders we have already ensured. Cloudflare's
18
+ // /ensure-folder is idempotent but a POST per folder per connect is
19
+ // pure round-trip overhead — Interocitor.connect re-runs the same
20
+ // 4-folder loop on every reload. Cache wipes via `resetFolderCache()`
21
+ // (mesh swap / poison) or process exit.
22
+ this.ensuredFolders = new Set();
17
23
  this.config = { ...config, baseUrl: config.baseUrl.replace(/\/$/, '') };
18
24
  }
19
25
  headers(extra) {
@@ -23,23 +29,33 @@ export class CloudflareAdapter {
23
29
  }
24
30
  return { ...base, ...extra };
25
31
  }
26
- get ioBaseUrl() {
27
- const u = new URL(this.config.baseUrl);
32
+ parseBaseUrl() {
33
+ const base = /^https?:\/\//i.test(this.config.baseUrl)
34
+ ? this.config.baseUrl
35
+ : new URL(this.config.baseUrl, 'http://interocitor').toString();
36
+ const u = new URL(base);
28
37
  if (!u.pathname.includes('/io/')) {
29
38
  throw new Error('CloudflareAdapter baseUrl must include /io/<prefix>');
30
39
  }
40
+ return u;
41
+ }
42
+ get ioBaseUrl() {
43
+ const u = this.parseBaseUrl();
44
+ if (!/^https?:\/\//i.test(this.config.baseUrl)) {
45
+ return `${u.pathname}${u.search}${u.hash}`.replace(/\/$/, '');
46
+ }
31
47
  return u.toString().replace(/\/$/, '');
32
48
  }
33
49
  get notifyUrl() {
34
- const u = new URL(this.config.baseUrl);
35
- if (!u.pathname.includes('/io/')) {
36
- throw new Error('CloudflareAdapter baseUrl must include /io/<prefix>');
37
- }
50
+ const u = this.parseBaseUrl();
38
51
  u.pathname = u.pathname.replace('/io/', '/notify/');
39
52
  u.protocol = u.protocol === 'https:' ? 'wss:' : 'ws:';
40
53
  if (this.config.token) {
41
54
  u.searchParams.set('access_token', this.config.token);
42
55
  }
56
+ if (!/^https?:\/\//i.test(this.config.baseUrl)) {
57
+ return `${u.pathname}${u.search}${u.hash}`.replace(/\/$/, '');
58
+ }
43
59
  return u.toString().replace(/\/$/, '');
44
60
  }
45
61
  ioUrl(pathname) {
@@ -87,8 +103,8 @@ export class CloudflareAdapter {
87
103
  try {
88
104
  ws = new WebSocket(this.notifyUrl);
89
105
  }
90
- catch {
91
- hooks?.onError?.();
106
+ catch (error) {
107
+ hooks?.onError?.(error);
92
108
  scheduleReconnect();
93
109
  return;
94
110
  }
@@ -107,12 +123,14 @@ export class CloudflareAdapter {
107
123
  onInvalidate({ type: 'unknown', path: '/', ts: Date.now() });
108
124
  }
109
125
  };
110
- ws.onerror = () => {
111
- hooks?.onError?.();
126
+ ws.onerror = (event) => {
127
+ hooks?.onError?.(event);
112
128
  };
113
129
  ws.onclose = () => {
114
- if (!cancelled)
130
+ if (!cancelled) {
131
+ hooks?.onClose?.();
115
132
  scheduleReconnect();
133
+ }
116
134
  };
117
135
  };
118
136
  const scheduleReconnect = () => {
@@ -132,6 +150,8 @@ export class CloudflareAdapter {
132
150
  };
133
151
  }
134
152
  async ensureFolder(path) {
153
+ if (this.ensuredFolders.has(path))
154
+ return;
135
155
  const res = await fetch(this.ioUrl('/ensure-folder'), {
136
156
  method: 'POST',
137
157
  headers: this.headers({ 'Content-Type': 'application/json; charset=utf-8' }),
@@ -140,6 +160,13 @@ export class CloudflareAdapter {
140
160
  if (!res.ok && res.status !== 405) {
141
161
  throw new Error(`Failed to ensure folder ${path}: HTTP ${res.status}`);
142
162
  }
163
+ this.ensuredFolders.add(path);
164
+ }
165
+ /** Drop the per-session ensureFolder cache. Call after mesh swap, poison,
166
+ * or any state where a previous "this folder exists" observation must
167
+ * not be trusted. */
168
+ resetFolderCache() {
169
+ this.ensuredFolders.clear();
143
170
  }
144
171
  async listFiles(path) {
145
172
  const res = await fetch(this.ioUrl('/list-files'), {
@@ -224,4 +251,3 @@ export class CloudflareAdapter {
224
251
  };
225
252
  }
226
253
  }
227
- //# sourceMappingURL=cloudflare.js.map
@@ -22,7 +22,7 @@ interface GoogleDriveConfig {
22
22
  * @example
23
23
  * ```ts
24
24
  * const adapter = new GoogleDriveAdapter({ clientId: 'YOUR_GOOGLE_CLIENT_ID' });
25
- * const engine = new SyncEngine(adapter, { remotePath: '/MyApp' });
25
+ * const engine = new Interocitor(adapter, { remotePath: '/MyApp' });
26
26
  * ```
27
27
  */
28
28
  export declare class GoogleDriveAdapter implements StorageAdapter {
@@ -19,7 +19,7 @@ const UPLOAD_API = 'https://www.googleapis.com/upload/drive/v3';
19
19
  * @example
20
20
  * ```ts
21
21
  * const adapter = new GoogleDriveAdapter({ clientId: 'YOUR_GOOGLE_CLIENT_ID' });
22
- * const engine = new SyncEngine(adapter, { remotePath: '/MyApp' });
22
+ * const engine = new Interocitor(adapter, { remotePath: '/MyApp' });
23
23
  * ```
24
24
  */
25
25
  export class GoogleDriveAdapter {
@@ -337,4 +337,3 @@ export class GoogleDriveAdapter {
337
337
  this.folderIdCache.clear();
338
338
  }
339
339
  }
340
- //# sourceMappingURL=google-drive.js.map
@@ -14,7 +14,7 @@ import type { StorageAdapter, FileEntry } from '../core/types.ts';
14
14
  * @example
15
15
  * ```ts
16
16
  * const adapter = new MemoryAdapter();
17
- * const engine = new SyncEngine(adapter, { remotePath: '/Demo' });
17
+ * const engine = new Interocitor(adapter, { remotePath: '/Demo' });
18
18
  * ```
19
19
  */
20
20
  export declare class MemoryAdapter implements StorageAdapter {
@@ -22,6 +22,7 @@ export declare class MemoryAdapter implements StorageAdapter {
22
22
  private files;
23
23
  private folders;
24
24
  private authenticated;
25
+ private ensuredFolders;
25
26
  authenticate(): Promise<void>;
26
27
  /**
27
28
  * Memory adapter has no real backend — returns an empty config.
@@ -30,6 +31,8 @@ export declare class MemoryAdapter implements StorageAdapter {
30
31
  getHandshakeConfig(): string;
31
32
  isAuthenticated(): boolean;
32
33
  ensureFolder(path: string): Promise<void>;
34
+ /** Drop the per-session ensureFolder cache. Tests / mesh-swap callers. */
35
+ resetFolderCache(): void;
33
36
  listFiles(folderPath: string): Promise<FileEntry[]>;
34
37
  /** List immediate subfolder names under a path. */
35
38
  listFolders(folderPath: string): Promise<string[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,IAAI,YAAY;IAEzB,OAAO,CAAC,KAAK,CAAsE;IACnF,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,aAAa,CAAS;IAExB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAInC;;;OAGG;IACH,kBAAkB,IAAI,MAAM;IAI5B,eAAe,IAAI,OAAO;IAIpB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzC,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAsBzD,mDAAmD;IAC7C,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAuBlD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAM3C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAY9D,kDAAkD;IAClD,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAS9B,oCAAoC;IACpC,KAAK,IAAI,IAAI;CAId"}
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElE;;;;;;;;;;;GAWG;AACH,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,IAAI,YAAY;IAEzB,OAAO,CAAC,KAAK,CAAsE;IACnF,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,aAAa,CAAS;IAK9B,OAAO,CAAC,cAAc,CAA0B;IAE1C,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAInC;;;OAGG;IACH,kBAAkB,IAAI,MAAM;IAI5B,eAAe,IAAI,OAAO;IAIpB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/C,0EAA0E;IAC1E,gBAAgB,IAAI,IAAI;IAIlB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IAsBzD,mDAAmD;IAC7C,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAuBlD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAM3C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAY9D,kDAAkD;IAClD,IAAI,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAS9B,oCAAoC;IACpC,KAAK,IAAI,IAAI;CAId"}
@@ -13,7 +13,7 @@
13
13
  * @example
14
14
  * ```ts
15
15
  * const adapter = new MemoryAdapter();
16
- * const engine = new SyncEngine(adapter, { remotePath: '/Demo' });
16
+ * const engine = new Interocitor(adapter, { remotePath: '/Demo' });
17
17
  * ```
18
18
  */
19
19
  export class MemoryAdapter {
@@ -22,6 +22,11 @@ export class MemoryAdapter {
22
22
  this.files = new Map();
23
23
  this.folders = new Set();
24
24
  this.authenticated = false;
25
+ // Mirrors the cloud-adapter convention: cache "ensured" paths so a
26
+ // re-`ensureFolder` is a no-op. Memory adapter is cheap, but keeping
27
+ // the same shape lets tests assert call-count parity with the real
28
+ // adapters (cloudflare, webdav).
29
+ this.ensuredFolders = new Set();
25
30
  }
26
31
  async authenticate() {
27
32
  this.authenticated = true;
@@ -37,7 +42,14 @@ export class MemoryAdapter {
37
42
  return this.authenticated;
38
43
  }
39
44
  async ensureFolder(path) {
45
+ if (this.ensuredFolders.has(path))
46
+ return;
40
47
  this.folders.add(path);
48
+ this.ensuredFolders.add(path);
49
+ }
50
+ /** Drop the per-session ensureFolder cache. Tests / mesh-swap callers. */
51
+ resetFolderCache() {
52
+ this.ensuredFolders.clear();
41
53
  }
42
54
  async listFiles(folderPath) {
43
55
  const prefix = folderPath.endsWith('/') ? folderPath : folderPath + '/';
@@ -126,4 +138,3 @@ export class MemoryAdapter {
126
138
  this.folders.clear();
127
139
  }
128
140
  }
129
- //# sourceMappingURL=memory.js.map
@@ -34,6 +34,7 @@ export declare class WebDAVAdapter implements StorageAdapter {
34
34
  readonly name = "webdav";
35
35
  private config;
36
36
  private authenticated;
37
+ private ensuredFolders;
37
38
  constructor(config: WebDAVConfig);
38
39
  private url;
39
40
  private authHeader;
@@ -47,6 +48,10 @@ export declare class WebDAVAdapter implements StorageAdapter {
47
48
  getHandshakeConfig(): string;
48
49
  isAuthenticated(): boolean;
49
50
  ensureFolder(path: string): Promise<void>;
51
+ /** Drop the per-session ensureFolder cache. Call after mesh swap, poison,
52
+ * or any state where we cannot trust a previous "this folder exists"
53
+ * observation. */
54
+ resetFolderCache(): void;
50
55
  listFiles(folderPath: string): Promise<FileEntry[]>;
51
56
  private parsePropfindResponse;
52
57
  listFolders(folderPath: string): Promise<string[]>;
@@ -1 +1 @@
1
- {"version":3,"file":"webdav.d.ts","sourceRoot":"","sources":["../../src/adapters/webdav.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElE,UAAU,YAAY;IACpB,sGAAsG;IACtG,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAClE;AAED;;;;;;;;;;GAUG;AACH,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,IAAI,YAAY;IAEzB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,aAAa,CAAS;gBAElB,MAAM,EAAE,YAAY;IAIhC,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,OAAO;IAOT,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBnC;;;;OAIG;IACH,kBAAkB,IAAI,MAAM;IAI5B,eAAe,IAAI,OAAO;IAIpB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBzC,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA0BzD,OAAO,CAAC,qBAAqB;IAoCvB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmClD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAY3C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;CAyC/D"}
1
+ {"version":3,"file":"webdav.d.ts","sourceRoot":"","sources":["../../src/adapters/webdav.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAElE,UAAU,YAAY;IACpB,sGAAsG;IACtG,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAClE;AAED;;;;;;;;;;GAUG;AACH,qBAAa,aAAc,YAAW,cAAc;IAClD,QAAQ,CAAC,IAAI,YAAY;IAEzB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,aAAa,CAAS;IAM9B,OAAO,CAAC,cAAc,CAA0B;gBAEpC,MAAM,EAAE,YAAY;IAIhC,OAAO,CAAC,GAAG;IAMX,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,OAAO;IAOT,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAgBnC;;;;OAIG;IACH,kBAAkB,IAAI,MAAM;IAI5B,eAAe,IAAI,OAAO;IAIpB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAsB/C;;uBAEmB;IACnB,gBAAgB,IAAI,IAAI;IAIlB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA0BzD,OAAO,CAAC,qBAAqB;IAoCvB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmClD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAY3C,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBjE,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYvC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;CAyC/D"}
@@ -22,6 +22,12 @@ export class WebDAVAdapter {
22
22
  constructor(config) {
23
23
  this.name = 'webdav';
24
24
  this.authenticated = false;
25
+ // Per-session cache of folders we have already MKCOL'd. WebDAV folders
26
+ // are stable for the life of the mesh; a caller (Interocitor.connect)
27
+ // re-runs the same 4-folder ensureFolder loop on every reload, which
28
+ // costs 4 sequential MKCOL round-trips for no benefit. Cache wipes on
29
+ // explicit `resetFolderCache()` (mesh swap / poison) or process exit.
30
+ this.ensuredFolders = new Set();
25
31
  this.config = config;
26
32
  }
27
33
  url(path) {
@@ -70,10 +76,14 @@ export class WebDAVAdapter {
70
76
  return this.authenticated;
71
77
  }
72
78
  async ensureFolder(path) {
79
+ if (this.ensuredFolders.has(path))
80
+ return;
73
81
  const parts = path.split('/').filter(Boolean);
74
82
  let current = '';
75
83
  for (const part of parts) {
76
84
  current += '/' + part;
85
+ if (this.ensuredFolders.has(current))
86
+ continue; // ancestor cached
77
87
  const res = await fetch(this.url(current), {
78
88
  method: 'MKCOL',
79
89
  headers: this.headers(),
@@ -82,7 +92,15 @@ export class WebDAVAdapter {
82
92
  if (!res.ok && res.status !== 405) {
83
93
  throw new Error(`Failed to create folder ${current}: ${res.status}`);
84
94
  }
95
+ this.ensuredFolders.add(current);
85
96
  }
97
+ this.ensuredFolders.add(path);
98
+ }
99
+ /** Drop the per-session ensureFolder cache. Call after mesh swap, poison,
100
+ * or any state where we cannot trust a previous "this folder exists"
101
+ * observation. */
102
+ resetFolderCache() {
103
+ this.ensuredFolders.clear();
86
104
  }
87
105
  async listFiles(folderPath) {
88
106
  const res = await fetch(this.url(folderPath), {
@@ -244,4 +262,3 @@ export class WebDAVAdapter {
244
262
  };
245
263
  }
246
264
  }
247
- //# sourceMappingURL=webdav.js.map
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Codec — encryption/decryption of change and snapshot payloads.
3
3
  *
4
- * Extracted from SyncEngine to keep the orchestrator lean.
4
+ * Extracted from Interocitor to keep the orchestrator lean.
5
5
  * Not part of the public API.
6
6
  */
7
7
  import type { ChangeEntry, Manifest, Snapshot, LocalStoreAdapter } from './types.ts';
@@ -1 +1 @@
1
- {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/core/codec.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,QAAQ,EAGR,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,SAAS,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG1F;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGtF;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,iBAAiB,EACxB,QAAQ,EAAE,QAAQ,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAQhG;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,WAAW,CAAC,CAUtB;AAED,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAQlG;AAED,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,QAAQ,CAAC,CAUnB"}
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/core/codec.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,QAAQ,EACR,QAAQ,EAGR,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAGpB,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,SAAS,GAAG,IAAI,CAAC;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC3B;AAED,wBAAsB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG1F;AAED,wBAAsB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BtF;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,iBAAiB,EACxB,QAAQ,EAAE,QAAQ,GAAG,IAAI,EACzB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAQhG;AAED,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,WAAW,CAAC,CAgBtB;AAED,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAQlG;AAED,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,QAAQ,CAAC,CAgBnB"}
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Codec — encryption/decryption of change and snapshot payloads.
3
3
  *
4
- * Extracted from SyncEngine to keep the orchestrator lean.
4
+ * Extracted from Interocitor to keep the orchestrator lean.
5
5
  * Not part of the public API.
6
6
  */
7
7
  import { encryptEntry, decryptEntry } from "../crypto/encryption.js";
@@ -13,7 +13,36 @@ export async function encodeForCloud(state, plaintext) {
13
13
  export async function decodeFromCloud(state, data) {
14
14
  if (!state.encrypted || !state.encryptionKey)
15
15
  return data;
16
- return decryptEntry(state.encryptionKey, data);
16
+ try {
17
+ return await decryptEntry(state.encryptionKey, data);
18
+ }
19
+ catch (err) {
20
+ // Re-throw with explicit context. Decode errors at this layer mean the
21
+ // active key cannot decrypt this payload — either the wrong key was
22
+ // loaded, or the payload was written under a different key (mesh swap,
23
+ // passphrase rotated, two devices bound to same dbName but different
24
+ // passphrases). Caller wraps this in poisonRemote with the file path.
25
+ const reason = err instanceof Error ? err.message : String(err);
26
+ let keyFingerprint = '<unknown>';
27
+ try {
28
+ const raw = await crypto.subtle.exportKey('raw', state.encryptionKey);
29
+ const hash = await crypto.subtle.digest('SHA-256', raw);
30
+ const bytes = new Uint8Array(hash);
31
+ const hex = Array.from(bytes.slice(0, 6)).map(b => b.toString(16).padStart(2, '0')).join('');
32
+ keyFingerprint = `sha256-${hex}`;
33
+ }
34
+ catch { /* ignore */ }
35
+ console.log('[interocitor:decode] decodeFromCloud() — DECRYPT FAIL', {
36
+ keyFingerprint,
37
+ meshId: state.manifest?.meshId,
38
+ payloadFirst32: data.slice(0, 32),
39
+ payloadLen: data.length,
40
+ reason,
41
+ });
42
+ const e = new Error(`Decryption failed: payload not decryptable with the active mesh key (${reason}). The remote was likely written under a different key/mesh.`);
43
+ e.cause = err;
44
+ throw e;
45
+ }
17
46
  }
18
47
  export async function assertExpectedMeshId(local, manifest, meshId) {
19
48
  if (!meshId) {
@@ -40,6 +69,10 @@ export async function encodeChangePayload(state, entry) {
40
69
  export async function decodeChangePayload(state, local, data, path) {
41
70
  const decoded = await decodeFromCloud(state, data);
42
71
  const payload = JSON.parse(decoded);
72
+ const payloadRecord = payload;
73
+ if ('mesh' in payloadRecord && !('meshId' in payloadRecord)) {
74
+ throw new Error(`Remote change payload has invalid shape: ${path}`);
75
+ }
43
76
  if (payload.kind !== 'change' || !payload.entry) {
44
77
  throw new Error(`Remote change payload has invalid shape: ${path}`);
45
78
  }
@@ -57,10 +90,13 @@ export async function encodeSnapshotPayload(state, snapshot) {
57
90
  export async function decodeSnapshotPayload(state, local, data, path) {
58
91
  const decoded = await decodeFromCloud(state, data);
59
92
  const payload = JSON.parse(decoded);
93
+ const payloadRecord = payload;
94
+ if ('mesh' in payloadRecord && !('meshId' in payloadRecord)) {
95
+ throw new Error(`Remote snapshot payload has invalid shape: ${path}`);
96
+ }
60
97
  if (payload.kind !== 'snapshot' || !payload.snapshot) {
61
98
  throw new Error(`Remote snapshot payload has invalid shape: ${path}`);
62
99
  }
63
100
  await assertExpectedMeshId(local, state.manifest, String(payload.meshId || ''));
64
101
  return payload.snapshot;
65
102
  }
66
- //# sourceMappingURL=codec.js.map
@@ -1,7 +1,14 @@
1
1
  /**
2
2
  * Compaction — snapshot + manifest rotation + change file pruning.
3
3
  *
4
- * Extracted from SyncEngine. Not part of the public API.
4
+ * Concurrency note:
5
+ * - No built-in lock or CAS on manifest pointer.
6
+ * - Concurrent compaction can race and overwrite the pointer.
7
+ * - Recommended: acquire a remote lease (e.g. mainline/compact-lock.json)
8
+ * and abort if another compactor is active, or if manifest generation
9
+ * changes after the lease is acquired.
10
+ *
11
+ * Extracted from Interocitor. Not part of the public API.
5
12
  */
6
13
  import type { StorageAdapter, LocalStoreAdapter, Manifest, Row, SyncEvent } from './types.ts';
7
14
  import type { HLC } from './types.ts';
@@ -17,6 +24,7 @@ export interface CompactContext {
17
24
  serverId: string;
18
25
  emit: (event: SyncEvent) => void;
19
26
  pull: () => Promise<void>;
27
+ offlineGraceMs?: number;
20
28
  }
21
29
  export declare function compact(ctx: CompactContext): Promise<Manifest>;
22
30
  export interface RehydrateContext {
@@ -1 +1 @@
1
- {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,QAAQ,EAGR,GAAG,EACH,SAAS,EACV,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAItC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,wBAAsB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAuFpE;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,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,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACjC,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAChE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,iDAAiD;AACjD,wBAAsB,SAAS,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,CAgDnE"}
1
+ {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,QAAQ,EAGR,GAAG,EACH,SAAS,EAEV,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAItC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,UAAU,EAAE,UAAU,CAAC;IACvB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACjC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAkCD,wBAAsB,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsGpE;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,iBAAiB,CAAC;IACzB,UAAU,EAAE,UAAU,CAAC;IACvB,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1B,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,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACjC,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAChE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,iDAAiD;AACjD,wBAAsB,SAAS,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,CAiDnE"}
@@ -1,13 +1,56 @@
1
1
  /**
2
2
  * Compaction — snapshot + manifest rotation + change file pruning.
3
3
  *
4
- * Extracted from SyncEngine. Not part of the public API.
4
+ * Concurrency note:
5
+ * - No built-in lock or CAS on manifest pointer.
6
+ * - Concurrent compaction can race and overwrite the pointer.
7
+ * - Recommended: acquire a remote lease (e.g. mainline/compact-lock.json)
8
+ * and abort if another compactor is active, or if manifest generation
9
+ * changes after the lease is acquired.
10
+ *
11
+ * Extracted from Interocitor. Not part of the public API.
5
12
  */
6
13
  import { hlcSerialize, hlcCompareStr } from "./hlc.js";
7
14
  import { paths, textEncoder, textDecoder, generateId, computeContentHash, log } from "./internals.js";
8
15
  import { encodeSnapshotPayload, decodeSnapshotPayload } from "./codec.js";
9
- import { writeJson } from "./manifest.js";
16
+ import { readJsonIfExists, writeJson } from "./manifest.js";
10
17
  import { hlcParse } from "./hlc.js";
18
+ async function computeGcFloor(ctx, nowMs) {
19
+ const p = paths(ctx.remotePath);
20
+ const graceMs = ctx.offlineGraceMs ?? 7 * 24 * 60 * 60000;
21
+ const cutoffMs = nowMs - graceMs;
22
+ const floors = [];
23
+ try {
24
+ const files = await ctx.adapter.listFiles(p.devicesFolder);
25
+ for (const file of files) {
26
+ if (!file.name.endsWith('.json'))
27
+ continue;
28
+ const deviceId = file.name.slice(0, -'.json'.length);
29
+ const meta = await readJsonIfExists(ctx.adapter, p.deviceFile(deviceId));
30
+ if (!meta || meta.retired)
31
+ continue;
32
+ const lastSeen = Date.parse(meta.lastSeenAt || '');
33
+ if (Number.isFinite(lastSeen) && lastSeen < cutoffMs)
34
+ continue;
35
+ // Active devices that have not yet acknowledged a watermark block
36
+ // advancement. They are still inside the offline grace window.
37
+ if (!meta.observedWatermarkHlc)
38
+ return ctx.manifest.gcFloorHlc ?? '';
39
+ floors.push(meta.observedWatermarkHlc);
40
+ }
41
+ }
42
+ catch {
43
+ return ctx.manifest.gcFloorHlc ?? '';
44
+ }
45
+ if (floors.length === 0)
46
+ return ctx.manifest.gcFloorHlc ?? '';
47
+ floors.sort(hlcCompareStr);
48
+ const candidate = floors[0];
49
+ const existing = ctx.manifest.gcFloorHlc ?? '';
50
+ if (existing && hlcCompareStr(existing, candidate) > 0)
51
+ return existing;
52
+ return candidate;
53
+ }
11
54
  export async function compact(ctx) {
12
55
  const { adapter, local, remotePath, manifest, codecState, deviceId, serverId } = ctx;
13
56
  if (manifest.server.managed && deviceId !== serverId) {
@@ -16,7 +59,9 @@ export async function compact(ctx) {
16
59
  // Ensure the compactor has merged latest remote changes before snapshotting.
17
60
  await ctx.pull();
18
61
  const p = paths(remotePath);
19
- const now = new Date().toISOString();
62
+ const nowDate = new Date();
63
+ const now = nowDate.toISOString();
64
+ const gcFloorHlc = await computeGcFloor(ctx, nowDate.getTime());
20
65
  const nextEpoch = manifest.epoch + 1;
21
66
  const nextGeneration = manifest.generation + 1;
22
67
  const snapshotPath = `${p.mainlineFolder}/snapshot-${nextEpoch}-${serverId}.json`;
@@ -24,9 +69,16 @@ export async function compact(ctx) {
24
69
  const allRows = await local.getAllRows();
25
70
  const snapshotTables = {};
26
71
  for (const row of allRows) {
27
- if (!snapshotTables[row._table])
28
- snapshotTables[row._table] = {};
29
- snapshotTables[row._table][row._rowId] = row;
72
+ if (gcFloorHlc
73
+ && row._meta.deleted
74
+ && row._meta.deletedHlc
75
+ && hlcCompareStr(row._meta.deletedHlc, gcFloorHlc) <= 0) {
76
+ continue;
77
+ }
78
+ const t = row._meta.table;
79
+ if (!snapshotTables[t])
80
+ snapshotTables[t] = {};
81
+ snapshotTables[t][row._meta.rowId] = row;
30
82
  }
31
83
  const snapshot = {
32
84
  snapshotId: generateId('snap'),
@@ -53,6 +105,10 @@ export async function compact(ctx) {
53
105
  watermarkHlc: hlcSerialize(ctx.hlc),
54
106
  snapshotPath,
55
107
  deltaPath: null,
108
+ gcFloorHlc,
109
+ gcEpoch: gcFloorHlc ? nextEpoch : manifest.gcEpoch,
110
+ gcCreatedAt: gcFloorHlc ? now : manifest.gcCreatedAt,
111
+ offlineGraceMs: ctx.offlineGraceMs,
56
112
  };
57
113
  const nextManifest = {
58
114
  ...manifestPayload,
@@ -123,6 +179,7 @@ export async function rehydrate(ctx) {
123
179
  ctx.emit({ type: 'rehydrate:complete', rowCount });
124
180
  }
125
181
  catch (err) {
182
+ ctx.emit({ type: 'decode:error', error: err instanceof Error ? err : new Error(String(err)), path: snapshotPath, context: { stage: 'rehydrate' } });
126
183
  const poisoned = await ctx.poisonRemote(err, snapshotPath);
127
184
  ctx.emit({ type: 'sync:error', error: poisoned });
128
185
  throw poisoned;
@@ -131,4 +188,3 @@ export async function rehydrate(ctx) {
131
188
  await ctx.pull();
132
189
  return hlc;
133
190
  }
134
- //# sourceMappingURL=compaction.js.map
@@ -10,6 +10,10 @@
10
10
  * - custom function — `(local, remote, ctx) => ColumnEntry`
11
11
  *
12
12
  * Deletes are soft (tombstone with HLC) and always use LWW.
13
+ *
14
+ * Row shape (`{_meta, payload}`) keeps user payload fully isolated from
15
+ * engine metadata. The merge loop only touches `row.payload`; `row._meta`
16
+ * is never indexed by user-controlled keys.
13
17
  */
14
18
  import type { Row, Op, ChangeEntry, DatabaseSchemaDefinition } from './types.ts';
15
19
  /**
@@ -22,12 +26,11 @@ export declare function applyOp(tables: Record<string, Record<string, Row>>, op:
22
26
  * Returns list of affected rows.
23
27
  */
24
28
  export declare function applyChangeEntry(tables: Record<string, Record<string, Row>>, entry: ChangeEntry, schemaVersion: number, schema?: DatabaseSchemaDefinition): Row[];
25
- /**
26
- * Read a column value from a row, unwrapping the ColumnEntry.
27
- */
29
+ /** Read a column value from a row, unwrapping the ColumnEntry. */
28
30
  export declare function readColumn(row: Row, column: string): unknown;
29
31
  /**
30
32
  * Build a plain object from a row (strip HLC metadata).
33
+ * Returns user-facing fields with `_meta` projection (table, rowId, deleted).
31
34
  */
32
35
  export declare function rowToPlain(row: Row): Record<string, unknown>;
33
36
  //# sourceMappingURL=crdt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crdt.d.ts","sourceRoot":"","sources":["../../src/core/crdt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,GAAG,EACH,EAAE,EAEF,WAAW,EACX,wBAAwB,EAGzB,MAAM,YAAY,CAAC;AA0EpB;;;GAGG;AACH,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EAC3C,EAAE,EAAE,EAAE,EACN,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,wBAAwB,GAChC,GAAG,GAAG,IAAI,CA4EZ;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EAC3C,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,wBAAwB,GAChC,GAAG,EAAE,CAOP;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAM5D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAa5D"}
1
+ {"version":3,"file":"crdt.d.ts","sourceRoot":"","sources":["../../src/core/crdt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACV,GAAG,EACH,EAAE,EAEF,WAAW,EACX,wBAAwB,EAGzB,MAAM,YAAY,CAAC;AAoEpB;;;GAGG;AACH,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EAC3C,EAAE,EAAE,EAAE,EACN,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,wBAAwB,GAChC,GAAG,GAAG,IAAI,CAuEZ;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,EAC3C,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,wBAAwB,GAChC,GAAG,EAAE,CAOP;AAED,kEAAkE;AAClE,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAG5D;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAU5D"}