@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/crdt.js
CHANGED
|
@@ -10,10 +10,12 @@
|
|
|
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 { hlcCompareStr } from "./hlc.js";
|
|
15
|
-
/** Reserved keys that are not user columns */
|
|
16
|
-
const META_KEYS = new Set(['_table', '_rowId', '_deleted', '_deletedHlc', '_schemaVersion']);
|
|
17
19
|
/**
|
|
18
20
|
* Resolve the merge strategy for a specific column.
|
|
19
21
|
*
|
|
@@ -25,7 +27,6 @@ function resolveStrategy(schema, table, field) {
|
|
|
25
27
|
if (tableDef?.merge) {
|
|
26
28
|
const m = tableDef.merge;
|
|
27
29
|
if (typeof m === 'object' && 'fields' in m) {
|
|
28
|
-
// TableMergeConfig
|
|
29
30
|
const config = m;
|
|
30
31
|
if (config.fields?.[field])
|
|
31
32
|
return config.fields[field];
|
|
@@ -33,12 +34,9 @@ function resolveStrategy(schema, table, field) {
|
|
|
33
34
|
return config.strategy;
|
|
34
35
|
}
|
|
35
36
|
else {
|
|
36
|
-
// bare MergeStrategy (string or function) on the table
|
|
37
37
|
return m;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
// No schema at all → LWW (backwards compat for raw applyOp callers).
|
|
41
|
-
// Schema present but no mergeStrategy → remote-wins (sensible default).
|
|
42
40
|
if (!schema)
|
|
43
41
|
return 'lww';
|
|
44
42
|
return schema.mergeStrategy ?? 'remote-wins';
|
|
@@ -50,26 +48,28 @@ function resolveStrategy(schema, table, field) {
|
|
|
50
48
|
* regardless of strategy — there's no conflict.
|
|
51
49
|
*/
|
|
52
50
|
function mergeColumn(local, remote, strategy, table, rowId, field) {
|
|
53
|
-
// No local value → accept remote unconditionally
|
|
54
51
|
if (!local || !local.hlc)
|
|
55
52
|
return remote;
|
|
56
53
|
if (typeof strategy === 'function') {
|
|
57
54
|
const result = strategy(local, remote, { table, rowId, field });
|
|
58
|
-
// Only count as changed if the result differs from local
|
|
59
55
|
return result.hlc !== local.hlc || result.value !== local.value ? result : null;
|
|
60
56
|
}
|
|
61
57
|
switch (strategy) {
|
|
62
58
|
case 'remote-wins':
|
|
63
59
|
return remote;
|
|
64
60
|
case 'local-wins':
|
|
65
|
-
// Only accept remote if it's strictly newer (no conflict — local
|
|
66
|
-
// hasn't written this column yet at this HLC).
|
|
67
|
-
// When both have values, local keeps its value.
|
|
68
61
|
return null;
|
|
69
62
|
default:
|
|
70
63
|
return hlcCompareStr(remote.hlc, local.hlc) > 0 ? remote : null;
|
|
71
64
|
}
|
|
72
65
|
}
|
|
66
|
+
/** Build a fresh row stub. */
|
|
67
|
+
function blankRow(table, rowId, schemaVersion, deleted = false, deletedHlc) {
|
|
68
|
+
return {
|
|
69
|
+
_meta: { table, rowId, deleted, deletedHlc, schemaVersion },
|
|
70
|
+
payload: {},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
73
|
/**
|
|
74
74
|
* Apply a single op to the in-memory state.
|
|
75
75
|
* Returns the affected row (mutated in place) or null if no change.
|
|
@@ -82,63 +82,50 @@ export function applyOp(tables, op, schemaVersion, schema) {
|
|
|
82
82
|
if (op.type === 'delete') {
|
|
83
83
|
const existing = table[op.rowId];
|
|
84
84
|
if (existing) {
|
|
85
|
-
//
|
|
86
|
-
if (existing.
|
|
87
|
-
return null;
|
|
85
|
+
// Stale delete (older than current tombstone)?
|
|
86
|
+
if (existing._meta.deletedHlc && hlcCompareStr(op.hlc, existing._meta.deletedHlc) <= 0) {
|
|
87
|
+
return null;
|
|
88
88
|
}
|
|
89
|
-
//
|
|
90
|
-
const hasNewerColumn = Object.
|
|
91
|
-
if (META_KEYS.has(key))
|
|
92
|
-
return false;
|
|
93
|
-
const entry = val;
|
|
89
|
+
// Any payload column newer than this delete? Then delete loses.
|
|
90
|
+
const hasNewerColumn = Object.values(existing.payload).some(entry => {
|
|
94
91
|
return entry?.hlc && hlcCompareStr(entry.hlc, op.hlc) > 0;
|
|
95
92
|
});
|
|
96
93
|
if (hasNewerColumn)
|
|
97
94
|
return null;
|
|
98
|
-
existing.
|
|
99
|
-
existing.
|
|
95
|
+
existing._meta.deleted = true;
|
|
96
|
+
existing._meta.deletedHlc = op.hlc;
|
|
100
97
|
return existing;
|
|
101
98
|
}
|
|
102
|
-
// Tombstone for
|
|
103
|
-
const row =
|
|
104
|
-
_table: op.table,
|
|
105
|
-
_rowId: op.rowId,
|
|
106
|
-
_deleted: true,
|
|
107
|
-
_deletedHlc: op.hlc,
|
|
108
|
-
_schemaVersion: schemaVersion,
|
|
109
|
-
};
|
|
99
|
+
// Tombstone for unseen row.
|
|
100
|
+
const row = blankRow(op.table, op.rowId, schemaVersion, true, op.hlc);
|
|
110
101
|
table[op.rowId] = row;
|
|
111
102
|
return row;
|
|
112
103
|
}
|
|
113
|
-
// Upsert
|
|
104
|
+
// Upsert.
|
|
114
105
|
let row = table[op.rowId];
|
|
115
106
|
let changed = false;
|
|
116
107
|
if (!row) {
|
|
117
|
-
row =
|
|
118
|
-
_table: op.table,
|
|
119
|
-
_rowId: op.rowId,
|
|
120
|
-
_deleted: false,
|
|
121
|
-
_schemaVersion: schemaVersion,
|
|
122
|
-
};
|
|
108
|
+
row = blankRow(op.table, op.rowId, schemaVersion);
|
|
123
109
|
table[op.rowId] = row;
|
|
124
110
|
changed = true;
|
|
125
111
|
}
|
|
126
112
|
for (const [col, entry] of Object.entries(op.columns)) {
|
|
127
|
-
const existing = row[col];
|
|
113
|
+
const existing = row.payload[col];
|
|
128
114
|
const strategy = resolveStrategy(schema, op.table, col);
|
|
129
115
|
const winner = mergeColumn(existing, entry, strategy, op.table, op.rowId, col);
|
|
130
116
|
if (winner) {
|
|
131
|
-
row[col] = winner;
|
|
117
|
+
row.payload[col] = winner;
|
|
132
118
|
changed = true;
|
|
133
119
|
}
|
|
134
120
|
}
|
|
135
|
-
//
|
|
136
|
-
if (row.
|
|
121
|
+
// Resurrection: upsert with HLC newer than tombstone revives the row.
|
|
122
|
+
if (row._meta.deleted && row._meta.deletedHlc) {
|
|
137
123
|
const newestOpHlc = Object.values(op.columns).reduce((max, entry) => {
|
|
138
124
|
return !max || hlcCompareStr(entry.hlc, max) > 0 ? entry.hlc : max;
|
|
139
125
|
}, '');
|
|
140
|
-
if (newestOpHlc && hlcCompareStr(newestOpHlc, row.
|
|
141
|
-
row.
|
|
126
|
+
if (newestOpHlc && hlcCompareStr(newestOpHlc, row._meta.deletedHlc) > 0) {
|
|
127
|
+
row._meta.deleted = false;
|
|
128
|
+
row._meta.deletedHlc = undefined;
|
|
142
129
|
changed = true;
|
|
143
130
|
}
|
|
144
131
|
}
|
|
@@ -157,32 +144,23 @@ export function applyChangeEntry(tables, entry, schemaVersion, schema) {
|
|
|
157
144
|
}
|
|
158
145
|
return affected;
|
|
159
146
|
}
|
|
160
|
-
/**
|
|
161
|
-
* Read a column value from a row, unwrapping the ColumnEntry.
|
|
162
|
-
*/
|
|
147
|
+
/** Read a column value from a row, unwrapping the ColumnEntry. */
|
|
163
148
|
export function readColumn(row, column) {
|
|
164
|
-
const entry = row[column];
|
|
165
|
-
|
|
166
|
-
return entry.value;
|
|
167
|
-
}
|
|
168
|
-
return undefined;
|
|
149
|
+
const entry = row.payload?.[column];
|
|
150
|
+
return entry?.value;
|
|
169
151
|
}
|
|
170
152
|
/**
|
|
171
153
|
* Build a plain object from a row (strip HLC metadata).
|
|
154
|
+
* Returns user-facing fields with `_meta` projection (table, rowId, deleted).
|
|
172
155
|
*/
|
|
173
156
|
export function rowToPlain(row) {
|
|
174
157
|
const result = {
|
|
175
|
-
_table: row.
|
|
176
|
-
_rowId: row.
|
|
177
|
-
_deleted: row.
|
|
158
|
+
_table: row._meta.table,
|
|
159
|
+
_rowId: row._meta.rowId,
|
|
160
|
+
_deleted: row._meta.deleted,
|
|
178
161
|
};
|
|
179
|
-
for (const [key,
|
|
180
|
-
|
|
181
|
-
continue;
|
|
182
|
-
if (val && typeof val === 'object' && 'value' in val && 'hlc' in val) {
|
|
183
|
-
result[key] = val.value;
|
|
184
|
-
}
|
|
162
|
+
for (const [key, entry] of Object.entries(row.payload)) {
|
|
163
|
+
result[key] = entry.value;
|
|
185
164
|
}
|
|
186
165
|
return result;
|
|
187
166
|
}
|
|
188
|
-
//# sourceMappingURL=crdt.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error classes for the Interocitor engine.
|
|
3
|
+
*
|
|
4
|
+
* Callers should prefer `instanceof` over message/code string matching.
|
|
5
|
+
* Every typed error keeps its `code` field stable across releases — it
|
|
6
|
+
* is part of the public contract.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Thrown by `connect()` when the engine's `encrypted` flag does not
|
|
10
|
+
* match the encryption mode the remote mesh was bootstrapped with.
|
|
11
|
+
*
|
|
12
|
+
* Common cause: the app constructs the engine with `encrypted: false`
|
|
13
|
+
* on first run (e.g. before the user supplies a passphrase) and later
|
|
14
|
+
* reconnects with `encrypted: true`. The remote is healthy and is
|
|
15
|
+
* NOT poisoned by this error — the local engine config is wrong.
|
|
16
|
+
*
|
|
17
|
+
* Recovery: rebuild the engine with `encrypted` set to `expectedMode`
|
|
18
|
+
* and supply the matching passphrase when `expectedMode === true`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Thrown by `connect()` when the credential store has a record under the
|
|
22
|
+
* engine's `dbName` but the stored `meshId` does not match the live mesh
|
|
23
|
+
* the engine is connecting to.
|
|
24
|
+
*
|
|
25
|
+
* Common cause: app reuses the same `dbName` for "create new mesh" — the
|
|
26
|
+
* old record (passphrase + deviceId for the previous mesh) survives the
|
|
27
|
+
* recreate. Silently reusing the old key would either fail to decrypt
|
|
28
|
+
* the new mesh's files or, worse, encrypt new writes under the wrong
|
|
29
|
+
* key and poison the remote.
|
|
30
|
+
*
|
|
31
|
+
* Recovery: the caller decides — either `clearCredentials()` and retry,
|
|
32
|
+
* or change `dbName` so the two meshes have isolated credential stores.
|
|
33
|
+
*/
|
|
34
|
+
export declare class MeshCredentialMismatchError extends Error {
|
|
35
|
+
readonly code: "MESH_CREDENTIAL_MISMATCH";
|
|
36
|
+
readonly dbName: string;
|
|
37
|
+
readonly storedMeshId: string;
|
|
38
|
+
readonly activeMeshId: string;
|
|
39
|
+
constructor(dbName: string, storedMeshId: string, activeMeshId: string);
|
|
40
|
+
}
|
|
41
|
+
export declare class MeshEncryptionMismatchError extends Error {
|
|
42
|
+
readonly code: "MESH_ENCRYPTION_MISMATCH";
|
|
43
|
+
readonly expectedMode: boolean;
|
|
44
|
+
readonly actualMode: boolean;
|
|
45
|
+
constructor(expectedMode: boolean, actualMode: boolean);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;GAWG;AACH;;;;;;;;;;;;;GAaG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,IAAI,EAAG,0BAA0B,CAAU;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;CAavE;AAED,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,IAAI,EAAG,0BAA0B,CAAU;IACpD,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;gBAEjB,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO;CAavD"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error classes for the Interocitor engine.
|
|
3
|
+
*
|
|
4
|
+
* Callers should prefer `instanceof` over message/code string matching.
|
|
5
|
+
* Every typed error keeps its `code` field stable across releases — it
|
|
6
|
+
* is part of the public contract.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Thrown by `connect()` when the engine's `encrypted` flag does not
|
|
10
|
+
* match the encryption mode the remote mesh was bootstrapped with.
|
|
11
|
+
*
|
|
12
|
+
* Common cause: the app constructs the engine with `encrypted: false`
|
|
13
|
+
* on first run (e.g. before the user supplies a passphrase) and later
|
|
14
|
+
* reconnects with `encrypted: true`. The remote is healthy and is
|
|
15
|
+
* NOT poisoned by this error — the local engine config is wrong.
|
|
16
|
+
*
|
|
17
|
+
* Recovery: rebuild the engine with `encrypted` set to `expectedMode`
|
|
18
|
+
* and supply the matching passphrase when `expectedMode === true`.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Thrown by `connect()` when the credential store has a record under the
|
|
22
|
+
* engine's `dbName` but the stored `meshId` does not match the live mesh
|
|
23
|
+
* the engine is connecting to.
|
|
24
|
+
*
|
|
25
|
+
* Common cause: app reuses the same `dbName` for "create new mesh" — the
|
|
26
|
+
* old record (passphrase + deviceId for the previous mesh) survives the
|
|
27
|
+
* recreate. Silently reusing the old key would either fail to decrypt
|
|
28
|
+
* the new mesh's files or, worse, encrypt new writes under the wrong
|
|
29
|
+
* key and poison the remote.
|
|
30
|
+
*
|
|
31
|
+
* Recovery: the caller decides — either `clearCredentials()` and retry,
|
|
32
|
+
* or change `dbName` so the two meshes have isolated credential stores.
|
|
33
|
+
*/
|
|
34
|
+
export class MeshCredentialMismatchError extends Error {
|
|
35
|
+
constructor(dbName, storedMeshId, activeMeshId) {
|
|
36
|
+
super(`Stored credentials under dbName="${dbName}" belong to meshId="${storedMeshId}" ` +
|
|
37
|
+
`but the active mesh is meshId="${activeMeshId}". ` +
|
|
38
|
+
`Refusing to silently reuse the wrong key. ` +
|
|
39
|
+
`Call engine.clearCredentials() to drop the stale record, ` +
|
|
40
|
+
`or use a different dbName for the new mesh.`);
|
|
41
|
+
this.code = 'MESH_CREDENTIAL_MISMATCH';
|
|
42
|
+
this.name = 'MeshCredentialMismatchError';
|
|
43
|
+
this.dbName = dbName;
|
|
44
|
+
this.storedMeshId = storedMeshId;
|
|
45
|
+
this.activeMeshId = activeMeshId;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export class MeshEncryptionMismatchError extends Error {
|
|
49
|
+
constructor(expectedMode, actualMode) {
|
|
50
|
+
super(`Mesh encryption mode mismatch: remote mesh was bootstrapped with ` +
|
|
51
|
+
`encrypted=${expectedMode} but this engine was created with ` +
|
|
52
|
+
`encrypted=${actualMode}. Recreate the Interocitor instance with ` +
|
|
53
|
+
`encrypted=${expectedMode}` +
|
|
54
|
+
(expectedMode ? ' and supply the matching passphrase' : '') +
|
|
55
|
+
`, or join a fresh mesh. Remote was NOT poisoned.`);
|
|
56
|
+
this.code = 'MESH_ENCRYPTION_MISMATCH';
|
|
57
|
+
this.name = 'MeshEncryptionMismatchError';
|
|
58
|
+
this.expectedMode = expectedMode;
|
|
59
|
+
this.actualMode = actualMode;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/core/flush.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Flush — push queued local outbox entries to remote cloud.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from
|
|
4
|
+
* Extracted from Interocitor. Not part of the public API.
|
|
5
5
|
*/
|
|
6
|
-
import type { StorageAdapter, ChangeEntry } from './types.ts';
|
|
6
|
+
import type { StorageAdapter, ChangeEntry, SyncEvent } from './types.ts';
|
|
7
7
|
import type { CodecState } from './codec.ts';
|
|
8
|
-
export declare function flushToAdapter(adapter: StorageAdapter, remotePath: string, entries: ChangeEntry[], isPrimary: boolean, codecState: CodecState, deviceId: string): Promise<void>;
|
|
8
|
+
export declare function flushToAdapter(adapter: StorageAdapter, remotePath: string, entries: ChangeEntry[], isPrimary: boolean, codecState: CodecState, deviceId: string, emit?: (event: SyncEvent) => void): Promise<void>;
|
|
9
9
|
//# sourceMappingURL=flush.d.ts.map
|
package/dist/core/flush.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flush.d.ts","sourceRoot":"","sources":["../../src/core/flush.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,WAAW,
|
|
1
|
+
{"version":3,"file":"flush.d.ts","sourceRoot":"","sources":["../../src/core/flush.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,WAAW,EAEX,SAAS,EACV,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,wBAAsB,cAAc,CAClC,OAAO,EAAE,cAAc,EACvB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,WAAW,EAAE,EACtB,SAAS,EAAE,OAAO,EAClB,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAe,GAC1C,OAAO,CAAC,IAAI,CAAC,CAqGf"}
|
package/dist/core/flush.js
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Flush — push queued local outbox entries to remote cloud.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from
|
|
4
|
+
* Extracted from Interocitor. Not part of the public API.
|
|
5
5
|
*/
|
|
6
6
|
import { hlcCompareStr } from "./hlc.js";
|
|
7
7
|
import { paths, textEncoder, textDecoder } from "./internals.js";
|
|
8
8
|
import { encodeChangePayload } from "./codec.js";
|
|
9
9
|
import { upsertDeviceMetadata } from "./manifest.js";
|
|
10
|
-
export async function flushToAdapter(adapter, remotePath, entries, isPrimary, codecState, deviceId) {
|
|
10
|
+
export async function flushToAdapter(adapter, remotePath, entries, isPrimary, codecState, deviceId, emit = () => { }) {
|
|
11
11
|
const p = paths(remotePath);
|
|
12
12
|
await adapter.ensureFolder(p.changesFolder);
|
|
13
13
|
let lastWrittenHlc = '';
|
|
14
14
|
for (const entry of entries) {
|
|
15
15
|
const fileName = `${entry.hlc}-${entry.id}.json`;
|
|
16
16
|
const payload = await encodeChangePayload(codecState, entry);
|
|
17
|
+
console.log('[interocitor:write] flush.changeFile', { path: p.changeFile(fileName), deviceId, isPrimary, entryId: entry.id, hlc: entry.hlc });
|
|
17
18
|
await adapter.writeFile(p.changeFile(fileName), textEncoder.encode(payload));
|
|
18
19
|
if (!lastWrittenHlc || hlcCompareStr(entry.hlc, lastWrittenHlc) > 0) {
|
|
19
20
|
lastWrittenHlc = entry.hlc;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
// Update global head — monotonic HLC hint for fast poll skipping.
|
|
24
|
+
// Tracing rules (see SyncEvent `trace:head`):
|
|
25
|
+
// - read → emit { op: 'read', reason: 'flush', priorHlc }
|
|
26
|
+
// - write strictly forward → emit { op: 'write', priorHlc, nextHlc }
|
|
27
|
+
// - write where nextHlc <= priorHlc → emit
|
|
28
|
+
// { op: 'write', regressed: true } AND skip the writeFile
|
|
29
|
+
// (regression = bug signal; never let head go backwards on disk).
|
|
30
|
+
// - no entries to flush → emit { op: 'skip-no-change' } and don't touch head.
|
|
31
|
+
if (!lastWrittenHlc) {
|
|
32
|
+
emit({
|
|
33
|
+
type: 'trace:head',
|
|
34
|
+
op: 'skip-no-change',
|
|
35
|
+
reason: 'flush',
|
|
36
|
+
path: p.changesHead,
|
|
37
|
+
nextHlc: null,
|
|
38
|
+
});
|
|
39
|
+
if (isPrimary) {
|
|
40
|
+
await upsertDeviceMetadata(adapter, remotePath, deviceId);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
23
44
|
const readHeadIfExists = async () => {
|
|
24
45
|
try {
|
|
25
46
|
const data = await adapter.readFile(p.changesHead);
|
|
@@ -30,12 +51,48 @@ export async function flushToAdapter(adapter, remotePath, entries, isPrimary, co
|
|
|
30
51
|
}
|
|
31
52
|
};
|
|
32
53
|
const priorHead = await readHeadIfExists();
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
:
|
|
36
|
-
|
|
54
|
+
const priorHlc = priorHead?.latestHlc ?? null;
|
|
55
|
+
emit({
|
|
56
|
+
type: 'trace:head',
|
|
57
|
+
op: 'read',
|
|
58
|
+
reason: 'flush',
|
|
59
|
+
path: p.changesHead,
|
|
60
|
+
priorHlc,
|
|
61
|
+
});
|
|
62
|
+
// Already-current short-circuit. Nothing to write — head already at or
|
|
63
|
+
// beyond what we just wrote (replica catching up, retried flush, etc.).
|
|
64
|
+
if (priorHlc && hlcCompareStr(priorHlc, lastWrittenHlc) >= 0) {
|
|
65
|
+
emit({
|
|
66
|
+
type: 'trace:head',
|
|
67
|
+
op: 'skip-no-change',
|
|
68
|
+
reason: 'flush',
|
|
69
|
+
path: p.changesHead,
|
|
70
|
+
priorHlc,
|
|
71
|
+
nextHlc: lastWrittenHlc,
|
|
72
|
+
});
|
|
73
|
+
if (isPrimary) {
|
|
74
|
+
await upsertDeviceMetadata(adapter, remotePath, deviceId);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Defensive: if priorHlc somehow > our lastWritten (shouldn't happen
|
|
79
|
+
// after the >= short-circuit) flag as regression and don't write.
|
|
80
|
+
const regressed = priorHlc !== null && hlcCompareStr(priorHlc, lastWrittenHlc) > 0;
|
|
81
|
+
const bestHlc = regressed ? priorHlc : lastWrittenHlc;
|
|
82
|
+
emit({
|
|
83
|
+
type: 'trace:head',
|
|
84
|
+
op: 'write',
|
|
85
|
+
reason: 'flush',
|
|
86
|
+
path: p.changesHead,
|
|
87
|
+
priorHlc,
|
|
88
|
+
nextHlc: bestHlc,
|
|
89
|
+
regressed,
|
|
90
|
+
});
|
|
91
|
+
if (!regressed) {
|
|
92
|
+
console.log('[interocitor:write] flush.head', { path: p.changesHead, deviceId, isPrimary, priorHlc, nextHlc: bestHlc });
|
|
93
|
+
await adapter.writeFile(p.changesHead, textEncoder.encode(JSON.stringify({ latestHlc: bestHlc }, null, 2)));
|
|
94
|
+
}
|
|
37
95
|
if (isPrimary) {
|
|
38
96
|
await upsertDeviceMetadata(adapter, remotePath, deviceId);
|
|
39
97
|
}
|
|
40
98
|
}
|
|
41
|
-
//# sourceMappingURL=flush.js.map
|
package/dist/core/hlc.js
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured ID generation and validation.
|
|
3
|
+
*
|
|
4
|
+
* Device IDs: UUIDv7 — timestamp + random, client-generated.
|
|
5
|
+
* Mesh IDs: UUIDv7 + HMAC tag — timestamp + random + MAC, worker-issued.
|
|
6
|
+
* Row IDs: UUID v4/v7 with optional prefix — client-generated.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Generate a UUIDv7: 48-bit ms timestamp + 74 bits random, RFC 9562.
|
|
10
|
+
* Sortable by creation time. Globally unique without coordination.
|
|
11
|
+
*/
|
|
12
|
+
export declare function uuidv7(): string;
|
|
13
|
+
/** Generate a device ID. UUIDv7 — sortable, globally unique. */
|
|
14
|
+
export declare function createDeviceId(): string;
|
|
15
|
+
/** Validate a device ID (must be UUIDv7). */
|
|
16
|
+
export declare function isValidDeviceId(id: unknown): id is string;
|
|
17
|
+
/**
|
|
18
|
+
* Issue a mesh/team ID with embedded HMAC tag.
|
|
19
|
+
*
|
|
20
|
+
* Layout (opaque string):
|
|
21
|
+
* <uuidv7>.<tag>
|
|
22
|
+
*
|
|
23
|
+
* Where tag = base64url(HMAC-SHA256(secret, uuidv7))[0..10]
|
|
24
|
+
*
|
|
25
|
+
* Only workers with the secret can mint valid mesh IDs.
|
|
26
|
+
* Clients and workers validate with `isValidMeshId(id, secret)`.
|
|
27
|
+
*
|
|
28
|
+
* @param secret — HMAC key (CryptoKey or raw bytes). Workers hold this.
|
|
29
|
+
*/
|
|
30
|
+
export declare function issueMeshId(secret: CryptoKey): Promise<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Validate a mesh ID: format check + HMAC tag verification.
|
|
33
|
+
*
|
|
34
|
+
* @param id — the full mesh ID string (uuid.tag)
|
|
35
|
+
* @param secret — same HMAC key used to issue
|
|
36
|
+
*/
|
|
37
|
+
export declare function isValidMeshId(id: unknown, secret: CryptoKey): Promise<boolean>;
|
|
38
|
+
/**
|
|
39
|
+
* Parse a mesh ID into its parts. Does NOT verify tag.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseMeshId(id: string): {
|
|
42
|
+
uuid: string;
|
|
43
|
+
tag: string;
|
|
44
|
+
} | null;
|
|
45
|
+
/**
|
|
46
|
+
* Create a mesh HMAC secret key for use with issueMeshId / isValidMeshId.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createMeshSecret(): Promise<CryptoKey>;
|
|
49
|
+
//# sourceMappingURL=ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/core/ids.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH;;;GAGG;AACH,wBAAgB,MAAM,IAAI,MAAM,CAiC/B;AAID,gEAAgE;AAChE,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAID,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,GAAG,EAAE,IAAI,MAAM,CAEzD;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAIpE;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CASpF;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO5E;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,SAAS,CAAC,CAM3D"}
|
package/dist/core/ids.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured ID generation and validation.
|
|
3
|
+
*
|
|
4
|
+
* Device IDs: UUIDv7 — timestamp + random, client-generated.
|
|
5
|
+
* Mesh IDs: UUIDv7 + HMAC tag — timestamp + random + MAC, worker-issued.
|
|
6
|
+
* Row IDs: UUID v4/v7 with optional prefix — client-generated.
|
|
7
|
+
*/
|
|
8
|
+
// ─── UUIDv7 ──────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Generate a UUIDv7: 48-bit ms timestamp + 74 bits random, RFC 9562.
|
|
11
|
+
* Sortable by creation time. Globally unique without coordination.
|
|
12
|
+
*/
|
|
13
|
+
export function uuidv7() {
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
// 6 bytes timestamp (48-bit ms)
|
|
16
|
+
const tsBytes = new Uint8Array(6);
|
|
17
|
+
let ts = now;
|
|
18
|
+
for (let i = 5; i >= 0; i--) {
|
|
19
|
+
tsBytes[i] = ts & 0xff;
|
|
20
|
+
ts = Math.floor(ts / 256);
|
|
21
|
+
}
|
|
22
|
+
// 10 bytes random
|
|
23
|
+
const randBytes = crypto.getRandomValues(new Uint8Array(10));
|
|
24
|
+
// Assemble 16 bytes
|
|
25
|
+
const bytes = new Uint8Array(16);
|
|
26
|
+
bytes.set(tsBytes, 0);
|
|
27
|
+
bytes.set(randBytes, 6);
|
|
28
|
+
// Set version 7 (bits 48-51)
|
|
29
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
30
|
+
// Set variant 10xx (bits 64-65)
|
|
31
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
32
|
+
// Format as UUID string
|
|
33
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
34
|
+
return [
|
|
35
|
+
hex.slice(0, 8),
|
|
36
|
+
hex.slice(8, 12),
|
|
37
|
+
hex.slice(12, 16),
|
|
38
|
+
hex.slice(16, 20),
|
|
39
|
+
hex.slice(20, 32),
|
|
40
|
+
].join('-');
|
|
41
|
+
}
|
|
42
|
+
// ─── Device IDs ──────────────────────────────────────────────────────
|
|
43
|
+
/** Generate a device ID. UUIDv7 — sortable, globally unique. */
|
|
44
|
+
export function createDeviceId() {
|
|
45
|
+
return uuidv7();
|
|
46
|
+
}
|
|
47
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
48
|
+
/** Validate a device ID (must be UUIDv7). */
|
|
49
|
+
export function isValidDeviceId(id) {
|
|
50
|
+
return typeof id === 'string' && UUID_RE.test(id);
|
|
51
|
+
}
|
|
52
|
+
// ─── Mesh / Team IDs ─────────────────────────────────────────────────
|
|
53
|
+
/**
|
|
54
|
+
* Issue a mesh/team ID with embedded HMAC tag.
|
|
55
|
+
*
|
|
56
|
+
* Layout (opaque string):
|
|
57
|
+
* <uuidv7>.<tag>
|
|
58
|
+
*
|
|
59
|
+
* Where tag = base64url(HMAC-SHA256(secret, uuidv7))[0..10]
|
|
60
|
+
*
|
|
61
|
+
* Only workers with the secret can mint valid mesh IDs.
|
|
62
|
+
* Clients and workers validate with `isValidMeshId(id, secret)`.
|
|
63
|
+
*
|
|
64
|
+
* @param secret — HMAC key (CryptoKey or raw bytes). Workers hold this.
|
|
65
|
+
*/
|
|
66
|
+
export async function issueMeshId(secret) {
|
|
67
|
+
const id = uuidv7();
|
|
68
|
+
const tag = await computeTag(id, secret);
|
|
69
|
+
return `${id}.${tag}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate a mesh ID: format check + HMAC tag verification.
|
|
73
|
+
*
|
|
74
|
+
* @param id — the full mesh ID string (uuid.tag)
|
|
75
|
+
* @param secret — same HMAC key used to issue
|
|
76
|
+
*/
|
|
77
|
+
export async function isValidMeshId(id, secret) {
|
|
78
|
+
if (typeof id !== 'string')
|
|
79
|
+
return false;
|
|
80
|
+
const dot = id.lastIndexOf('.');
|
|
81
|
+
if (dot === -1)
|
|
82
|
+
return false;
|
|
83
|
+
const uuid = id.slice(0, dot);
|
|
84
|
+
const tag = id.slice(dot + 1);
|
|
85
|
+
if (!UUID_RE.test(uuid) || !tag)
|
|
86
|
+
return false;
|
|
87
|
+
const expected = await computeTag(uuid, secret);
|
|
88
|
+
return timingSafeEqual(tag, expected);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Parse a mesh ID into its parts. Does NOT verify tag.
|
|
92
|
+
*/
|
|
93
|
+
export function parseMeshId(id) {
|
|
94
|
+
const dot = id.lastIndexOf('.');
|
|
95
|
+
if (dot === -1)
|
|
96
|
+
return null;
|
|
97
|
+
const uuid = id.slice(0, dot);
|
|
98
|
+
const tag = id.slice(dot + 1);
|
|
99
|
+
if (!UUID_RE.test(uuid) || !tag)
|
|
100
|
+
return null;
|
|
101
|
+
return { uuid, tag };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Create a mesh HMAC secret key for use with issueMeshId / isValidMeshId.
|
|
105
|
+
*/
|
|
106
|
+
export async function createMeshSecret() {
|
|
107
|
+
return crypto.subtle.generateKey({ name: 'HMAC', hash: 'SHA-256' }, true, ['sign', 'verify']);
|
|
108
|
+
}
|
|
109
|
+
// ─── Internal helpers ────────────────────────────────────────────────
|
|
110
|
+
const encoder = new TextEncoder();
|
|
111
|
+
async function computeTag(data, secret) {
|
|
112
|
+
const sig = await crypto.subtle.sign('HMAC', secret, encoder.encode(data));
|
|
113
|
+
// Take first 8 bytes (64 bits) → base64url (11 chars)
|
|
114
|
+
const bytes = new Uint8Array(sig, 0, 8);
|
|
115
|
+
return base64url(bytes);
|
|
116
|
+
}
|
|
117
|
+
function base64url(bytes) {
|
|
118
|
+
let binary = '';
|
|
119
|
+
for (let i = 0; i < bytes.length; i++)
|
|
120
|
+
binary += String.fromCharCode(bytes[i]);
|
|
121
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
122
|
+
}
|
|
123
|
+
/** Constant-time string comparison to prevent timing attacks on tag. */
|
|
124
|
+
function timingSafeEqual(a, b) {
|
|
125
|
+
if (a.length !== b.length)
|
|
126
|
+
return false;
|
|
127
|
+
let result = 0;
|
|
128
|
+
for (let i = 0; i < a.length; i++) {
|
|
129
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
130
|
+
}
|
|
131
|
+
return result === 0;
|
|
132
|
+
}
|
package/dist/core/internals.d.ts
CHANGED
|
@@ -14,12 +14,20 @@ export interface CloudPaths {
|
|
|
14
14
|
changeFile: (fileName: string) => string;
|
|
15
15
|
}
|
|
16
16
|
export declare function paths(root: string): CloudPaths;
|
|
17
|
-
|
|
17
|
+
declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error"];
|
|
18
|
+
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
19
|
+
export declare function logAtLevel(currentLevel: LogLevel, level: LogLevel, ...args: unknown[]): void;
|
|
20
|
+
export declare function log(level: LogLevel, ...args: unknown[]): void;
|
|
21
|
+
export declare function normalizeLogLevel(level: string | null | undefined): LogLevel;
|
|
22
|
+
export { LOG_LEVELS };
|
|
23
|
+
/**
|
|
24
|
+
* Generate a prefixed ID for internal use (change entries, snapshots, etc).
|
|
25
|
+
* Uses UUIDv7 for sortability.
|
|
26
|
+
*/
|
|
18
27
|
export declare function generateId(prefix: string): string;
|
|
19
28
|
export declare function getDeviceId(override?: string): string;
|
|
20
29
|
export declare const textEncoder: TextEncoder;
|
|
21
30
|
export declare const textDecoder: TextDecoder;
|
|
22
|
-
export declare const ROW_META_KEYS: Set<string>;
|
|
23
31
|
export declare function hexFromBytes(bytes: Uint8Array): string;
|
|
24
32
|
export declare function computeContentHash(payload: unknown): Promise<string>;
|
|
25
33
|
//# sourceMappingURL=internals.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internals.d.ts","sourceRoot":"","sources":["../../src/core/internals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CAC1C;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAY9C;
|
|
1
|
+
{"version":3,"file":"internals.d.ts","sourceRoot":"","sources":["../../src/core/internals.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CAC1C;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAY9C;AAKD,QAAA,MAAM,UAAU,6CAA8C,CAAC;AAE/D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAMnD,wBAAgB,UAAU,CAAC,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAI5F;AAED,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAE7D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,QAAQ,CAE5E;AAED,OAAO,EAAE,UAAU,EAAE,CAAC;AAOtB;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAUrD;AAID,eAAO,MAAM,WAAW,aAAoB,CAAC;AAC7C,eAAO,MAAM,WAAW,aAAoB,CAAC;AAE7C,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEtD;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAI1E"}
|