@xnetjs/runtime 0.0.1

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.
@@ -0,0 +1,1020 @@
1
+ import * as Y from 'yjs';
2
+ import { ContentId, DID, PolicyEvaluator, AuthCheckInput, AuthDecision } from '@xnetjs/core';
3
+ import { NodeStore, NodeStorageAdapter, NodeContentCipher, StoreAuthAPI, SchemaLookup, LensRegistry, PropertyBuilder, DefinedSchema, NodeState, TransactionOperation, NodeChange, NodePayload } from '@xnetjs/data';
4
+ import { SyncLifecycleState, SyncReplicationConfig, ChangeSigner } from '@xnetjs/sync';
5
+ export { SyncLifecyclePhase, SyncLifecycleState } from '@xnetjs/sync';
6
+ import { Awareness } from 'y-protocols/awareness';
7
+ import { DataBridge, MainThreadBridgeOptions, SyncStatus as SyncStatus$1, QueryOptions, BridgeTransactionResult, AcquiredDoc } from '@xnetjs/data-bridge';
8
+ import { Identity } from '@xnetjs/identity';
9
+ import { Platform, PluginRegistry } from '@xnetjs/plugins';
10
+ import { UndoManager } from '@xnetjs/history';
11
+
12
+ /**
13
+ * Connection Manager - Multiplexed WebSocket connection for all tracked Nodes
14
+ *
15
+ * Instead of one WebSocket per Node (current WebSocketSyncProvider approach),
16
+ * the Connection Manager maintains a single WebSocket subscribed to multiple
17
+ * rooms. This reduces connection count from O(N) to O(1).
18
+ *
19
+ * The signaling protocol supports multi-room subscriptions:
20
+ * { type: "subscribe", topics: ["xnet-doc-abc", "xnet-doc-def"] }
21
+ */
22
+ type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
23
+ interface ConnectionManagerConfig {
24
+ /** Signaling/hub WebSocket URL */
25
+ url: string;
26
+ /** Base reconnect delay in ms (default: 2000); grows exponentially per attempt. */
27
+ reconnectDelay?: number;
28
+ /**
29
+ * Cap on the exponentially-backed-off reconnect delay (default: 30000).
30
+ * Backoff is `min(reconnectDelay * 2^(attempt-1), maxReconnectDelay)`.
31
+ */
32
+ maxReconnectDelay?: number;
33
+ /** Max reconnect attempts (default: Infinity) */
34
+ maxReconnects?: number;
35
+ /**
36
+ * Backoff after a policy-violation close (WebSocket code 1008, e.g. the hub's
37
+ * "Rate limit exceeded"): a longer, jittered delay so we don't tight-loop or
38
+ * stampede the hub that just asked us to stop (default: 15000). Jitter adds
39
+ * up to 50% on top (exploration 0206).
40
+ */
41
+ rateLimitBackoffMs?: number;
42
+ /**
43
+ * Max time to wait for the WebSocket handshake to open before treating the
44
+ * attempt as failed and backing off (default: 10000). A browser WebSocket has
45
+ * no built-in connect timeout, so a stalled handshake can otherwise hang for
46
+ * tens of seconds (exploration 0188). Set to 0 to disable.
47
+ */
48
+ connectTimeout?: number;
49
+ /**
50
+ * Timeout for the FIRST handshake attempt (default: min(connectTimeout,
51
+ * 6000)). A healthy hub handshake completes in well under a second, so the
52
+ * first attempt fails fast and retries instead of pinning the "connecting"
53
+ * indicator for the full connectTimeout on a stalled cold dial; subsequent
54
+ * attempts use the full connectTimeout since the network is evidently slow
55
+ * (exploration 0204). Set to 0 to disable bounding the first attempt.
56
+ */
57
+ initialConnectTimeout?: number;
58
+ /** UCAN token for hub auth */
59
+ ucanToken?: string;
60
+ /** Async UCAN token provider (preferred for rotation) */
61
+ getUCANToken?: () => Promise<string>;
62
+ }
63
+ type RoomHandler = (data: Record<string, unknown>) => void;
64
+ type StatusHandler = (status: ConnectionStatus) => void;
65
+ interface RoomJoinResult {
66
+ /** Unsubscribe function */
67
+ unsubscribe: () => void;
68
+ /** Promise that resolves when server confirms subscription */
69
+ ready: Promise<void>;
70
+ }
71
+ interface ConnectionManager {
72
+ /** Current connection status */
73
+ readonly status: ConnectionStatus;
74
+ /** Connect to the signaling server */
75
+ connect(): void;
76
+ /** Disconnect and cleanup */
77
+ disconnect(): void;
78
+ /** Subscribe to a room (returns unsubscribe function) */
79
+ joinRoom(room: string, handler: RoomHandler): () => void;
80
+ /** Subscribe to a room with confirmation (returns cleanup and ready promise) */
81
+ joinRoomAsync(room: string, handler: RoomHandler): RoomJoinResult;
82
+ /** Leave a room */
83
+ leaveRoom(room: string): void;
84
+ /** Publish a message to a room */
85
+ publish(room: string, data: object): void;
86
+ /** Send a raw message on the WebSocket */
87
+ sendRaw(message: object): void;
88
+ /** Listen for non-room messages */
89
+ onMessage(handler: (message: Record<string, unknown>) => void): () => void;
90
+ /** Listen for status changes */
91
+ onStatus(handler: StatusHandler): () => void;
92
+ /** Number of active room subscriptions */
93
+ readonly roomCount: number;
94
+ }
95
+ interface MultiHubConnectionManagerConfig {
96
+ /** Hub connections to orchestrate as one logical transport. */
97
+ hubs: readonly ConnectionManagerConfig[];
98
+ }
99
+ declare function createConnectionManager(config: ConnectionManagerConfig): ConnectionManager;
100
+ declare function createMultiHubConnectionManager(config: MultiHubConnectionManagerConfig): ConnectionManager;
101
+
102
+ /**
103
+ * Blob Sync Provider - Handles blob synchronization between peers.
104
+ *
105
+ * Integrates with the Background Sync Manager's ConnectionManager to send
106
+ * blob have/want/data messages over the same multiplexed WebSocket connection
107
+ * used for Y.Doc sync. Uses a dedicated room for blob sync messages.
108
+ *
109
+ * Protocol:
110
+ * blob-have: Announce available CIDs to peers
111
+ * blob-want: Request missing blobs by CID
112
+ * blob-data: Transfer blob bytes (base64 encoded)
113
+ * blob-not-found: Signal unavailability
114
+ */
115
+
116
+ /** Minimal blob store interface for sync (satisfied by BlobStore from @xnetjs/storage) */
117
+ interface BlobStoreForSync {
118
+ get(cid: ContentId): Promise<Uint8Array | null>;
119
+ put(data: Uint8Array): Promise<ContentId>;
120
+ has(cid: ContentId): Promise<boolean>;
121
+ }
122
+
123
+ /**
124
+ * Sync Manager - Top-level orchestrator for Background Sync
125
+ *
126
+ * Wires together Node Pool, Registry, and Connection Manager into a cohesive
127
+ * sync service. Handles the Yjs sync protocol (state vectors, diffs, incremental
128
+ * updates) for each tracked Node.
129
+ *
130
+ * Components acquire Y.Docs via the SyncManager (through useNode). When released,
131
+ * docs stay alive in the pool and continue syncing in the background.
132
+ */
133
+
134
+ type SyncStatus = ConnectionStatus;
135
+
136
+ interface SyncManagerConfig {
137
+ /** NodeStore for meta bridge */
138
+ nodeStore: NodeStore;
139
+ /** Storage adapter for pool persistence (Y.Doc content) */
140
+ storage: NodeStorageAdapter;
141
+ /** Signaling/hub WebSocket URL */
142
+ signalingUrl: string;
143
+ /** Additional signaling/hub WebSocket URLs for multi-hub fan-out */
144
+ signalingUrls?: string[];
145
+ /** Max Y.Docs in memory (default: 50) */
146
+ poolSize?: number;
147
+ /** TTL for tracked Nodes in ms (default: 7 days) */
148
+ trackTTL?: number;
149
+ /** Author DID for awareness */
150
+ authorDID?: string;
151
+ /** Signing key for signed Yjs replication */
152
+ signingKey?: Uint8Array;
153
+ /** Replication compatibility policy */
154
+ replication?: SyncReplicationConfig;
155
+ /** Blob store for P2P blob sync (optional — if omitted, blob sync is disabled) */
156
+ blobStore?: BlobStoreForSync;
157
+ /** Optional UCAN token for hub auth */
158
+ ucanToken?: string;
159
+ /** Optional UCAN token provider for hub auth */
160
+ getUCANToken?: () => Promise<string>;
161
+ /** Optional pool update callback */
162
+ onDocUpdate?: (nodeId: string, doc: Y.Doc) => void;
163
+ /** Optional pool eviction callback */
164
+ onDocEvict?: (nodeId: string, doc: Y.Doc) => void;
165
+ /** Optional room for node-change relay (enables NodeStore sync via hub) */
166
+ nodeSyncRoom?: string;
167
+ }
168
+ type SyncReconciliationOptions = {
169
+ /** Optional node subset. Defaults to all joined rooms. */
170
+ nodeIds?: string[];
171
+ /** Diagnostic reason included in reports and debug traces. */
172
+ reason?: 'manual' | 'reconnect' | 'partition-repair';
173
+ };
174
+ type SyncReconciliationReport = {
175
+ reason: NonNullable<SyncReconciliationOptions['reason']>;
176
+ replayedOfflineChanges: number;
177
+ repairedNodeIds: string[];
178
+ skippedNodeIds: string[];
179
+ at: number;
180
+ };
181
+ interface SyncManager {
182
+ /** Start the sync manager (connect, load registry, sync tracked Nodes) */
183
+ start(): Promise<void>;
184
+ /** Stop (disconnect, flush, save registry) */
185
+ stop(): Promise<void>;
186
+ /** Track a Node for background sync */
187
+ track(nodeId: string, schemaId: string): void;
188
+ /** Stop tracking a Node */
189
+ untrack(nodeId: string): void;
190
+ /** Acquire a Y.Doc (used by useNode) */
191
+ acquire(nodeId: string): Promise<Y.Doc>;
192
+ /** Release a Y.Doc (component unmounted) */
193
+ release(nodeId: string): void;
194
+ /** Get awareness for a Node (for cursor presence) */
195
+ getAwareness(nodeId: string): Awareness | null;
196
+ /** Listen for awareness snapshots from the hub */
197
+ onAwarenessSnapshot(nodeId: string, handler: (users: AwarenessSnapshotUser[]) => void): () => void;
198
+ /** Request blobs from peers by CID (no-op if blob sync is disabled) */
199
+ requestBlobs(cids: string[]): Promise<void>;
200
+ /** Announce blob CIDs to peers (no-op if blob sync is disabled) */
201
+ announceBlobs(cids: string[]): void;
202
+ /** Explicitly drain queued updates and re-request state for tracked rooms. */
203
+ reconcile(options?: SyncReconciliationOptions): Promise<SyncReconciliationReport>;
204
+ /**
205
+ * Wipe this client's node-change data on the hub ("reset my data") and reset
206
+ * the local sync cursor. Returns how many changes the hub removed (0 if there
207
+ * is no node-sync room or we're offline). Pair with a local wipe + reload.
208
+ */
209
+ clearHubData(): Promise<number>;
210
+ /** Connection status */
211
+ readonly status: SyncStatus;
212
+ /** Canonical background-sync lifecycle */
213
+ readonly lifecycle: SyncLifecycleState;
214
+ /** Pool stats */
215
+ readonly poolSize: number;
216
+ /** Tracked count */
217
+ readonly trackedCount: number;
218
+ /** Offline queue size */
219
+ readonly queueSize: number;
220
+ /** Pending blob requests */
221
+ readonly pendingBlobCount: number;
222
+ /** Last rejected replication payload, if any */
223
+ readonly lastVerificationFailure: {
224
+ nodeId: string;
225
+ sender: string | null;
226
+ reason: string;
227
+ at: number;
228
+ } | null;
229
+ /** Last reconciliation/repair report, if any. */
230
+ readonly lastReconciliationReport: SyncReconciliationReport | null;
231
+ /** Listen for events */
232
+ on(event: 'status', handler: (status: SyncStatus) => void): () => void;
233
+ on(event: 'lifecycle', handler: (state: SyncLifecycleState) => void): () => void;
234
+ on(event: 'verification-failure', handler: (failure: {
235
+ nodeId: string;
236
+ sender: string | null;
237
+ reason: string;
238
+ at: number;
239
+ }) => void): () => void;
240
+ on(event: 'reconciliation', handler: (report: SyncReconciliationReport) => void): () => void;
241
+ /** Underlying ConnectionManager (if available) */
242
+ readonly connection?: ConnectionManager;
243
+ }
244
+ type AwarenessSnapshotUser = {
245
+ did: string;
246
+ state: {
247
+ user?: {
248
+ name?: string;
249
+ color?: string;
250
+ avatar?: string;
251
+ did?: string;
252
+ };
253
+ cursor?: {
254
+ anchor: number;
255
+ head: number;
256
+ };
257
+ selection?: unknown;
258
+ online?: boolean;
259
+ [key: string]: unknown;
260
+ };
261
+ lastSeen: number;
262
+ isStale: boolean;
263
+ };
264
+ declare function createSyncManager(config: SyncManagerConfig): SyncManager;
265
+
266
+ /** Telemetry reporter accepted by the runtime (duck-typed, no hard dep). */
267
+ interface XNetClientTelemetry {
268
+ reportPerformance(metricName: string, durationMs: number, codeNamespace?: string): void;
269
+ reportUsage(metricName: string, value: number): void;
270
+ reportCrash(error: Error, context?: {
271
+ codeNamespace?: string;
272
+ }): void;
273
+ reportSecurityEvent(eventName: string, severity: 'low' | 'medium' | 'high' | 'critical'): void;
274
+ }
275
+ /** Background-sync configuration. Omit (or pass `false`) for a local-only client. */
276
+ interface XNetClientSyncOptions {
277
+ /** Primary signaling/hub WebSocket URL (default: ws://localhost:4444). */
278
+ signalingUrl?: string;
279
+ /** Additional signaling/hub URLs for multi-hub fan-out. */
280
+ signalingUrls?: string[];
281
+ /** Replication compatibility policy. */
282
+ replication?: SyncReplicationConfig;
283
+ /** Blob store for P2P blob sync (images, files). */
284
+ blobStore?: BlobStoreForSync;
285
+ /** Room for node-change relay (enables NodeStore sync via hub). */
286
+ nodeSyncRoom?: string;
287
+ /** Static UCAN token for hub auth. */
288
+ ucanToken?: string;
289
+ /** UCAN token provider for hub auth. */
290
+ getUCANToken?: () => Promise<string>;
291
+ /** Max Y.Docs held in memory (default: 50). */
292
+ poolSize?: number;
293
+ /** TTL for tracked Nodes in ms (default: 7 days). */
294
+ trackTTL?: number;
295
+ /** Pool update callback (e.g. for auto-backup). */
296
+ onDocUpdate?: (nodeId: string, doc: Y.Doc) => void;
297
+ /** Pool eviction callback. */
298
+ onDocEvict?: (nodeId: string, doc: Y.Doc) => void;
299
+ /** Start the sync manager immediately (default: true). */
300
+ autoStart?: boolean;
301
+ }
302
+ /** Plugin-system configuration. Omit for no plugin registry (lean default). */
303
+ interface XNetClientPluginOptions {
304
+ /** Plugin compatibility platform (default: 'web'). */
305
+ platform?: Platform;
306
+ /** Load previously installed plugins from the store on init (default: true). */
307
+ autoLoad?: boolean;
308
+ }
309
+ /** App-wide undo configuration. Omit for no undo manager (lean default). */
310
+ interface XNetClientUndoOptions {
311
+ /** Only undo the local author's own changes (default: true). */
312
+ localOnly?: boolean;
313
+ /** Max undo stack size (default: 200). */
314
+ maxStackSize?: number;
315
+ }
316
+ /** Options for {@link createXNetClient}. */
317
+ interface CreateXNetClientOptions {
318
+ /** Node storage adapter (default: in-memory). */
319
+ nodeStorage?: NodeStorageAdapter;
320
+ /** Author's DID for signing changes. */
321
+ authorDID: DID;
322
+ /** Ed25519 signing key. */
323
+ signingKey: Uint8Array;
324
+ /** Optional full identity (exposed as `client.identity`). */
325
+ identity?: Identity;
326
+ /** Optional async change signer (e.g. WebCrypto/worker-backed). */
327
+ changeSigner?: ChangeSigner;
328
+ /** Authorization evaluator for read/write gating. */
329
+ authEvaluator?: PolicyEvaluator;
330
+ /** Transparent node-content cipher (encrypt/decrypt snapshots). */
331
+ nodeContentCipher?: NodeContentCipher;
332
+ /** High-level authorization API attached as `store.auth`. */
333
+ auth?: StoreAuthAPI;
334
+ /** Schema lookup for temp-id resolution in relation properties. */
335
+ schemaLookup?: SchemaLookup;
336
+ /** Property lookup for unknown-property preservation. */
337
+ propertyLookup?: (schemaId: string) => Set<string> | undefined;
338
+ /** Lens registry for automatic schema migrations on read. */
339
+ lensRegistry?: LensRegistry;
340
+ /** Telemetry reporter. */
341
+ telemetry?: XNetClientTelemetry;
342
+ /**
343
+ * Custom DataBridge (e.g. a WorkerBridge or IPC bridge), already
344
+ * initialized. When omitted, a main-thread bridge is created internally.
345
+ */
346
+ dataBridge?: DataBridge;
347
+ /** Options for the internally created main-thread bridge. */
348
+ bridgeOptions?: MainThreadBridgeOptions;
349
+ /** Background sync — omit or pass `false` for a local-only client. */
350
+ sync?: XNetClientSyncOptions | false;
351
+ /** Plugin system — omit or pass `false` to disable. */
352
+ plugins?: XNetClientPluginOptions | false;
353
+ /** App-wide undo — omit or pass `false` to disable. */
354
+ undo?: XNetClientUndoOptions | false;
355
+ }
356
+ type XNetClientRuntimePhase = 'ready' | 'destroyed';
357
+ type XNetClientBridgeMode = 'main-thread' | 'custom';
358
+ interface XNetClientRuntimeStatus {
359
+ phase: XNetClientRuntimePhase;
360
+ /** Whether the bridge was created internally ('main-thread') or supplied ('custom'). */
361
+ bridgeMode: XNetClientBridgeMode;
362
+ /** Whether background sync is active. */
363
+ syncEnabled: boolean;
364
+ }
365
+ /**
366
+ * A fully constructed, framework-agnostic xNet client. Owns its store, bridge,
367
+ * and (optionally) sync/plugins/undo, and exposes the read/write/auth/doc
368
+ * surface the React hooks expose.
369
+ */
370
+ interface XNetClient {
371
+ readonly store: NodeStore;
372
+ readonly bridge: DataBridge;
373
+ readonly syncManager: SyncManager | null;
374
+ readonly plugins: PluginRegistry | null;
375
+ readonly undo: UndoManager | null;
376
+ readonly identity?: Identity;
377
+ readonly authorDID: DID;
378
+ /** Current bridge/sync connection status. */
379
+ readonly status: SyncStatus$1;
380
+ readonly runtimeStatus: XNetClientRuntimeStatus;
381
+ /** Live query — returns a `{ getSnapshot, subscribe }` subscription. */
382
+ query: DataBridge['query'];
383
+ /** One-shot read — resolves once with the first non-null snapshot. */
384
+ fetch<P extends Record<string, PropertyBuilder>>(schema: DefinedSchema<P>, options?: QueryOptions<P>): Promise<NodeState[]>;
385
+ /** Read a single node by id. */
386
+ get(nodeId: string): Promise<NodeState | null>;
387
+ mutate: {
388
+ create: DataBridge['create'];
389
+ update: DataBridge['update'];
390
+ delete: DataBridge['delete'];
391
+ restore: DataBridge['restore'];
392
+ bulkWrite: DataBridge['bulkWrite'];
393
+ transaction(operations: TransactionOperation[]): Promise<BridgeTransactionResult>;
394
+ };
395
+ /** High-level grant/role API (null when no `auth` was configured). */
396
+ auth: StoreAuthAPI | null;
397
+ /** Evaluate an authorization decision (permissive when no evaluator is set). */
398
+ can(input: AuthCheckInput): Promise<AuthDecision>;
399
+ node: {
400
+ acquire(nodeId: string): Promise<AcquiredDoc>;
401
+ release(nodeId: string): void;
402
+ };
403
+ /** Sign a message with the client's signing key. */
404
+ sign(message: Uint8Array): Uint8Array;
405
+ /** Verify a signature (defaults to the client's own public key). */
406
+ verify(message: Uint8Array, signature: Uint8Array, publicKey?: Uint8Array): boolean;
407
+ on(event: 'status', handler: (status: SyncStatus$1) => void): () => void;
408
+ /** Tear down sync, plugins, undo, the (internally created) bridge, and storage. Idempotent. */
409
+ destroy(): Promise<void>;
410
+ }
411
+ /**
412
+ * Construct an xNet runtime. Returns a ready-to-use {@link XNetClient}.
413
+ *
414
+ * @example
415
+ * const client = await createXNetClient({
416
+ * nodeStorage: new MemoryNodeStorageAdapter(),
417
+ * authorDID: identity.did,
418
+ * signingKey: privateKey
419
+ * })
420
+ * const tasks = await client.fetch(TaskSchema, { where: { status: 'todo' } })
421
+ * await client.mutate.create(TaskSchema, { title: 'Ship it' })
422
+ * await client.destroy()
423
+ */
424
+ declare function createXNetClient(options: CreateXNetClientOptions): Promise<XNetClient>;
425
+
426
+ /**
427
+ * liveQuery — a tiny framework-agnostic reactive wrapper over a client query.
428
+ *
429
+ * The whole point of the runtime is that `client.query(...)` returns a
430
+ * `{ getSnapshot, subscribe }` subscription — the universal external-store
431
+ * contract. `liveQuery` adapts that into the **Svelte store contract**
432
+ * (`subscribe(run) => unsubscribe`, where `run` is invoked immediately with the
433
+ * current value and again on every change), which is also trivially consumable
434
+ * from Vue (`shallowRef` + `watchSyncEffect`), Solid, Angular, or plain JS.
435
+ *
436
+ * It is intentionally dependency-free: there is no Svelte/Vue import here, so it
437
+ * works in any environment, yet a Svelte component can use it directly with
438
+ * `$liveQuery(...)` auto-subscription.
439
+ */
440
+
441
+ /** The current value of a live query: `null` while loading, then the rows. */
442
+ type LiveQueryValue = NodeState[] | null;
443
+ /** A reactive handle over a live query (Svelte-store compatible). */
444
+ interface LiveQuery {
445
+ /**
446
+ * Svelte store contract. `run` is called synchronously with the current value
447
+ * and again on every change. Returns an unsubscribe function.
448
+ */
449
+ subscribe(run: (value: LiveQueryValue) => void): () => void;
450
+ /** Read the current value synchronously. */
451
+ get(): LiveQueryValue;
452
+ /** Release the underlying query subscription and drop all subscribers. */
453
+ destroy(): void;
454
+ }
455
+ /**
456
+ * Create a reactive, Svelte-store-compatible live query.
457
+ *
458
+ * @example Svelte
459
+ * ```svelte
460
+ * <script>
461
+ * import { liveQuery } from '@xnetjs/runtime'
462
+ * const tasks = liveQuery(client, TaskSchema, { where: { status: 'todo' } })
463
+ * </script>
464
+ * {#each $tasks ?? [] as task}
465
+ * <li>{task.properties.title}</li>
466
+ * {/each}
467
+ * ```
468
+ *
469
+ * @example Vanilla
470
+ * ```ts
471
+ * const tasks = liveQuery(client, TaskSchema)
472
+ * const stop = tasks.subscribe((rows) => render(rows))
473
+ * // …later
474
+ * stop()
475
+ * ```
476
+ */
477
+ declare function liveQuery<P extends Record<string, PropertyBuilder>>(client: XNetClient, schema: DefinedSchema<P>, options?: QueryOptions<P>): LiveQuery;
478
+
479
+ /**
480
+ * WebSocketSyncProvider - Syncs Y.Doc via WebSocket relay
481
+ *
482
+ * Unlike y-webrtc (which uses WebRTC DataChannels for P2P), this provider
483
+ * relays all Yjs updates through the signaling server. This works in all
484
+ * environments (same-machine, different networks, behind NATs) at the cost
485
+ * of server-relayed traffic.
486
+ *
487
+ * Protocol (extends the y-webrtc signaling protocol):
488
+ * - Uses the same subscribe/publish mechanism as y-webrtc signaling
489
+ * - Sync messages are published as JSON with base64-encoded binary data
490
+ * - Message types: 'sync-step1', 'sync-step2', 'sync-update', 'awareness'
491
+ *
492
+ * Sync flow:
493
+ * 1. On connect: subscribe to room, broadcast sync-step1 (state vector)
494
+ * 2. On receiving sync-step1: respond with sync-step2 (diff for their vector)
495
+ * 3. On local update: broadcast sync-update to room
496
+ * 4. On receiving sync-update: apply to local doc
497
+ *
498
+ * Awareness flow:
499
+ * 1. On connect: broadcast local awareness state
500
+ * 2. On awareness change: broadcast updated state
501
+ * 3. On receiving awareness: apply to local awareness instance
502
+ * 4. On disconnect: peers remove the disconnected client's state
503
+ */
504
+
505
+ interface WebSocketSyncProviderOptions {
506
+ /** WebSocket URL of the signaling/relay server */
507
+ url: string;
508
+ /** Room name (peers in the same room sync together) */
509
+ room: string;
510
+ /** Author DID for signed replication */
511
+ authorDID?: string;
512
+ /** Signing key for signed replication */
513
+ signingKey?: Uint8Array;
514
+ /** Replication compatibility policy */
515
+ replication?: SyncReplicationConfig;
516
+ /** Reconnect delay in ms (default: 2000) */
517
+ reconnectDelay?: number;
518
+ /** Maximum reconnect attempts (default: Infinity) */
519
+ maxReconnectAttempts?: number;
520
+ }
521
+ type SyncEventType = 'status' | 'synced' | 'peers' | 'awareness-snapshot';
522
+ type SyncEventHandler = (event: unknown) => void;
523
+ declare class WebSocketSyncProvider {
524
+ readonly doc: Y.Doc;
525
+ readonly room: string;
526
+ readonly url: string;
527
+ readonly awareness: Awareness;
528
+ private ws;
529
+ private reconnectDelay;
530
+ private maxReconnectAttempts;
531
+ private reconnectAttempts;
532
+ private reconnectTimer;
533
+ private destroyed;
534
+ private connected;
535
+ private synced;
536
+ private peerId;
537
+ private remotePeerIds;
538
+ private eventHandlers;
539
+ private readonly options;
540
+ private readonly replicationPolicy;
541
+ constructor(doc: Y.Doc, options: WebSocketSyncProviderOptions);
542
+ get isConnected(): boolean;
543
+ get isSynced(): boolean;
544
+ /** Helper to get XML fragment length for debug logging */
545
+ private _getFragmentLength;
546
+ on(event: SyncEventType, handler: SyncEventHandler): void;
547
+ off(event: SyncEventType, handler: SyncEventHandler): void;
548
+ private emit;
549
+ destroy(): void;
550
+ private _connect;
551
+ private _scheduleReconnect;
552
+ private _send;
553
+ private _publish;
554
+ private _createOutgoingPayload;
555
+ private _extractIncomingUpdate;
556
+ /** Handle incoming sync messages from other peers */
557
+ private _handleSyncMessage;
558
+ /** Broadcast local doc updates to peers */
559
+ private _onDocUpdate;
560
+ /** Broadcast local awareness changes to peers */
561
+ private _onAwarenessUpdate;
562
+ }
563
+
564
+ /**
565
+ * Meta Bridge - Unidirectional sync from NodeStore → Y.Doc meta map
566
+ *
567
+ * SECURITY: This bridge is intentionally ONE-WAY. The Y.Doc meta map is a
568
+ * read-only cache for the editor UI. Property changes MUST go through the
569
+ * signed NodeChange pipeline (via mutate()), never through Yjs.
570
+ *
571
+ * Before (VULNERABLE):
572
+ * NodeStore ↔ MetaBridge ↔ Y.Doc meta
573
+ * (Malicious Yjs updates could poison NodeStore!)
574
+ *
575
+ * After (SECURE):
576
+ * NodeStore → MetaBridge → Y.Doc meta (write)
577
+ * Y.Doc meta → Editor UI (read-only display)
578
+ * Editor UI → mutate() → NodeStore (signed writes)
579
+ *
580
+ * See: docs/plans/plan03_4_1YjsSecurity/04-metabridge-isolation.md
581
+ */
582
+
583
+ /** Transaction origin for MetaBridge writes (for debugging/monitoring) */
584
+ declare const METABRIDGE_ORIGIN = "metabridge";
585
+ declare const METABRIDGE_SEED_ORIGIN = "metabridge-seed";
586
+ interface MetaBridge {
587
+ /**
588
+ * Start observing NodeStore changes for a Node and syncing to Y.Doc meta.
589
+ * Direction: NodeStore → Y.Doc meta map (ONE-WAY)
590
+ *
591
+ * @returns Unsubscribe function
592
+ */
593
+ observe(nodeId: string, doc: Y.Doc): () => void;
594
+ /**
595
+ * Seed the Y.Doc meta map with current NodeStore state.
596
+ * Called on document open to populate editor UI.
597
+ */
598
+ seed(nodeId: string, doc: Y.Doc): Promise<void>;
599
+ /**
600
+ * @deprecated Use seed() instead. applyNow() was the bidirectional API.
601
+ * This is kept for backward compatibility but now just calls seed().
602
+ */
603
+ applyNow(nodeId: string, doc: Y.Doc): Promise<void>;
604
+ }
605
+ /**
606
+ * Create a unidirectional MetaBridge.
607
+ *
608
+ * @param store - NodeStore to observe
609
+ * @param options - Configuration options
610
+ */
611
+ declare function createMetaBridge(store: NodeStore, options?: {
612
+ /** Log warnings for non-MetaBridge meta map changes (default: true) */
613
+ warnOnExternalMetaChanges?: boolean;
614
+ }): MetaBridge;
615
+
616
+ /**
617
+ * Node Pool - LRU cache of Y.Doc instances with acquire/release semantics
618
+ *
619
+ * Components acquire a Y.Doc when they need it and release it when they unmount.
620
+ * Released Y.Docs stay in the pool (warm state) and continue receiving sync
621
+ * updates via the Connection Manager.
622
+ *
623
+ * States:
624
+ * - Active: refCount > 0, never evicted
625
+ * - Warm: refCount = 0, evictable (LRU)
626
+ * - Cold: evicted, serialized to storage (load on demand)
627
+ */
628
+
629
+ type PoolEntryState = 'active' | 'warm' | 'cold';
630
+ interface NodePoolConfig {
631
+ /** Storage adapter for persisting Y.Doc state */
632
+ storage: NodeStorageAdapter;
633
+ /** Meta bridge for syncing properties to NodeStore */
634
+ metaBridge: MetaBridge;
635
+ /** Max warm entries before LRU eviction (default: 50) */
636
+ maxWarm?: number;
637
+ /** Debounce delay for persisting dirty docs (default: 2000ms) */
638
+ persistDelay?: number;
639
+ /** Optional callback when a doc is updated */
640
+ onDocUpdate?: (nodeId: string, doc: Y.Doc) => void;
641
+ /** Optional callback before a doc is evicted */
642
+ onDocEvict?: (nodeId: string, doc: Y.Doc) => void;
643
+ }
644
+ interface NodePool {
645
+ /** Acquire a Y.Doc for a Node (load from storage or create new) */
646
+ acquire(nodeId: string): Promise<Y.Doc>;
647
+ /** Release a Y.Doc (component unmounted, doc stays warm) */
648
+ release(nodeId: string): void;
649
+ /** Check if a Node is in the pool */
650
+ has(nodeId: string): boolean;
651
+ /** Get pool entry state */
652
+ getState(nodeId: string): PoolEntryState | null;
653
+ /** Number of entries currently in memory */
654
+ readonly size: number;
655
+ /** Force-persist all dirty docs */
656
+ flushAll(): Promise<void>;
657
+ /** Destroy pool, persist all docs, cleanup */
658
+ destroy(): Promise<void>;
659
+ }
660
+ declare function createNodePool(config: NodePoolConfig): NodePool;
661
+
662
+ /**
663
+ * NodeStoreSyncProvider - Sync NodeChange events via hub ConnectionManager.
664
+ *
665
+ * Anti-flood design (exploration 0206):
666
+ * - A persisted per-room cursor (lastSyncedLamport) is loaded on connect, so
667
+ * a reload no longer replays the entire change log (getChangesSince(0)).
668
+ * - Request-sync-first: on connect we ask the hub for its high-water mark
669
+ * before pushing, so when the hub is already ahead we push nothing.
670
+ * - A throttled send queue caps outbound node-change messages per second so a
671
+ * genuine backlog can't trip the hub's per-connection rate limiter (1008).
672
+ */
673
+
674
+ type SerializedNodeChange = {
675
+ id: string;
676
+ type: string;
677
+ hash: string;
678
+ room: string;
679
+ nodeId: string;
680
+ schemaId?: string;
681
+ lamportTime: number;
682
+ lamportAuthor: string;
683
+ authorDid: string;
684
+ wallTime: number;
685
+ parentHash: string | null;
686
+ payload: NodePayload;
687
+ signatureB64: string;
688
+ protocolVersion?: number;
689
+ batchId?: string;
690
+ batchIndex?: number;
691
+ batchSize?: number;
692
+ };
693
+ type NodeSyncResponse = {
694
+ type: 'node-sync-response';
695
+ room: string;
696
+ changes: SerializedNodeChange[];
697
+ highWaterMark: number;
698
+ };
699
+ /**
700
+ * Listener for unknown change type events.
701
+ */
702
+ type UnknownChangeTypeListener = (change: NodeChange, peerId: string) => void;
703
+ declare class NodeStoreSyncProvider {
704
+ private store;
705
+ private room;
706
+ /** Confirmed, persisted high-water mark (advanced from the hub's response). */
707
+ private lastSyncedLamport;
708
+ /** Optimistic in-memory cursor: lamport of the last change actually sent. */
709
+ private pushedThrough;
710
+ private cursorLoaded;
711
+ private connection;
712
+ private roomCleanup;
713
+ private statusCleanup;
714
+ private messageCleanup;
715
+ private storeCleanup;
716
+ private unknownChangeTypeListeners;
717
+ private sendQueue;
718
+ private queuedHashes;
719
+ private sendTimer;
720
+ private sentInWindow;
721
+ private syncResponseResolver;
722
+ private structuralRejections;
723
+ private outboundHalted;
724
+ private clearResolver;
725
+ private firstRemoteApplyMarked;
726
+ constructor(store: NodeStore, room: string);
727
+ /**
728
+ * Mark the first remote apply once. Platform-agnostic and defensive: a
729
+ * missing `performance` global, or a throw, is a no-op — instrumentation
730
+ * must never break sync.
731
+ */
732
+ private markFirstRemoteApply;
733
+ /**
734
+ * Subscribe to unknown change type events.
735
+ * These are changes received from peers with types this version doesn't know how to process.
736
+ * The changes are still stored in the change log for forward compatibility.
737
+ *
738
+ * @param listener - Callback invoked when an unknown change type is received
739
+ * @returns Unsubscribe function
740
+ */
741
+ onUnknownChangeType(listener: UnknownChangeTypeListener): () => void;
742
+ private emitUnknownChangeType;
743
+ attach(connection: ConnectionManager): void;
744
+ detach(): void;
745
+ /**
746
+ * On connect: load the persisted cursor, ask the hub for its high-water mark
747
+ * FIRST, then push only the changes the hub is actually missing. Combined
748
+ * with the persisted cursor, a normal reload of an in-sync workspace pushes
749
+ * nothing (exploration 0206).
750
+ */
751
+ private onConnected;
752
+ private onDisconnected;
753
+ private ensureCursorLoaded;
754
+ private waitForSyncResponse;
755
+ private resolveSyncResponse;
756
+ /**
757
+ * Ask the hub to wipe every stored change for this room ("reset my data"),
758
+ * then reset the local sync cursor so a later sync re-pulls from scratch.
759
+ * Resolves with the number of changes the hub removed (0 on timeout or when
760
+ * offline). Pairs with a local wipe + reload for a full reset.
761
+ */
762
+ clearRoom(): Promise<number>;
763
+ private resolveClear;
764
+ private handleRoomMessage;
765
+ private handleDirectMessage;
766
+ /**
767
+ * Trip the circuit breaker after enough consecutive structural rejections.
768
+ *
769
+ * Structural rejections (bad hash/signature/shape) are not transient: the
770
+ * same change re-sent is rejected again, and — when it's a protocol/build
771
+ * skew — so is every other local change. Rather than re-flood the hub forever
772
+ * (the symptom that motivated this), we stop pushing, drop the queue, and log
773
+ * ONE actionable error. A reconnect clears the breaker (the hub may have been
774
+ * upgraded) via {@link onConnected}, and any forward progress resets the
775
+ * counter so sparse one-off rejections never accumulate to a false trip.
776
+ */
777
+ private recordStructuralRejection;
778
+ private requestSync;
779
+ private handleRemoteChange;
780
+ private handleSyncResponse;
781
+ /** Enqueue every local change since the last confirmed send, then drain. */
782
+ private syncLocalChanges;
783
+ /** Queue a change for throttled broadcast (deduped by hash). */
784
+ private enqueueChange;
785
+ private scheduleDrain;
786
+ private drain;
787
+ private publishChange;
788
+ private clearSendQueue;
789
+ private serializeChange;
790
+ private deserializeChange;
791
+ }
792
+
793
+ /**
794
+ * Registry - Persistent tracked-Node set that survives app restarts
795
+ *
796
+ * The Registry maintains the set of Nodes the BSM should keep synced.
797
+ * Nodes are added automatically when opened and expire after a configurable TTL.
798
+ * Pinned Nodes never expire.
799
+ */
800
+ interface TrackedNode {
801
+ nodeId: string;
802
+ schemaId: string;
803
+ /** When the user last opened this Node */
804
+ lastOpened: number;
805
+ /** When sync last completed for this Node */
806
+ lastSynced: number;
807
+ /** Whether explicitly pinned (never expires) */
808
+ pinned: boolean;
809
+ }
810
+ interface RegistryStorage {
811
+ get(key: string): Promise<TrackedNode[] | null>;
812
+ set(key: string, entries: TrackedNode[]): Promise<void>;
813
+ }
814
+ interface RegistryConfig {
815
+ /** Storage adapter for persistence */
816
+ storage: RegistryStorage;
817
+ /** TTL for tracked Nodes in ms (default: 7 days) */
818
+ trackTTL?: number;
819
+ /** Storage key for the tracked set */
820
+ storageKey?: string;
821
+ }
822
+ interface Registry {
823
+ /** Add a Node to the tracked set */
824
+ track(nodeId: string, schemaId: string): void;
825
+ /** Remove a Node from the tracked set */
826
+ untrack(nodeId: string): void;
827
+ /** Pin a Node (never expires) */
828
+ pin(nodeId: string): void;
829
+ /** Unpin a Node (subject to TTL expiry) */
830
+ unpin(nodeId: string): void;
831
+ /** Mark a Node as recently opened (refreshes TTL) */
832
+ touch(nodeId: string): void;
833
+ /** Mark a Node as synced */
834
+ markSynced(nodeId: string): void;
835
+ /** Get all tracked Nodes (excluding expired) */
836
+ getTracked(): TrackedNode[];
837
+ /** Check if a Node is tracked */
838
+ isTracked(nodeId: string): boolean;
839
+ /** Load from storage */
840
+ load(): Promise<void>;
841
+ /** Persist to storage */
842
+ save(): Promise<void>;
843
+ /** Remove expired entries */
844
+ prune(): number;
845
+ }
846
+ declare function createRegistry(config: RegistryConfig): Registry;
847
+
848
+ /**
849
+ * Offline Queue - Persistent queue for Y.Doc updates made while disconnected
850
+ *
851
+ * When the network is unavailable, local Y.Doc updates are queued in persistent
852
+ * storage. On reconnect, the queue is drained (replayed in order). This ensures
853
+ * no local changes are lost, even across app restarts.
854
+ */
855
+
856
+ interface QueueEntry {
857
+ /** Node ID this update belongs to */
858
+ nodeId: string;
859
+ /** Serialized Y.Doc update (base64 encoded) */
860
+ update: string;
861
+ /** Original Yjs client ID when the update was queued */
862
+ clientId?: number;
863
+ /** Timestamp when queued */
864
+ queuedAt: number;
865
+ }
866
+ interface OfflineQueueConfig {
867
+ /** Storage adapter for persistence */
868
+ storage: NodeStorageAdapter;
869
+ /** Storage key for the queue (default: '_xnet_offline_queue') */
870
+ storageKey?: string;
871
+ /** Max queue size before dropping oldest entries (default: 1000) */
872
+ maxSize?: number;
873
+ }
874
+ interface OfflineQueue {
875
+ /** Enqueue an update for later broadcast */
876
+ enqueue(nodeId: string, update: Uint8Array, clientId?: number): Promise<void>;
877
+ /** Drain the queue, calling handler for each entry. Returns count drained. */
878
+ drain(handler: (entry: QueueEntry) => Promise<void>): Promise<number>;
879
+ /** Number of entries in the queue */
880
+ readonly size: number;
881
+ /** Load queue from storage */
882
+ load(): Promise<void>;
883
+ /** Persist queue to storage */
884
+ save(): Promise<void>;
885
+ /** Clear all entries */
886
+ clear(): Promise<void>;
887
+ }
888
+ declare function createOfflineQueue(config: OfflineQueueConfig): OfflineQueue;
889
+
890
+ /**
891
+ * @xnetjs/react/sync - Initial sync manager for new device onboarding
892
+ *
893
+ * Orchestrates the full state sync when a new device connects to the hub
894
+ * with an existing identity. Tracks progress and provides callbacks.
895
+ */
896
+ type SyncPhase = 'connecting' | 'syncing' | 'complete' | 'error';
897
+ type SyncProgress = {
898
+ /** Current phase of the sync process */
899
+ phase: SyncPhase;
900
+ /** Total rooms discovered on the hub */
901
+ roomsTotal: number;
902
+ /** Rooms that have been fully synced */
903
+ roomsSynced: number;
904
+ /** Total bytes received from hub */
905
+ bytesReceived: number;
906
+ /** Error if phase === 'error' */
907
+ error?: Error;
908
+ };
909
+ type InitialSyncMessage = {
910
+ type: 'initial-sync' | 'node-changes' | 'initial-sync-complete';
911
+ room?: string;
912
+ update?: Uint8Array;
913
+ changes?: unknown[];
914
+ roomCount?: number;
915
+ };
916
+ type ProgressListener = (progress: SyncProgress) => void;
917
+ /**
918
+ * Manages the initial sync process for a new device.
919
+ *
920
+ * Usage:
921
+ * ```ts
922
+ * const manager = createInitialSyncManager()
923
+ * const unsub = manager.onProgress((p) => updateUI(p))
924
+ *
925
+ * // Feed messages from the hub WebSocket:
926
+ * manager.handleMessage({ type: 'initial-sync', room: 'r1', update: ... })
927
+ * manager.handleMessage({ type: 'initial-sync-complete', roomCount: 5 })
928
+ *
929
+ * unsub()
930
+ * ```
931
+ */
932
+ type InitialSyncManager = {
933
+ /** Subscribe to progress updates */
934
+ onProgress(listener: ProgressListener): () => void;
935
+ /** Handle an incoming sync message from the hub */
936
+ handleMessage(msg: InitialSyncMessage): void;
937
+ /** Get current progress snapshot */
938
+ getProgress(): SyncProgress;
939
+ /** Mark sync as started (connecting phase) */
940
+ start(): void;
941
+ /** Mark sync as errored */
942
+ setError(error: Error): void;
943
+ /** Reset to initial state */
944
+ reset(): void;
945
+ };
946
+ declare function createInitialSyncManager(): InitialSyncManager;
947
+
948
+ /**
949
+ * The umbrella XNet Protocol Version.
950
+ *
951
+ * Five subsystems version independently (the change record, the Yjs sync
952
+ * envelope, awareness, schemas, the crypto level). To avoid a five-way
953
+ * negotiation, peers advertise a single named bundle — exactly as Matrix
954
+ * bundles breaking changes into "room versions". A breaking change to any
955
+ * normative layer mints a new umbrella version; the handshake advertises a
956
+ * set, so old and new peers can coexist.
957
+ *
958
+ * This is the machine-readable counterpart of the normative specification in
959
+ * `docs/specs/protocol/`. See `00-overview.md` §5.
960
+ */
961
+ /** Schema profile version (xnet://authority/Name@version default). */
962
+ declare const XNET_SCHEMA_VERSION = "1.0.0";
963
+ /** Signed Yjs envelope wire version (SignedYjsEnvelopeV2). */
964
+ declare const XNET_SYNC_ENVELOPE_VERSION = 2;
965
+ /** y-protocols awareness profile version. */
966
+ declare const XNET_AWARENESS_VERSION = 1;
967
+ /** The L1 data-model version (Node shape + canonicalization contract). */
968
+ declare const XNET_DATA_MODEL_VERSION = 1;
969
+ /** UCAN capability-token profile pinned by this umbrella version. */
970
+ declare const XNET_UCAN_PROFILE = "1.0";
971
+ /**
972
+ * The per-subsystem versions bundled by one umbrella version. Implementations
973
+ * negotiate the umbrella `id`; this record documents what it expands to.
974
+ */
975
+ interface XNetProtocolBundle {
976
+ /** Umbrella identifier advertised in the handshake, e.g. `"xnet/1.0"`. */
977
+ id: string;
978
+ /** L1 data-model version. */
979
+ dataModel: number;
980
+ /** Change-record protocol version (`CURRENT_PROTOCOL_VERSION`). */
981
+ change: number;
982
+ /** Signed Yjs envelope wire version. */
983
+ syncEnvelope: number;
984
+ /** Awareness profile version. */
985
+ awareness: number;
986
+ /** Default schema profile version. */
987
+ schema: string;
988
+ /**
989
+ * Default crypto signature level: 0 = Ed25519/X25519, 1 = hybrid (+ML-DSA),
990
+ * 2 = post-quantum. Level 0 is the only one required for baseline conformance.
991
+ */
992
+ cryptoLevel: number;
993
+ /** UCAN capability-token profile. */
994
+ ucan: string;
995
+ }
996
+ /**
997
+ * The current umbrella version this implementation speaks. The canonical
998
+ * version token is {@link XNET_PROTOCOL_VERSION.id} (`"xnet/1.0"`).
999
+ */
1000
+ declare const XNET_PROTOCOL_VERSION: XNetProtocolBundle;
1001
+ /**
1002
+ * Every umbrella version this implementation can interoperate with, newest
1003
+ * first. Older entries are added here (not removed) as the protocol evolves,
1004
+ * so the handshake can advertise the full supported set.
1005
+ */
1006
+ declare const XNET_SUPPORTED_PROTOCOL_VERSIONS: readonly string[];
1007
+ /**
1008
+ * Two peers are compatible when their advertised umbrella-version sets
1009
+ * intersect. Returns the newest shared version id, or `null` when there is no
1010
+ * overlap (the caller should then refuse with a typed version-mismatch rather
1011
+ * than partially syncing — see `docs/specs/protocol/03-replication.md` §7).
1012
+ */
1013
+ declare function negotiateProtocolVersion(ours: readonly string[], theirs: readonly string[]): string | null;
1014
+ /**
1015
+ * Convenience: whether this implementation can interoperate with a peer that
1016
+ * advertises `theirs`.
1017
+ */
1018
+ declare function isProtocolCompatible(theirs: readonly string[]): boolean;
1019
+
1020
+ export { type BlobStoreForSync, type ConnectionManager, type ConnectionManagerConfig, type ConnectionStatus, type CreateXNetClientOptions, type InitialSyncManager, type InitialSyncMessage, type LiveQuery, type LiveQueryValue, METABRIDGE_ORIGIN, METABRIDGE_SEED_ORIGIN, type MetaBridge, type MultiHubConnectionManagerConfig, type NodePool, type NodePoolConfig, NodeStoreSyncProvider, type NodeSyncResponse, type OfflineQueue, type OfflineQueueConfig, type PoolEntryState, type ProgressListener, type QueueEntry, type Registry, type RegistryConfig, type RegistryStorage, type SerializedNodeChange, type SyncManager, type SyncManagerConfig, type SyncPhase, type SyncProgress, type SyncReconciliationOptions, type SyncReconciliationReport, type SyncStatus, type TrackedNode, WebSocketSyncProvider, type WebSocketSyncProviderOptions, XNET_AWARENESS_VERSION, XNET_DATA_MODEL_VERSION, XNET_PROTOCOL_VERSION, XNET_SCHEMA_VERSION, XNET_SUPPORTED_PROTOCOL_VERSIONS, XNET_SYNC_ENVELOPE_VERSION, XNET_UCAN_PROFILE, type XNetClient, type XNetClientBridgeMode, type XNetClientPluginOptions, type XNetClientRuntimePhase, type XNetClientRuntimeStatus, type XNetClientSyncOptions, type XNetClientTelemetry, type XNetClientUndoOptions, type XNetProtocolBundle, createConnectionManager, createInitialSyncManager, createMetaBridge, createMultiHubConnectionManager, createNodePool, createOfflineQueue, createRegistry, createSyncManager, createXNetClient, isProtocolCompatible, liveQuery, negotiateProtocolVersion };