@interocitor/core 0.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +706 -0
  3. package/dist/adapters/cloudflare.d.ts +78 -0
  4. package/dist/adapters/cloudflare.d.ts.map +1 -0
  5. package/dist/adapters/cloudflare.js +325 -0
  6. package/dist/adapters/google-drive.d.ts +64 -0
  7. package/dist/adapters/google-drive.d.ts.map +1 -0
  8. package/dist/adapters/google-drive.js +339 -0
  9. package/dist/adapters/memory.d.ts +53 -0
  10. package/dist/adapters/memory.d.ts.map +1 -0
  11. package/dist/adapters/memory.js +182 -0
  12. package/dist/adapters/webdav.d.ts +70 -0
  13. package/dist/adapters/webdav.d.ts.map +1 -0
  14. package/dist/adapters/webdav.js +323 -0
  15. package/dist/core/codec.d.ts +20 -0
  16. package/dist/core/codec.d.ts.map +1 -0
  17. package/dist/core/codec.js +102 -0
  18. package/dist/core/compaction.d.ts +45 -0
  19. package/dist/core/compaction.d.ts.map +1 -0
  20. package/dist/core/compaction.js +190 -0
  21. package/dist/core/connected-stores.d.ts +77 -0
  22. package/dist/core/connected-stores.d.ts.map +1 -0
  23. package/dist/core/connected-stores.js +76 -0
  24. package/dist/core/crdt.d.ts +36 -0
  25. package/dist/core/crdt.d.ts.map +1 -0
  26. package/dist/core/crdt.js +174 -0
  27. package/dist/core/errors.d.ts +47 -0
  28. package/dist/core/errors.d.ts.map +1 -0
  29. package/dist/core/errors.js +61 -0
  30. package/dist/core/flush.d.ts +9 -0
  31. package/dist/core/flush.d.ts.map +1 -0
  32. package/dist/core/flush.js +98 -0
  33. package/dist/core/hlc.d.ts +25 -0
  34. package/dist/core/hlc.d.ts.map +1 -0
  35. package/dist/core/hlc.js +75 -0
  36. package/dist/core/ids.d.ts +49 -0
  37. package/dist/core/ids.d.ts.map +1 -0
  38. package/dist/core/ids.js +132 -0
  39. package/dist/core/internals.d.ts +33 -0
  40. package/dist/core/internals.d.ts.map +1 -0
  41. package/dist/core/internals.js +72 -0
  42. package/dist/core/manifest.d.ts +56 -0
  43. package/dist/core/manifest.d.ts.map +1 -0
  44. package/dist/core/manifest.js +203 -0
  45. package/dist/core/pull.d.ts +26 -0
  46. package/dist/core/pull.d.ts.map +1 -0
  47. package/dist/core/pull.js +113 -0
  48. package/dist/core/row-id.d.ts +12 -0
  49. package/dist/core/row-id.d.ts.map +1 -0
  50. package/dist/core/row-id.js +11 -0
  51. package/dist/core/schema-types.d.ts +26 -0
  52. package/dist/core/schema-types.d.ts.map +1 -0
  53. package/dist/core/schema-types.js +31 -0
  54. package/dist/core/schema-types.type-test.d.ts +2 -0
  55. package/dist/core/schema-types.type-test.d.ts.map +1 -0
  56. package/dist/core/schema-types.type-test.js +224 -0
  57. package/dist/core/sync-engine.d.ts +364 -0
  58. package/dist/core/sync-engine.d.ts.map +1 -0
  59. package/dist/core/sync-engine.js +2475 -0
  60. package/dist/core/table.d.ts +260 -0
  61. package/dist/core/table.d.ts.map +1 -0
  62. package/dist/core/table.js +461 -0
  63. package/dist/core/types.d.ts +952 -0
  64. package/dist/core/types.d.ts.map +1 -0
  65. package/dist/core/types.js +6 -0
  66. package/dist/crypto/encryption.d.ts +61 -0
  67. package/dist/crypto/encryption.d.ts.map +1 -0
  68. package/dist/crypto/encryption.js +216 -0
  69. package/dist/crypto/keys.d.ts +48 -0
  70. package/dist/crypto/keys.d.ts.map +1 -0
  71. package/dist/crypto/keys.js +54 -0
  72. package/dist/handshake/channel.d.ts +117 -0
  73. package/dist/handshake/channel.d.ts.map +1 -0
  74. package/dist/handshake/channel.js +245 -0
  75. package/dist/handshake/index.d.ts +216 -0
  76. package/dist/handshake/index.d.ts.map +1 -0
  77. package/dist/handshake/index.js +199 -0
  78. package/dist/handshake/qr-public.d.ts +3 -0
  79. package/dist/handshake/qr-public.d.ts.map +1 -0
  80. package/dist/handshake/qr-public.js +1 -0
  81. package/dist/handshake/qr.d.ts +100 -0
  82. package/dist/handshake/qr.d.ts.map +1 -0
  83. package/dist/handshake/qr.js +102 -0
  84. package/dist/index.d.ts +50 -0
  85. package/dist/index.d.ts.map +1 -0
  86. package/dist/index.js +50 -0
  87. package/dist/storage/credential-store.d.ts +122 -0
  88. package/dist/storage/credential-store.d.ts.map +1 -0
  89. package/dist/storage/credential-store.js +356 -0
  90. package/dist/storage/local-store.d.ts +64 -0
  91. package/dist/storage/local-store.d.ts.map +1 -0
  92. package/dist/storage/local-store.js +490 -0
  93. package/dist/storage/reset.d.ts +10 -0
  94. package/dist/storage/reset.d.ts.map +1 -0
  95. package/dist/storage/reset.js +18 -0
  96. package/package.json +76 -0
@@ -0,0 +1,952 @@
1
+ /**
2
+ * interocitor — encrypted local-first CRDT sync over cloud storage
3
+ *
4
+ * Core type definitions
5
+ */
6
+ /**
7
+ * Stable identity metadata for a client device participating in a mesh.
8
+ */
9
+ export interface DeviceInfo {
10
+ deviceId: string;
11
+ userId?: string;
12
+ name?: string;
13
+ }
14
+ /**
15
+ * Hybrid logical clock state used to order CRDT writes across devices.
16
+ */
17
+ export interface HLC {
18
+ ts: number;
19
+ counter: number;
20
+ nodeId: string;
21
+ }
22
+ export type ColumnValue = string | number | boolean | null | object;
23
+ /**
24
+ * CRDT cell value paired with the HLC timestamp that last wrote it.
25
+ */
26
+ export interface ColumnEntry {
27
+ value: ColumnValue;
28
+ hlc: string;
29
+ }
30
+ export interface UpsertOp {
31
+ type: 'upsert';
32
+ table: string;
33
+ rowId: string;
34
+ columns: Record<string, ColumnEntry>;
35
+ }
36
+ export interface DeleteOp {
37
+ type: 'delete';
38
+ table: string;
39
+ rowId: string;
40
+ hlc: string;
41
+ }
42
+ export type Op = UpsertOp | DeleteOp;
43
+ /**
44
+ * Serialized batch of CRDT operations written to a device-specific change log.
45
+ */
46
+ export interface ChangeEntry {
47
+ id: string;
48
+ ts: number;
49
+ device: string;
50
+ user?: string;
51
+ hlc: string;
52
+ ops: Op[];
53
+ }
54
+ /**
55
+ * CRDT row metadata. Lives under `Row._meta` and is fully isolated from
56
+ * user payload. Engine never reads or writes user-controlled fields here.
57
+ */
58
+ export interface RowMeta {
59
+ table: string;
60
+ rowId: string;
61
+ deleted: boolean;
62
+ deletedHlc?: string;
63
+ schemaVersion: number;
64
+ /** Device ID that last wrote this row. Set automatically on every write. */
65
+ owner?: string;
66
+ /** Composite IndexedDB key. Computed by local-store on putRow. */
67
+ key?: string;
68
+ }
69
+ /**
70
+ * Row representation as stored in the local CRDT cache.
71
+ *
72
+ * Two namespaces:
73
+ * - `_meta` — engine-owned metadata. Reserved.
74
+ * - `payload` — user columns, each wrapped in a `ColumnEntry` ({value, hlc}).
75
+ *
76
+ * Public table APIs return plain typed objects (see `rowToTyped` in table.ts);
77
+ * this shape is for internal consumers (CRDT merge, flush, compaction, events).
78
+ */
79
+ export interface Row {
80
+ _meta: RowMeta;
81
+ payload: Record<string, ColumnEntry>;
82
+ }
83
+ export interface TableIndexDefinition {
84
+ /** Stable index id used for migration and diagnostics. */
85
+ name: string;
86
+ /** Top-level column name to index (plain row field, e.g. "status"). */
87
+ field: string;
88
+ unique?: boolean;
89
+ }
90
+ /**
91
+ * Built-in column merge strategies (git-style):
92
+ *
93
+ * - `'remote-wins'` — Like git `--theirs`. Remote always overwrites local. Default.
94
+ * - `'lww'` — Last-Writer-Wins. Highest HLC wins.
95
+ * - `'local-wins'` — Like git `--ours`. Keep local value on conflict.
96
+ */
97
+ export type BuiltinMergeStrategy = 'lww' | 'local-wins' | 'remote-wins';
98
+ /**
99
+ * Custom merge function. Receives the local and incoming column entries
100
+ * plus context, returns the winning entry.
101
+ *
102
+ * Called only when both local and remote have a value for the column.
103
+ * Return `local` to keep, `remote` to accept, or a new ColumnEntry to
104
+ * produce a merged result.
105
+ */
106
+ export type MergeFunction = (local: ColumnEntry, remote: ColumnEntry, context: MergeContext) => ColumnEntry;
107
+ export interface MergeContext {
108
+ table: string;
109
+ rowId: string;
110
+ field: string;
111
+ }
112
+ /** Per-column merge strategy — builtin name or custom function. */
113
+ export type MergeStrategy = BuiltinMergeStrategy | MergeFunction;
114
+ /**
115
+ * Table-level merge config.
116
+ *
117
+ * - Set `strategy` for a table-wide default.
118
+ * - Set `fields` to override per column (like `.gitattributes` per file).
119
+ *
120
+ * Unspecified = inherits from {@link DatabaseSchemaDefinition.mergeStrategy},
121
+ * which itself defaults to `'remote-wins'`.
122
+ */
123
+ export interface TableMergeConfig {
124
+ /** Default strategy for all fields in this table. */
125
+ strategy?: MergeStrategy;
126
+ /** Per-field overrides. */
127
+ fields?: Record<string, MergeStrategy>;
128
+ }
129
+ export type SchemaFieldKind = 'string' | 'number' | 'boolean' | 'date' | 'json' | 'enum';
130
+ export type IndexableSchemaFieldKind = Exclude<SchemaFieldKind, 'json'>;
131
+ /** A field descriptor — carries kind, optional index flags, and a phantom TS type. */
132
+ export interface SchemaField<T = unknown, K extends SchemaFieldKind = SchemaFieldKind> {
133
+ readonly kind: K;
134
+ readonly index?: boolean;
135
+ readonly unique?: boolean;
136
+ /** Optional field in app-level typing. Omitted fields are allowed. */
137
+ readonly optional?: true;
138
+ /** @internal phantom — never exists at runtime; typed as T to preserve inference */
139
+ readonly _type: T;
140
+ }
141
+ export type OptionalSchemaField<T = unknown, K extends SchemaFieldKind = SchemaFieldKind> = SchemaField<T, K> & {
142
+ readonly optional: true;
143
+ readonly __optional: true;
144
+ };
145
+ /** Narrows kind to the set that IndexedDB can use as a key. */
146
+ export type IndexableSchemaField<T = unknown> = SchemaField<T, IndexableSchemaFieldKind>;
147
+ /**
148
+ * Schema metadata for a single table, including field kinds and local indexes.
149
+ *
150
+ * @typeParam T — plain record type for rows in this table, e.g.
151
+ * `{ title: string; status: 'open' | 'done' }`.
152
+ * When omitted, defaults to `Record<string, unknown>` (untyped).
153
+ */
154
+ export interface TableSchemaDefinition<T extends Record<string, unknown> = Record<string, unknown>> {
155
+ /**
156
+ * Production style: define field kind + index intent in one place.
157
+ * Keys must match keys of T; values carry the phantom TS type via SchemaField<T[K]>.
158
+ */
159
+ fields?: {
160
+ [K in keyof T]?: SchemaField<T[K]>;
161
+ } & Record<string, SchemaField>;
162
+ /** Legacy style: kept for compatibility. */
163
+ indexes?: TableIndexDefinition[];
164
+ /** Merge strategy for this table. Overrides the database-level default. */
165
+ merge?: MergeStrategy | TableMergeConfig;
166
+ }
167
+ /**
168
+ * Schema definition used for local query planning and logical compatibility checks.
169
+ *
170
+ * @typeParam S — database shape: `{ tableName: { fieldName: FieldType } }`.
171
+ * Inferred automatically when you pass a schema literal to `SyncConfig`.
172
+ * Omit for untyped usage.
173
+ *
174
+ * @example
175
+ * const schema = {
176
+ * tables: {
177
+ * tasks: { fields: { title: types.string, status: types.enum('open', 'done') } },
178
+ * },
179
+ * } satisfies DatabaseSchemaDefinition;
180
+ * // → DatabaseSchemaDefinition<{ tasks: { title: string; status: 'open' | 'done' } }>
181
+ */
182
+ export interface DatabaseSchemaDefinition<S extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>> {
183
+ /** Optional logical schema version for app-level compatibility checks. */
184
+ version?: number;
185
+ tables: {
186
+ [K in keyof S]: TableSchemaDefinition<S[K]>;
187
+ } & Record<string, TableSchemaDefinition>;
188
+ /** Default merge strategy for all tables. Default: `'remote-wins'`. */
189
+ mergeStrategy?: MergeStrategy;
190
+ }
191
+ /**
192
+ * Extracts the TS type encoded in a `SchemaField`'s `_type` phantom.
193
+ * Falls back to `unknown` for untyped fields.
194
+ * @internal
195
+ */
196
+ export type InferFieldType<F> = F extends SchemaField<infer T> ? T : unknown;
197
+ /**
198
+ * Extracts the row type for a single `TableSchemaDefinition`-shaped object.
199
+ * Works on literal `typeof table` shapes — no generic parameter needed.
200
+ * @internal
201
+ */
202
+ type OptionalFieldKeys<F> = {
203
+ [K in keyof F]-?: F[K] extends {
204
+ optional: true;
205
+ } ? K : never;
206
+ }[keyof F];
207
+ type RequiredFieldKeys<F> = Exclude<keyof F, OptionalFieldKeys<F>>;
208
+ export type InferTableShape<T> = T extends {
209
+ fields: infer F;
210
+ } ? ({
211
+ [K in RequiredFieldKeys<F>]: InferFieldType<F[K]>;
212
+ } & {
213
+ [K in OptionalFieldKeys<F>]?: InferFieldType<F[K]>;
214
+ }) : Record<string, unknown>;
215
+ /**
216
+ * Extracts the full database shape from a `DatabaseSchemaDefinition`-shaped literal.
217
+ *
218
+ * Works directly on `typeof schema` — no manual type annotation needed.
219
+ *
220
+ * @example
221
+ * const schema = {
222
+ * tables: { tasks: { fields: { title: types.string, status: types.enum('open', 'done') } } },
223
+ * } satisfies DatabaseSchemaDefinition;
224
+ *
225
+ * type DB = InferSchemaType<typeof schema>;
226
+ * // → { tasks: { title: string; status: 'open' | 'done' } }
227
+ *
228
+ * type TaskRow = InferTableType<typeof schema, 'tasks'>;
229
+ * // → { title: string; status: 'open' | 'done' }
230
+ */
231
+ export type InferSchemaType<D extends DatabaseSchemaDefinition> = D extends {
232
+ tables: infer Tables;
233
+ } ? {
234
+ [K in keyof Tables]: InferTableShape<Tables[K]>;
235
+ } : Record<string, Record<string, unknown>>;
236
+ /**
237
+ * Extracts the row type for a single table `K` from a `DatabaseSchemaDefinition`.
238
+ */
239
+ export type InferTableType<D extends DatabaseSchemaDefinition, K extends keyof InferSchemaType<D> & string> = InferSchemaType<D>[K];
240
+ export type WherePrimitive = string | number | boolean | Date;
241
+ export type WhereOperator = 'equals' | 'above' | 'aboveOrEqual' | 'below' | 'belowOrEqual' | 'between' | 'startsWith' | 'anyOf';
242
+ /**
243
+ * Dexie-style predicate description used by {@link Table.where} and
244
+ * {@link Interocitor.queryWhere}.
245
+ */
246
+ export interface WhereClause {
247
+ field: string;
248
+ op: WhereOperator;
249
+ value?: WherePrimitive;
250
+ values?: WherePrimitive[];
251
+ lower?: WherePrimitive;
252
+ upper?: WherePrimitive;
253
+ lowerOpen?: boolean;
254
+ upperOpen?: boolean;
255
+ }
256
+ export interface QueryOrderBy {
257
+ field: string;
258
+ dir: 'asc' | 'desc';
259
+ }
260
+ export interface QueryDescriptor {
261
+ table: string;
262
+ clause?: WhereClause;
263
+ orderBy?: QueryOrderBy;
264
+ }
265
+ /** Identity of a single-row read. Lives next to QueryDescriptor on purpose. */
266
+ export interface RowDescriptor {
267
+ table: string;
268
+ rowId: string;
269
+ }
270
+ export interface RowCacheSnapshot {
271
+ status: 'empty' | 'pending' | 'ready' | 'error';
272
+ promise: Promise<Row | undefined> | null;
273
+ /** `null` means "loaded, row absent/deleted". `undefined` means "no rows yet". */
274
+ row?: Row | null;
275
+ error?: Error;
276
+ }
277
+ export interface RowCacheOwner {
278
+ getRowCacheKey(descriptor: RowDescriptor): string;
279
+ loadRow(descriptor: RowDescriptor, options?: QueryExecutionOptions): Promise<Row | undefined>;
280
+ readRowCache(descriptor: RowDescriptor): RowCacheSnapshot;
281
+ }
282
+ export interface QueryExecutionOptions {
283
+ bypassCache?: boolean;
284
+ }
285
+ export interface QueryCacheSnapshot {
286
+ status: 'empty' | 'pending' | 'ready' | 'error';
287
+ promise: Promise<Row[]> | null;
288
+ rows?: Row[];
289
+ error?: Error;
290
+ }
291
+ export interface QueryCacheOwner {
292
+ getQueryCacheKey(descriptor: QueryDescriptor): string;
293
+ loadQueryRows(descriptor: QueryDescriptor, options?: QueryExecutionOptions): Promise<Row[]>;
294
+ readQueryCache(descriptor: QueryDescriptor): QueryCacheSnapshot;
295
+ }
296
+ export interface ReadinessAwareQueryExecutor extends QueryCacheOwner, RowCacheOwner {
297
+ isReady(): boolean;
298
+ }
299
+ export type QueryExecutionMode = 'default' | 'cache-first' | 'bypass-cache';
300
+ export interface QueryExecutionPolicy {
301
+ mode?: QueryExecutionMode;
302
+ }
303
+ export interface QueryMetadata {
304
+ descriptor: QueryDescriptor;
305
+ cacheKey: string;
306
+ }
307
+ export interface QueryReadable<T extends Record<string, unknown>> {
308
+ load(options?: QueryExecutionOptions): Promise<T[]>;
309
+ peekCache(): T[] | undefined;
310
+ readonly metadata: QueryMetadata;
311
+ }
312
+ export interface QueryReadyReadable<T extends Record<string, unknown>> extends QueryReadable<T> {
313
+ readForRender(policy?: QueryExecutionPolicy): Promise<T[]> | T[];
314
+ }
315
+ export interface QueryRuntime<T extends Record<string, unknown>> {
316
+ owner: QueryCacheOwner;
317
+ metadata: QueryMetadata;
318
+ load: (options?: QueryExecutionOptions) => Promise<T[]>;
319
+ }
320
+ export interface QueryReadyRuntime<T extends Record<string, unknown>> extends QueryRuntime<T> {
321
+ owner: ReadinessAwareQueryExecutor;
322
+ readForRender: (policy?: QueryExecutionPolicy) => Promise<T[]> | T[];
323
+ }
324
+ export interface QuerySubscriber {
325
+ subscribe(cb: TableEventListener<any>): () => void;
326
+ }
327
+ export type QueryLifecycle<T extends Record<string, unknown>> = QueryRuntime<T> & QuerySubscriber;
328
+ export type ReadyQueryLifecycle<T extends Record<string, unknown>> = QueryReadyRuntime<T> & QuerySubscriber;
329
+ /**
330
+ * Compacted mesh snapshot containing the full row set at a given epoch.
331
+ */
332
+ export interface Snapshot {
333
+ snapshotId: string;
334
+ timestamp: string;
335
+ hlc: string;
336
+ epoch: number;
337
+ schemaVersion: number;
338
+ tables: Record<string, Record<string, Row>>;
339
+ }
340
+ export interface MeshChangePayload {
341
+ meshId: string;
342
+ kind: 'change';
343
+ entry: ChangeEntry;
344
+ }
345
+ export interface MeshSnapshotPayload {
346
+ meshId: string;
347
+ kind: 'snapshot';
348
+ snapshot: Snapshot;
349
+ }
350
+ export interface ServerConfig {
351
+ managed: boolean;
352
+ relayUrl: string | null;
353
+ serverId: string;
354
+ }
355
+ export interface ManifestPointer {
356
+ currentGeneration: number;
357
+ file: string;
358
+ }
359
+ /**
360
+ * Authoritative mesh manifest describing the latest generation and snapshot state.
361
+ */
362
+ export interface Manifest {
363
+ generation: number;
364
+ parentGeneration: number;
365
+ writtenBy: string;
366
+ writtenAt: string;
367
+ contentHash: string;
368
+ version: number;
369
+ meshId: string;
370
+ schema: number;
371
+ encrypted: boolean;
372
+ server: ServerConfig;
373
+ createdAt: string;
374
+ /** Compaction epoch — incremented on each snapshot. */
375
+ epoch: number;
376
+ /** HLC watermark — all data ≤ this HLC is captured in the snapshot. */
377
+ watermarkHlc: string;
378
+ /** Cloud path to the latest snapshot file, or null before first compaction. */
379
+ snapshotPath: string | null;
380
+ /** Reserved for future delta-based catch-up. */
381
+ deltaPath: string | null;
382
+ /**
383
+ * Point of no return for this mesh. Local or remote change entries at or
384
+ * before this HLC are considered captured by canonical snapshots and must
385
+ * not be flushed by stale clients.
386
+ */
387
+ gcFloorHlc?: string;
388
+ /** Epoch that published gcFloorHlc. */
389
+ gcEpoch?: number;
390
+ /** Timestamp when gcFloorHlc was computed. */
391
+ gcCreatedAt?: string;
392
+ /** Offline grace used to compute the active device set. */
393
+ offlineGraceMs?: number;
394
+ }
395
+ export type DeviceType = 'web' | 'ios' | 'android' | 'worker' | 'desktop' | 'tv';
396
+ export interface DeviceMetadata extends DeviceInfo {
397
+ registeredAt: string;
398
+ lastSeenAt: string;
399
+ /** Human-readable device name, e.g. "Anton's laptop" */
400
+ displayName?: string;
401
+ /** Device class */
402
+ deviceType?: DeviceType;
403
+ retired?: boolean;
404
+ /** Latest manifest generation this device has fully observed. */
405
+ observedManifestGeneration?: number;
406
+ /** Latest snapshot epoch this device has fully observed. */
407
+ observedEpoch?: number;
408
+ /** Latest manifest watermark this device has fully observed. */
409
+ observedWatermarkHlc?: string;
410
+ /** Latest manifest GC floor this device has accepted. */
411
+ observedGcFloorHlc?: string;
412
+ /** Timestamp of the observation acknowledgement. */
413
+ observedAt?: string;
414
+ /** Timestamp when this device was excluded from the active set. */
415
+ cutOffAt?: string;
416
+ cutOffReason?: 'offline-grace-expired' | 'manual-retire';
417
+ }
418
+ export interface DeviceHead {
419
+ device: string;
420
+ latestHlc: string;
421
+ latestDate: string;
422
+ fileCount: number;
423
+ }
424
+ /** Global change-folder head — monotonic HLC hint for fast poll skipping. */
425
+ export interface ChangesHead {
426
+ latestHlc: string;
427
+ }
428
+ /**
429
+ * Normalized remote file metadata returned by a storage adapter.
430
+ */
431
+ export interface FileEntry {
432
+ name: string;
433
+ path: string;
434
+ size: number;
435
+ modifiedTime: string;
436
+ etag?: string;
437
+ revision?: string;
438
+ }
439
+ /** Metadata for durable application files stored beside a mesh. */
440
+ export interface StoredFileMetadata extends FileEntry {
441
+ /** Device that uploaded the current object version. */
442
+ uploadedByDeviceId?: string;
443
+ /** ISO timestamp for the current object version upload. */
444
+ uploadedAt?: string;
445
+ /** Last successful read timestamp, when tracked by the backend. */
446
+ lastAccessedAt?: string;
447
+ /** Total successful reads, when tracked by the backend. */
448
+ useCount?: number;
449
+ /** Original plaintext byte length, when known. */
450
+ plaintextSize?: number;
451
+ /** Stored ciphertext/transport byte length. Defaults to size. */
452
+ storedSize?: number;
453
+ /** Application content type, if provided by the uploader. */
454
+ contentType?: string;
455
+ }
456
+ export interface StoredFileWriteOptions {
457
+ /** Device identity to persist for abuse controls/audit. */
458
+ uploadedByDeviceId?: string;
459
+ /** Plaintext byte length before encryption. */
460
+ plaintextSize?: number;
461
+ /** Application content type. */
462
+ contentType?: string;
463
+ }
464
+ export type ImageInput = Blob | ArrayBuffer | Uint8Array | string;
465
+ export interface PutImageOptions {
466
+ /** Override content type. Defaults to Blob/File type or data URL media type. */
467
+ contentType?: string;
468
+ }
469
+ export interface StoredImageMetadata extends StoredFileMetadata {
470
+ contentType: string;
471
+ }
472
+ export interface StoredImage {
473
+ path: string;
474
+ data: Uint8Array;
475
+ blob: Blob;
476
+ metadata: StoredImageMetadata | null;
477
+ contentType: string;
478
+ }
479
+ export interface StoredImageBlobUrl {
480
+ path: string;
481
+ url: string;
482
+ blob: Blob;
483
+ metadata: StoredImageMetadata | null;
484
+ contentType: string;
485
+ revoke(): void;
486
+ }
487
+ export interface RemoteInvalidationPayload {
488
+ type: string;
489
+ path: string;
490
+ ts: number;
491
+ op?: string;
492
+ pathType?: string;
493
+ }
494
+ export interface RemoteInvalidationHooks {
495
+ onReady?: () => void;
496
+ onError?: (error?: unknown) => void;
497
+ onClose?: () => void;
498
+ }
499
+ export interface RemoteInvalidationStorageAdapter {
500
+ subscribeToInvalidations(onInvalidate: (payload: RemoteInvalidationPayload) => void, hooks?: RemoteInvalidationHooks): () => void;
501
+ }
502
+ /**
503
+ * Contract implemented by remote backends such as WebDAV, Google Drive,
504
+ * Cloudflare, or in-memory test adapters.
505
+ */
506
+ export interface StorageAdapter {
507
+ readonly name: string;
508
+ authenticate(): Promise<void>;
509
+ isAuthenticated(): boolean;
510
+ ensureFolder(path: string): Promise<void>;
511
+ listFiles(path: string): Promise<FileEntry[]>;
512
+ listFolders(path: string): Promise<string[]>;
513
+ readFile(path: string): Promise<Uint8Array>;
514
+ writeFile(path: string, data: Uint8Array | string): Promise<void>;
515
+ deleteFile(path: string): Promise<void>;
516
+ getFileMetadata(path: string): Promise<FileEntry | null>;
517
+ putStoredFile?(path: string, data: Uint8Array | string, options?: StoredFileWriteOptions): Promise<StoredFileMetadata>;
518
+ getStoredFile?(path: string): Promise<Uint8Array>;
519
+ deleteStoredFile?(path: string): Promise<void>;
520
+ getStoredFileMetadata?(path: string): Promise<StoredFileMetadata | null>;
521
+ /**
522
+ * Return an opaque config string describing how to reach this backend,
523
+ * suitable for embedding in a handshake QR code payload.
524
+ *
525
+ * Must NOT include credentials (passwords, tokens, OAuth secrets).
526
+ * The scanner uses this to configure their own adapter instance before
527
+ * starting the ECDH relay exchange.
528
+ *
529
+ * Returns undefined for adapters where the backend address is already
530
+ * baked into the app (e.g. a fixed Cloudflare Worker URL known to all
531
+ * app users). In that case the scanner configures their adapter independently.
532
+ *
533
+ * The returned string should be treated as opaque by the handshake layer;
534
+ * only the same adapter class knows how to parse it.
535
+ */
536
+ getHandshakeConfig?(): string;
537
+ /**
538
+ * Drop the per-session ensureFolder cache. Implementers cache "ensured"
539
+ * paths to avoid round-tripping a MKCOL/POST per connect; the engine
540
+ * calls this on mesh swap, transport teardown, and remote poison so the
541
+ * next connect re-validates folder presence on the new backend.
542
+ *
543
+ * Optional. Adapters that do no caching can omit it.
544
+ */
545
+ resetFolderCache?(): void;
546
+ }
547
+ /**
548
+ * Contract every local store implementation must satisfy.
549
+ * Implement this interface to plug in a custom local backend
550
+ * (e.g. in-memory for tests, SQLite via OPFS, etc.).
551
+ */
552
+ export interface LocalStoreAdapter {
553
+ open(): Promise<void>;
554
+ close(): void;
555
+ getRow(table: string, rowId: string): Promise<Row | undefined>;
556
+ putRow(row: Row): Promise<void>;
557
+ putRows(rows: Row[]): Promise<void>;
558
+ getTable(table: string): Promise<Row[]>;
559
+ queryWhere(table: string, clause: WhereClause): Promise<Row[]>;
560
+ getAllRows(): Promise<Row[]>;
561
+ clearRows(): Promise<void>;
562
+ getTableNames(): Promise<string[]>;
563
+ pushOutbox(entry: ChangeEntry): Promise<void>;
564
+ pushOutboxEntries(entries: ChangeEntry[]): Promise<void>;
565
+ drainOutbox(): Promise<ChangeEntry[]>;
566
+ outboxSize(): Promise<number>;
567
+ getCursor(deviceId: string): Promise<number>;
568
+ setCursor(deviceId: string, offset: number): Promise<void>;
569
+ getAllCursors(): Promise<Record<string, number>>;
570
+ getMeta(key: string): Promise<unknown>;
571
+ setMeta(key: string, value: unknown): Promise<void>;
572
+ clearAll(): Promise<void>;
573
+ }
574
+ /** Factory that creates a local store instance for this engine. */
575
+ export type LocalStoreFactory = () => LocalStoreAdapter;
576
+ /** Write-only replica adapter for backup. */
577
+ export interface ReplicaConfig {
578
+ adapter: StorageAdapter;
579
+ /** Override remotePath for this replica. Defaults to the primary remotePath. */
580
+ remotePath?: string;
581
+ }
582
+ /**
583
+ * Configuration for a {@link Interocitor} instance.
584
+ *
585
+ * Supports both fully local startup and immediate sync with a remote adapter.
586
+ */
587
+ export interface SyncInitialState {
588
+ remotePath?: string;
589
+ passphrase?: string | null;
590
+ encrypted?: boolean;
591
+ deviceId?: string;
592
+ }
593
+ export type LogLevel = import('./internals.ts').LogLevel;
594
+ export interface SyncConfig<S extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>> {
595
+ /** Cloud folder path prefix, e.g. "/Interocitor" */
596
+ remotePath?: string;
597
+ /**
598
+ * Base58 passphrase for mesh encryption.
599
+ * When set, the engine derives the AES-256 key internally and persists
600
+ * it via the credential store. Implies encrypted = true.
601
+ */
602
+ passphrase?: string;
603
+ /**
604
+ * Encryption is on by default. Set to false to opt out.
605
+ * When enabled without a passphrase, the engine generates a fresh key
606
+ * on first init (retrieve via getPassphrase()).
607
+ */
608
+ encrypted?: boolean;
609
+ /**
610
+ * Override the auto-generated device ID.
611
+ * Primarily for tests. In production, omit — the engine generates
612
+ * and persists a unique ID per origin automatically.
613
+ */
614
+ deviceId?: string;
615
+ /** Human-readable device name, e.g. "Anton's laptop", "Val's phone" */
616
+ deviceName?: string;
617
+ /** Device class — used in device manifest for peer discovery */
618
+ deviceType?: DeviceType;
619
+ /** If true, only serverId may publish manifests/compaction */
620
+ serverManaged?: boolean;
621
+ /** Authorized writer identity when serverManaged=true */
622
+ serverId?: string;
623
+ /** Polling interval in ms (default 30000) */
624
+ pollInterval?: number;
625
+ /** Enable relay/WebSocket invalidations when supported. Default true. */
626
+ relayEnabled?: boolean;
627
+ /** Poll interval to use while relay is healthy. Default max(pollInterval, 300000). */
628
+ relayHealthyPollInterval?: number;
629
+ /** Per-engine log threshold. Default: 'info'. */
630
+ logLevel?: LogLevel;
631
+ /** Flush debounce in ms (default 2000) */
632
+ flushDebounce?: number;
633
+ /** Max pending ops before forced flush (default 50) */
634
+ flushThreshold?: number;
635
+ /** Warn once queued local changes reach this count (default 50). */
636
+ compactWarnThreshold?: number;
637
+ /** Consider auto-compaction once queued local changes reach this count (default 50). */
638
+ compactAutoThreshold?: number;
639
+ /** Sampling numerator for the immediate auto-compact path. Chance = numerator / estimated device count. Default 10. */
640
+ compactAutoSampleNumerator?: number;
641
+ /** Estimated device count used to scale the immediate auto-compact sampling. Default 1. */
642
+ compactAutoDeviceCount?: number;
643
+ /** Enable automatic compact scheduling after large churn. Default true. */
644
+ autoCompact?: boolean;
645
+ /** First auto-compact delay base in ms (default 10m). Jittered by ± firstCompactDelayJitterMs. */
646
+ firstCompactDelayMs?: number;
647
+ /** First auto-compact delay jitter in ms (default 5m). */
648
+ firstCompactDelayJitterMs?: number;
649
+ /** Second auto-compact delay base in ms (default 15m). Jittered by ± secondCompactDelayJitterMs. */
650
+ secondCompactDelayMs?: number;
651
+ /** Second auto-compact delay jitter in ms (default 5m). */
652
+ secondCompactDelayJitterMs?: number;
653
+ /** Minimum remote change-file count required before the second delay starts (default 2). */
654
+ compactRemoteChangeThreshold?: number;
655
+ /**
656
+ * How long an unseen device remains part of compaction/tombstone-GC
657
+ * consensus. Devices older than this are excluded from the active set and
658
+ * must align from the current snapshot before writing again. Default 7 days.
659
+ */
660
+ offlineGraceMs?: number;
661
+ /** Implicit batch window in ms. All local writes inside the window join one ChangeEntry. Default 1000. */
662
+ batchWindowMs?: number;
663
+ /**
664
+ * Local database name for this engine's local cache.
665
+ * Use distinct names to isolate multiple engine instances on the same origin.
666
+ * Default: "interocitor"
667
+ */
668
+ dbName?: string;
669
+ /**
670
+ * Factory that produces the local store for this engine.
671
+ * When provided, dbName is ignored — the factory is fully responsible
672
+ * for constructing the store.
673
+ */
674
+ localStoreFactory?: LocalStoreFactory;
675
+ /** Optional table/index metadata for local query planning and migrations. */
676
+ schema?: DatabaseSchemaDefinition<S>;
677
+ /**
678
+ * Optional browser-owned bootstrap hook.
679
+ * Runs during init() before persisted credentials are restored.
680
+ * Returned values override constructor defaults; persisted storage fills blanks only.
681
+ */
682
+ resolveInitialState?: () => SyncInitialState | Promise<SyncInitialState | null> | null;
683
+ /**
684
+ * Called once after the engine has fully initialized (local store open,
685
+ * encryption resolved, local state loaded). Use for migrations.
686
+ *
687
+ * @example
688
+ * onInit: async (engine) => {
689
+ * await migrateLegacyData(engine);
690
+ * }
691
+ */
692
+ onInit?: (engine: import('./sync-engine.ts').InterocitorInitContext<S>) => Promise<void>;
693
+ /**
694
+ * Write-only replica adapters for backup.
695
+ * Flush writes to primary + all replicas. Pull reads primary only.
696
+ * Replica failures are emitted as 'replica:error' events but do not
697
+ * fail the primary flush.
698
+ */
699
+ replicas?: ReplicaConfig[];
700
+ /**
701
+ * Credential store for persisting key material (passphrase + device ID).
702
+ *
703
+ * Default: auto-detecting store that tries WebAuthn largeBlob (OS keychain,
704
+ * survives Safari ITP) and falls back to localStorage.
705
+ *
706
+ * Pass a custom `CredentialStore` implementation or `null` to disable
707
+ * credential persistence entirely.
708
+ */
709
+ credentialStore?: import('../storage/credential-store.ts').CredentialStore | null;
710
+ /**
711
+ * Human-readable app name shown in biometric prompts (Touch ID / Face ID)
712
+ * and OS keychain entries. Used by the default credential store.
713
+ */
714
+ appName: string;
715
+ }
716
+ /**
717
+ * Union of lifecycle, sync, auth, and replication events emitted by the engine.
718
+ */
719
+ export type SyncEvent = {
720
+ type: 'sync:start';
721
+ } | {
722
+ type: 'sync:complete';
723
+ entriesMerged: number;
724
+ } | {
725
+ type: 'sync:error';
726
+ error: Error;
727
+ } | {
728
+ type: 'credentials:restored';
729
+ source: 'silent-store' | 'biometric';
730
+ deviceIdChanged: boolean;
731
+ hadPassphrase: boolean;
732
+ } | {
733
+ type: 'remote:poisoned';
734
+ error: Error;
735
+ path?: string;
736
+ context?: Record<string, unknown>;
737
+ } | {
738
+ type: 'decode:error';
739
+ error: Error;
740
+ path?: string;
741
+ context?: Record<string, unknown>;
742
+ } | {
743
+ type: 'credentials:conflict';
744
+ storedDeviceId: string;
745
+ activeDeviceId: string;
746
+ dbName: string;
747
+ remotePath?: string;
748
+ } | {
749
+ type: 'credentials:meshMismatch';
750
+ dbName: string;
751
+ remotePath?: string;
752
+ storedMeshId: string;
753
+ activeMeshId: string;
754
+ } | {
755
+ type: 'credentials:persisted';
756
+ dbName: string;
757
+ remotePath?: string;
758
+ deviceId: string;
759
+ encrypted: boolean;
760
+ } | {
761
+ type: 'encryption:resolved';
762
+ strategy: 'passphrase' | 'existing-key' | 'generated';
763
+ dbName: string;
764
+ remotePath?: string;
765
+ encrypted: boolean;
766
+ } | {
767
+ type: 'mesh:configured';
768
+ dbName: string;
769
+ remotePath?: string;
770
+ deviceId: string;
771
+ encrypted: boolean;
772
+ hadPassphrase: boolean;
773
+ } | {
774
+ type: 'connect:state';
775
+ dbName: string;
776
+ remotePath?: string;
777
+ deviceId: string;
778
+ localEpoch?: number;
779
+ remoteEpoch?: number;
780
+ meshId?: string;
781
+ encrypted: boolean;
782
+ } | {
783
+ type: 'connect:noop';
784
+ dbName: string;
785
+ remotePath?: string;
786
+ deviceId: string;
787
+ reason: 'already-connected';
788
+ } | {
789
+ type: 'connect:error';
790
+ error: Error;
791
+ stage: string;
792
+ dbName: string;
793
+ remotePath?: string;
794
+ deviceId: string;
795
+ } | {
796
+ type: 'transport:teardown';
797
+ dbName: string;
798
+ remotePath?: string;
799
+ deviceId?: string;
800
+ reason: 'switch-adapter' | 'disconnect' | 'detach';
801
+ } | {
802
+ type: 'relay:subscribe';
803
+ adapter: string;
804
+ remotePath?: string;
805
+ deviceId: string;
806
+ } | {
807
+ type: 'relay:ready';
808
+ adapter: string;
809
+ } | {
810
+ type: 'relay:message';
811
+ adapter: string;
812
+ payload: RemoteInvalidationPayload;
813
+ } | {
814
+ type: 'relay:error';
815
+ adapter: string;
816
+ error: Error;
817
+ } | {
818
+ type: 'relay:closed';
819
+ adapter: string;
820
+ } | {
821
+ type: 'relay:unavailable';
822
+ adapter: string;
823
+ reason: 'adapter-unsupported' | 'disabled';
824
+ } | {
825
+ type: 'flush:start';
826
+ entryCount: number;
827
+ } | {
828
+ type: 'flush:complete';
829
+ } | {
830
+ type: 'flush:error';
831
+ error: Error;
832
+ } | {
833
+ type: 'compact:warning';
834
+ queuedChangeCount: number;
835
+ threshold: number;
836
+ autoCompactThreshold: number;
837
+ remotePath?: string;
838
+ deviceId: string;
839
+ } | {
840
+ type: 'compact:auto:start';
841
+ queuedChangeCount: number;
842
+ threshold: number;
843
+ sampleRoll?: number;
844
+ sampleWindow?: number;
845
+ remoteChangeFileCount?: number;
846
+ trigger: 'immediate' | 'delayed';
847
+ remotePath?: string;
848
+ deviceId: string;
849
+ } | {
850
+ type: 'compact:auto:skip';
851
+ queuedChangeCount: number;
852
+ threshold: number;
853
+ sampleRoll?: number;
854
+ sampleWindow?: number;
855
+ trigger: 'immediate' | 'delayed';
856
+ remotePath?: string;
857
+ deviceId: string;
858
+ reason: 'sampling' | 'disabled' | 'not-connected' | 'already-running' | 'poisoned' | 'missing-remote' | 'below-remote-threshold' | 'superseded';
859
+ } | {
860
+ type: 'compact:auto:complete';
861
+ queuedChangeCount: number;
862
+ threshold: number;
863
+ trigger: 'immediate' | 'delayed';
864
+ remoteChangeFileCount?: number;
865
+ remotePath?: string;
866
+ deviceId: string;
867
+ } | {
868
+ type: 'compact:auto:error';
869
+ queuedChangeCount: number;
870
+ threshold: number;
871
+ trigger: 'immediate' | 'delayed';
872
+ remoteChangeFileCount?: number;
873
+ remotePath?: string;
874
+ deviceId: string;
875
+ error: Error;
876
+ } | {
877
+ type: 'compact:delayed:scheduled';
878
+ queuedChangeCount: number;
879
+ delayMs: number;
880
+ phase: 'check' | 'compact';
881
+ remotePath?: string;
882
+ deviceId: string;
883
+ } | {
884
+ type: 'compact:delayed:check';
885
+ queuedChangeCount: number;
886
+ remoteChangeFileCount: number;
887
+ threshold: number;
888
+ remotePath?: string;
889
+ deviceId: string;
890
+ } | {
891
+ type: 'change';
892
+ table: string;
893
+ rowId: string;
894
+ row: Row;
895
+ } | {
896
+ type: 'delete';
897
+ table: string;
898
+ rowId: string;
899
+ } | {
900
+ type: 'rehydrate:start';
901
+ } | {
902
+ type: 'rehydrate:complete';
903
+ rowCount: number;
904
+ } | {
905
+ type: 'auth:required';
906
+ } | {
907
+ type: 'auth:complete';
908
+ } | {
909
+ type: 'schema:mismatch';
910
+ local: number;
911
+ remote: number;
912
+ } | {
913
+ type: 'replica:error';
914
+ adapter: string;
915
+ error: Error;
916
+ } | {
917
+ type: 'trace:manifest';
918
+ op: 'read' | 'write' | 'cache-hit' | 'bootstrap-create';
919
+ reason: string;
920
+ generation?: number;
921
+ path?: string;
922
+ cached?: boolean;
923
+ } | {
924
+ type: 'trace:head';
925
+ op: 'read' | 'write' | 'skip-no-change';
926
+ reason: string;
927
+ path?: string;
928
+ priorHlc?: string | null;
929
+ nextHlc?: string | null;
930
+ regressed?: boolean;
931
+ };
932
+ /**
933
+ * Listener callback registered with {@link Interocitor.on}.
934
+ */
935
+ export type SyncEventListener = (event: SyncEvent) => void;
936
+ /**
937
+ * Event emitted by table-level subscriptions.
938
+ */
939
+ export type TableEvent<T> = {
940
+ type: 'change';
941
+ rowId: string;
942
+ row: T;
943
+ } | {
944
+ type: 'delete';
945
+ rowId: string;
946
+ };
947
+ /**
948
+ * Listener callback for table-level subscriptions.
949
+ */
950
+ export type TableEventListener<T> = (event: TableEvent<T>) => void;
951
+ export {};
952
+ //# sourceMappingURL=types.d.ts.map