@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
package/dist/core/internals.js
CHANGED
|
@@ -18,31 +18,50 @@ export function paths(root) {
|
|
|
18
18
|
}
|
|
19
19
|
// ─── Logger ──────────────────────────────────────────────────────────
|
|
20
20
|
const LOG_PREFIX = '[interocitor]';
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
const storage = typeof localStorage !== 'undefined' ? localStorage : null;
|
|
52
|
+
let id = storage?.getItem(KEY) ?? null;
|
|
36
53
|
if (!id) {
|
|
37
|
-
id =
|
|
38
|
-
|
|
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
|
package/dist/core/manifest.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Manifest — reading, writing, creating, and validating cloud manifests.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from
|
|
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,22 @@ 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<
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
export declare function createBootstrapManifest(ctx: ManifestContext): 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
|
+
/**
|
|
40
|
+
* When true, skip the read-merge step. Use only when the caller knows
|
|
41
|
+
* no prior device record exists (e.g. immediately after bootstrap of
|
|
42
|
+
* a fresh mesh). Saves one round-trip per connect on first run.
|
|
43
|
+
*/
|
|
44
|
+
bootstrap?: boolean;
|
|
45
|
+
}): Promise<void>;
|
|
31
46
|
//# 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,
|
|
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,GACnB,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,CA0DxD;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;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,GACA,OAAO,CAAC,IAAI,CAAC,CAuBf"}
|
package/dist/core/manifest.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Manifest — reading, writing, creating, and validating cloud manifests.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from
|
|
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,6 +31,7 @@ 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
37
|
export async function createBootstrapManifest(ctx) {
|
|
@@ -60,21 +62,51 @@ export async function createBootstrapManifest(ctx) {
|
|
|
60
62
|
contentHash: await computeContentHash(payload),
|
|
61
63
|
};
|
|
62
64
|
const manifestFile = `manifest-${manifest.generation}.json`;
|
|
63
|
-
|
|
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
|
-
|
|
95
|
+
bootstrapped = true;
|
|
96
|
+
ctx.emit({ type: 'trace:manifest', op: 'bootstrap-create', reason, path: p.manifestPointer });
|
|
97
|
+
const bootstrap = await createBootstrapManifest(ctx);
|
|
98
|
+
// Skip the read-after-write — we just minted both files in this process,
|
|
99
|
+
// they are exactly what's on disk. No GETs needed.
|
|
100
|
+
pointer = bootstrap.pointer;
|
|
101
|
+
manifest = bootstrap.manifest;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
pointer = globalPointer;
|
|
105
|
+
const manifestPath = `${ctx.remotePath}/${pointer.file}`;
|
|
106
|
+
ctx.emit({ type: 'trace:manifest', op: 'read', reason, path: manifestPath, generation: pointer.currentGeneration });
|
|
107
|
+
manifest = await readJson(ctx.adapter, manifestPath);
|
|
74
108
|
}
|
|
75
|
-
const pointer = await readJson(ctx.adapter, p.manifestPointer);
|
|
76
109
|
const manifestPath = `${ctx.remotePath}/${pointer.file}`;
|
|
77
|
-
const manifest = await readJson(ctx.adapter, manifestPath);
|
|
78
110
|
await validateManifestHash(manifest);
|
|
79
111
|
try {
|
|
80
112
|
await assertExpectedMeshId(local, codecState.manifest, manifest.meshId);
|
|
@@ -92,20 +124,42 @@ export async function loadOrCreateManifest(ctx, codecState, local, poisonRemote)
|
|
|
92
124
|
if (manifest.server.managed) {
|
|
93
125
|
assertServerAuth(manifest, ctx.serverId);
|
|
94
126
|
}
|
|
95
|
-
|
|
127
|
+
// Encryption-mode parity check.
|
|
128
|
+
//
|
|
129
|
+
// The remote manifest pins the mesh's encryption mode at bootstrap.
|
|
130
|
+
// If the engine reconnects with a different `encrypted` flag (typical
|
|
131
|
+
// app bug: passphrase loaded asynchronously, so the first session
|
|
132
|
+
// wrote plaintext and the next session derives a key and tries to
|
|
133
|
+
// decrypt), every change file would fail decode and poison the remote.
|
|
134
|
+
//
|
|
135
|
+
// Surface this as an actionable error *before* any decode runs and
|
|
136
|
+
// *without* poisoning. The remote is not corrupt — the local config
|
|
137
|
+
// is wrong.
|
|
138
|
+
if (typeof manifest.encrypted === 'boolean' && manifest.encrypted !== ctx.encrypted) {
|
|
139
|
+
throw new MeshEncryptionMismatchError(manifest.encrypted, ctx.encrypted);
|
|
140
|
+
}
|
|
141
|
+
return { manifest, bootstrapped };
|
|
96
142
|
}
|
|
97
|
-
export async function upsertDeviceMetadata(adapter, remotePath, deviceId) {
|
|
143
|
+
export async function upsertDeviceMetadata(adapter, remotePath, deviceId, opts) {
|
|
98
144
|
const p = paths(remotePath);
|
|
99
145
|
const now = new Date().toISOString();
|
|
100
|
-
|
|
146
|
+
// Bootstrap fast-path: caller asserts no prior record. Skip the GET.
|
|
147
|
+
// Worst case if caller is wrong: we clobber displayName/deviceType the
|
|
148
|
+
// user set on a different device — which would itself indicate the
|
|
149
|
+
// bootstrap flag was misused. Sync engine only sets bootstrap=true
|
|
150
|
+
// when it just minted the manifest in this same connect cycle.
|
|
151
|
+
const existing = opts?.bootstrap
|
|
152
|
+
? null
|
|
153
|
+
: await readJsonIfExists(adapter, p.deviceFile(deviceId));
|
|
101
154
|
const next = {
|
|
102
155
|
deviceId,
|
|
103
156
|
registeredAt: existing?.registeredAt ?? now,
|
|
104
157
|
lastSeenAt: now,
|
|
105
158
|
userId: existing?.userId,
|
|
106
159
|
name: existing?.name,
|
|
160
|
+
displayName: opts?.displayName ?? existing?.displayName,
|
|
161
|
+
deviceType: opts?.deviceType ?? existing?.deviceType,
|
|
107
162
|
retired: existing?.retired,
|
|
108
163
|
};
|
|
109
164
|
await writeJson(adapter, p.deviceFile(deviceId), next);
|
|
110
165
|
}
|
|
111
|
-
//# sourceMappingURL=manifest.js.map
|
package/dist/core/pull.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pull — download remote changes and merge into local state.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from
|
|
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';
|
package/dist/core/pull.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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.
|
|
14
|
-
if (row.
|
|
15
|
-
emit({ type: 'delete', table: row.
|
|
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.
|
|
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
|
package/dist/core/row-id.js
CHANGED
|
@@ -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:
|
|
4
|
-
number:
|
|
5
|
-
boolean:
|
|
6
|
-
date:
|
|
7
|
-
Date:
|
|
8
|
-
json:
|
|
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"):
|
|
11
|
-
enum<const T extends string>(..._values: T[]):
|
|
12
|
-
index<T>(field: IndexableSchemaField<T>
|
|
13
|
-
|
|
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;
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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;
|