@interocitor/core 0.0.0-beta.3 → 0.0.0-beta.4
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/README.md +445 -91
- package/dist/adapters/cloudflare.d.ts +8 -9
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +38 -12
- package/dist/adapters/google-drive.d.ts +1 -1
- package/dist/adapters/google-drive.js +1 -2
- package/dist/adapters/memory.d.ts +4 -1
- package/dist/adapters/memory.d.ts.map +1 -1
- package/dist/adapters/memory.js +13 -2
- package/dist/adapters/webdav.d.ts +5 -0
- package/dist/adapters/webdav.d.ts.map +1 -1
- package/dist/adapters/webdav.js +18 -1
- package/dist/core/codec.d.ts +1 -1
- package/dist/core/codec.d.ts.map +1 -1
- package/dist/core/codec.js +39 -3
- package/dist/core/compaction.d.ts +8 -1
- package/dist/core/compaction.d.ts.map +1 -1
- package/dist/core/compaction.js +13 -5
- package/dist/core/crdt.d.ts +6 -3
- package/dist/core/crdt.d.ts.map +1 -1
- package/dist/core/crdt.js +38 -60
- package/dist/core/errors.d.ts +47 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +61 -0
- package/dist/core/flush.d.ts +3 -3
- package/dist/core/flush.d.ts.map +1 -1
- package/dist/core/flush.js +64 -7
- package/dist/core/hlc.js +0 -1
- package/dist/core/ids.d.ts +49 -0
- package/dist/core/ids.d.ts.map +1 -0
- package/dist/core/ids.js +132 -0
- package/dist/core/internals.d.ts +10 -2
- package/dist/core/internals.d.ts.map +1 -1
- package/dist/core/internals.js +27 -9
- package/dist/core/manifest.d.ts +20 -5
- package/dist/core/manifest.d.ts.map +1 -1
- package/dist/core/manifest.js +65 -11
- package/dist/core/pull.d.ts +1 -1
- package/dist/core/pull.d.ts.map +1 -1
- package/dist/core/pull.js +21 -6
- package/dist/core/row-id.js +0 -1
- package/dist/core/schema-types.d.ts +22 -11
- package/dist/core/schema-types.d.ts.map +1 -1
- package/dist/core/schema-types.js +18 -9
- package/dist/core/schema-types.type-test.js +59 -5
- package/dist/core/sync-engine.d.ts +163 -12
- package/dist/core/sync-engine.d.ts.map +1 -1
- package/dist/core/sync-engine.js +1521 -219
- package/dist/core/table.d.ts +217 -17
- package/dist/core/table.d.ts.map +1 -1
- package/dist/core/table.js +376 -24
- package/dist/core/types.d.ts +382 -17
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +0 -1
- package/dist/crypto/encryption.d.ts.map +1 -1
- package/dist/crypto/encryption.js +6 -1
- package/dist/crypto/keys.js +0 -1
- package/dist/handshake/channel.js +0 -1
- package/dist/handshake/index.d.ts +5 -2
- package/dist/handshake/index.d.ts.map +1 -1
- package/dist/handshake/index.js +19 -2
- package/dist/handshake/qr.js +0 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/storage/credential-store.d.ts +25 -2
- package/dist/storage/credential-store.d.ts.map +1 -1
- package/dist/storage/credential-store.js +55 -8
- package/dist/storage/local-store.d.ts +4 -1
- package/dist/storage/local-store.d.ts.map +1 -1
- package/dist/storage/local-store.js +37 -21
- package/package.json +3 -3
- package/dist/adapters/cloudflare.js.map +0 -1
- package/dist/adapters/google-drive.js.map +0 -1
- package/dist/adapters/memory.js.map +0 -1
- package/dist/adapters/webdav.js.map +0 -1
- package/dist/core/codec.js.map +0 -1
- package/dist/core/compaction.js.map +0 -1
- package/dist/core/crdt.js.map +0 -1
- package/dist/core/flush.js.map +0 -1
- package/dist/core/hlc.js.map +0 -1
- package/dist/core/internals.js.map +0 -1
- package/dist/core/manifest.js.map +0 -1
- package/dist/core/pull.js.map +0 -1
- package/dist/core/row-id.js.map +0 -1
- package/dist/core/schema-types.js.map +0 -1
- package/dist/core/schema-types.type-test.js.map +0 -1
- package/dist/core/sync-engine.js.map +0 -1
- package/dist/core/table.js.map +0 -1
- package/dist/core/types.js.map +0 -1
- package/dist/crypto/encryption.js.map +0 -1
- package/dist/crypto/keys.js.map +0 -1
- package/dist/handshake/channel.js.map +0 -1
- package/dist/handshake/index.js.map +0 -1
- package/dist/handshake/qr.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/storage/credential-store.js.map +0 -1
- 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
|
-
|
|
27
|
-
const
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
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"}
|
package/dist/adapters/memory.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @example
|
|
14
14
|
* ```ts
|
|
15
15
|
* const adapter = new MemoryAdapter();
|
|
16
|
-
* const engine = new
|
|
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;
|
|
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"}
|
package/dist/adapters/webdav.js
CHANGED
|
@@ -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
|
package/dist/core/codec.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codec — encryption/decryption of change and snapshot payloads.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from
|
|
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';
|
package/dist/core/codec.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/core/codec.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Codec — encryption/decryption of change and snapshot payloads.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from
|
|
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
|
-
|
|
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
|
-
*
|
|
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';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/core/compaction.ts"],"names":[],"mappings":"AAAA
|
|
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,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,CAwFpE;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"}
|
package/dist/core/compaction.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compaction — snapshot + manifest rotation + change file pruning.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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";
|
|
@@ -24,9 +31,10 @@ export async function compact(ctx) {
|
|
|
24
31
|
const allRows = await local.getAllRows();
|
|
25
32
|
const snapshotTables = {};
|
|
26
33
|
for (const row of allRows) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
const t = row._meta.table;
|
|
35
|
+
if (!snapshotTables[t])
|
|
36
|
+
snapshotTables[t] = {};
|
|
37
|
+
snapshotTables[t][row._meta.rowId] = row;
|
|
30
38
|
}
|
|
31
39
|
const snapshot = {
|
|
32
40
|
snapshotId: generateId('snap'),
|
|
@@ -123,6 +131,7 @@ export async function rehydrate(ctx) {
|
|
|
123
131
|
ctx.emit({ type: 'rehydrate:complete', rowCount });
|
|
124
132
|
}
|
|
125
133
|
catch (err) {
|
|
134
|
+
ctx.emit({ type: 'decode:error', error: err instanceof Error ? err : new Error(String(err)), path: snapshotPath, context: { stage: 'rehydrate' } });
|
|
126
135
|
const poisoned = await ctx.poisonRemote(err, snapshotPath);
|
|
127
136
|
ctx.emit({ type: 'sync:error', error: poisoned });
|
|
128
137
|
throw poisoned;
|
|
@@ -131,4 +140,3 @@ export async function rehydrate(ctx) {
|
|
|
131
140
|
await ctx.pull();
|
|
132
141
|
return hlc;
|
|
133
142
|
}
|
|
134
|
-
//# sourceMappingURL=compaction.js.map
|
package/dist/core/crdt.d.ts
CHANGED
|
@@ -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
|
package/dist/core/crdt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crdt.d.ts","sourceRoot":"","sources":["../../src/core/crdt.ts"],"names":[],"mappings":"AAAA
|
|
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,CA+DZ;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"}
|