@xnetjs/runtime 0.3.2 → 0.5.0

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/dist/index.d.ts CHANGED
@@ -164,6 +164,19 @@ interface SyncManagerConfig {
164
164
  onDocEvict?: (nodeId: string, doc: Y.Doc) => void;
165
165
  /** Optional room for node-change relay (enables NodeStore sync via hub) */
166
166
  nodeSyncRoom?: string;
167
+ /**
168
+ * Production Yjs history capture (exploration 0329): wired to
169
+ * `DocumentHistoryEngine` (duck-typed to avoid a hard coupling). Steady
170
+ * debounce persists go through the engine's own min-interval throttle
171
+ * (`captureSnapshot`); session boundaries (evict/flush/destroy) force a
172
+ * capture so a doc never leaves memory without a restorable snapshot.
173
+ */
174
+ documentHistory?: DocumentHistoryCapture;
175
+ }
176
+ /** Duck-typed slice of DocumentHistoryEngine used for capture wiring. */
177
+ interface DocumentHistoryCapture {
178
+ captureSnapshot(nodeId: string, doc: Y.Doc): Promise<unknown>;
179
+ forceCapture(nodeId: string, doc: Y.Doc): Promise<unknown>;
167
180
  }
168
181
  type SyncReconciliationOptions = {
169
182
  /** Optional node subset. Defaults to all joined rooms. */
@@ -716,6 +729,14 @@ interface NodePoolConfig {
716
729
  onDocUpdate?: (nodeId: string, doc: Y.Doc) => void;
717
730
  /** Optional callback before a doc is evicted */
718
731
  onDocEvict?: (nodeId: string, doc: Y.Doc) => void;
732
+ /**
733
+ * Fired after a doc's state was successfully persisted to `yjs_state`
734
+ * (exploration 0329: the production Yjs history capture hook). `context`
735
+ * distinguishes the steady-state debounce from session boundaries
736
+ * (evict/flush/destroy) so capture policies can throttle the former and
737
+ * force the latter. Must never throw — persist correctness wins.
738
+ */
739
+ onDocPersist?: (nodeId: string, doc: Y.Doc, context: 'debounce' | 'evict' | 'flush' | 'destroy') => void;
719
740
  /**
720
741
  * Predicate marking a node's Y.Doc as **ephemeral** — never persisted to
721
742
  * `yjs_state` and never cold-loaded from it. Workspace presence is the
@@ -997,6 +1018,13 @@ declare class NodeStoreSyncProvider {
997
1018
  private store;
998
1019
  private room;
999
1020
  private subscribeOnly;
1021
+ /**
1022
+ * Outbound exclusion (exploration 0329): return false to keep a change
1023
+ * out of this room entirely (live subscribe AND cursor backfill both
1024
+ * funnel through `enqueueChange`). Used to keep device-local draft
1025
+ * clones out of the personal node-sync room.
1026
+ */
1027
+ private shouldPublish?;
1000
1028
  /** Confirmed, persisted high-water mark (advanced from the hub's response). */
1001
1029
  private lastSyncedLamport;
1002
1030
  /** Optimistic in-memory cursor: lamport of the last change actually sent. */
@@ -1026,7 +1054,14 @@ declare class NodeStoreSyncProvider {
1026
1054
  * sync-response high-water mark, and author lamports across members are not
1027
1055
  * mutually ordered.
1028
1056
  */
1029
- constructor(store: NodeStore, room: string, subscribeOnly?: boolean);
1057
+ constructor(store: NodeStore, room: string, subscribeOnly?: boolean,
1058
+ /**
1059
+ * Outbound exclusion (exploration 0329): return false to keep a change
1060
+ * out of this room entirely (live subscribe AND cursor backfill both
1061
+ * funnel through `enqueueChange`). Used to keep device-local draft
1062
+ * clones out of the personal node-sync room.
1063
+ */
1064
+ shouldPublish?: ((change: NodeChange) => boolean) | undefined);
1030
1065
  /**
1031
1066
  * Mark the first remote apply once. Platform-agnostic and defensive: a
1032
1067
  * missing `performance` global, or a throw, is a no-op — instrumentation
package/dist/index.js CHANGED
@@ -2,7 +2,11 @@
2
2
  import { getSigningPublicKeyFromPrivate, sign, verify } from "@xnetjs/crypto";
3
3
  import { MemoryNodeStorageAdapter, NodeStore } from "@xnetjs/data";
4
4
  import { createMainThreadBridgeSync } from "@xnetjs/data-bridge";
5
- import { UndoManager } from "@xnetjs/history";
5
+ import {
6
+ DocumentHistoryEngine,
7
+ UndoManager,
8
+ rehydrateDraftPrivacy
9
+ } from "@xnetjs/history";
6
10
  import { PluginRegistry } from "@xnetjs/plugins";
7
11
 
8
12
  // src/sync/sync-manager.ts
@@ -750,6 +754,13 @@ function createNodePool(config) {
750
754
  const isEphemeral = config.isEphemeral ?? defaultIsEphemeral;
751
755
  const largeDocWarnBytes = config.largeDocWarnBytes ?? DEFAULT_LARGE_DOC_WARN_BYTES;
752
756
  let firstAcquireMarked = false;
757
+ function notifyPersist(nodeId, doc, context) {
758
+ try {
759
+ config.onDocPersist?.(nodeId, doc, context);
760
+ } catch (err) {
761
+ console.warn(`[NodePool] onDocPersist hook failed for ${nodeId}:`, err);
762
+ }
763
+ }
753
764
  function markFirstAcquire() {
754
765
  if (firstAcquireMarked) return;
755
766
  firstAcquireMarked = true;
@@ -795,6 +806,7 @@ function createNodePool(config) {
795
806
  const content = Y.encodeStateAsUpdate(entry.doc);
796
807
  await config.storage.setDocumentContent(nodeId, content);
797
808
  entry.dirty = false;
809
+ notifyPersist(nodeId, entry.doc, "debounce");
798
810
  }
799
811
  }, persistDelay)
800
812
  );
@@ -822,6 +834,7 @@ function createNodePool(config) {
822
834
  );
823
835
  }
824
836
  await config.storage.setDocumentContent(id, content);
837
+ notifyPersist(id, entry.doc, "evict");
825
838
  }
826
839
  } catch (err) {
827
840
  console.error(`[NodePool] Failed to persist document ${id} during eviction:`, err);
@@ -898,6 +911,7 @@ function createNodePool(config) {
898
911
  promises.push(
899
912
  config.storage.setDocumentContent(id, content).then(() => {
900
913
  entry.dirty = false;
914
+ notifyPersist(id, entry.doc, "flush");
901
915
  })
902
916
  );
903
917
  }
@@ -944,10 +958,11 @@ var NodeStoreSyncProvider = class {
944
958
  * sync-response high-water mark, and author lamports across members are not
945
959
  * mutually ordered.
946
960
  */
947
- constructor(store, room, subscribeOnly = false) {
961
+ constructor(store, room, subscribeOnly = false, shouldPublish) {
948
962
  this.store = store;
949
963
  this.room = room;
950
964
  this.subscribeOnly = subscribeOnly;
965
+ this.shouldPublish = shouldPublish;
951
966
  }
952
967
  /** Confirmed, persisted high-water mark (advanced from the hub's response). */
953
968
  lastSyncedLamport = 0;
@@ -1311,6 +1326,7 @@ var NodeStoreSyncProvider = class {
1311
1326
  /** Queue a change for throttled broadcast (deduped by hash). */
1312
1327
  enqueueChange(change) {
1313
1328
  if (this.outboundHalted) return;
1329
+ if (this.shouldPublish && !this.shouldPublish(change)) return;
1314
1330
  if (change.lamport <= this.pushedThrough) return;
1315
1331
  if (this.queuedHashes.has(change.hash)) return;
1316
1332
  this.queuedHashes.add(change.hash);
@@ -1701,7 +1717,7 @@ function hasEnvelope(data) {
1701
1717
  function extractBlobCids(doc) {
1702
1718
  const cids = [];
1703
1719
  const seen = /* @__PURE__ */ new Set();
1704
- for (const name of ["default", "prosemirror", ""]) {
1720
+ for (const name of ["content-v4", "content", "default", "prosemirror", ""]) {
1705
1721
  let fragment;
1706
1722
  try {
1707
1723
  fragment = doc.getXmlFragment(name);
@@ -1775,7 +1791,14 @@ function createSyncManager(config) {
1775
1791
  onDocEvict: (nodeId, doc) => {
1776
1792
  broadcastDocs.delete(nodeId);
1777
1793
  config.onDocEvict?.(nodeId, doc);
1778
- }
1794
+ },
1795
+ onDocPersist: config.documentHistory ? (nodeId, doc, context) => {
1796
+ const history = config.documentHistory;
1797
+ const capture = context === "debounce" ? history.captureSnapshot(nodeId, doc) : history.forceCapture(nodeId, doc);
1798
+ void capture.catch(
1799
+ (err) => console.warn(`[SyncManager] Yjs snapshot capture failed for ${nodeId}:`, err)
1800
+ );
1801
+ } : void 0
1779
1802
  });
1780
1803
  const registry = createRegistry({
1781
1804
  storage: createRegistryStorageAdapter(config.storage),
@@ -1841,7 +1864,14 @@ function createSyncManager(config) {
1841
1864
  storage: config.storage
1842
1865
  });
1843
1866
  let offlineQueueReady = Promise.resolve();
1844
- const nodeSyncProvider = config.nodeSyncRoom ? new NodeStoreSyncProvider(config.nodeStore, config.nodeSyncRoom) : null;
1867
+ const nodeSyncProvider = config.nodeSyncRoom ? new NodeStoreSyncProvider(
1868
+ config.nodeStore,
1869
+ config.nodeSyncRoom,
1870
+ false,
1871
+ // Draft privacy (0329): device-local draft nodes/clones never publish
1872
+ // to the personal node-sync room.
1873
+ (change) => !config.nodeStore.isDraftPrivate(change.payload.nodeId)
1874
+ ) : null;
1845
1875
  const shareRoomProviders = /* @__PURE__ */ new Map();
1846
1876
  let blobSync = null;
1847
1877
  if (config.blobStore) {
@@ -2530,6 +2560,15 @@ function buildNodeStore(options, storage) {
2530
2560
  telemetry: options.telemetry
2531
2561
  });
2532
2562
  }
2563
+ function buildDocumentHistory(storage) {
2564
+ const snapshotStorage = storage;
2565
+ if (typeof snapshotStorage.saveYjsSnapshot !== "function" || typeof snapshotStorage.getYjsSnapshots !== "function" || typeof snapshotStorage.deleteYjsSnapshots !== "function") {
2566
+ return void 0;
2567
+ }
2568
+ return new DocumentHistoryEngine(snapshotStorage, {
2569
+ pins: storage.pins
2570
+ });
2571
+ }
2533
2572
  function buildSyncManager(store, storage, options) {
2534
2573
  const sync = options.sync;
2535
2574
  if (!sync) return null;
@@ -2537,6 +2576,7 @@ function buildSyncManager(store, storage, options) {
2537
2576
  return createSyncManager({
2538
2577
  nodeStore: store,
2539
2578
  storage,
2579
+ documentHistory: buildDocumentHistory(storage),
2540
2580
  signalingUrl,
2541
2581
  authorDID: options.authorDID,
2542
2582
  signingKey: options.signingKey,
@@ -2558,6 +2598,14 @@ function startSyncManager(syncManager, bridge, sync, telemetry) {
2558
2598
  if (sync.autoStart === false) return;
2559
2599
  syncManager.start().catch((err) => reportCrash(telemetry, err, "runtime.syncManager.start"));
2560
2600
  }
2601
+ async function startSyncManagerWithDraftPrivacy(store, syncManager, bridge, sync, telemetry) {
2602
+ try {
2603
+ await rehydrateDraftPrivacy(store);
2604
+ } catch (err) {
2605
+ reportCrash(telemetry, err, "runtime.drafts.rehydratePrivacy");
2606
+ }
2607
+ startSyncManager(syncManager, bridge, sync, telemetry);
2608
+ }
2561
2609
  function buildPlugins(store, options) {
2562
2610
  const plugins = options.plugins;
2563
2611
  if (!plugins) return null;
@@ -2601,7 +2649,9 @@ async function createXNetClient(options) {
2601
2649
  const bridgeCreatedInternally = !dataBridge;
2602
2650
  const bridge = dataBridge ?? createMainThreadBridgeSync(store, bridgeOptions);
2603
2651
  const syncManager = buildSyncManager(store, storage, options);
2604
- if (syncManager && sync) startSyncManager(syncManager, bridge, sync, telemetry);
2652
+ if (syncManager && sync) {
2653
+ void startSyncManagerWithDraftPrivacy(store, syncManager, bridge, sync, telemetry);
2654
+ }
2605
2655
  const pluginRegistry = buildPlugins(store, options);
2606
2656
  const undoManager = buildUndo(store, options);
2607
2657
  const publicKey = getSigningPublicKeyFromPrivate(signingKey);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xnetjs/runtime",
3
- "version": "0.3.2",
3
+ "version": "0.5.0",
4
4
  "description": "Framework-agnostic xNet runtime: createXNetClient() and sync orchestration, usable from any framework, a CLI, a worker, or a Node service",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,15 +28,15 @@
28
28
  "dependencies": {
29
29
  "y-protocols": "^1.0.6",
30
30
  "yjs": "^13.6.24",
31
- "@xnetjs/core": "0.12.0",
32
- "@xnetjs/crypto": "0.12.0",
33
- "@xnetjs/data": "0.12.0",
34
- "@xnetjs/data-bridge": "0.12.0",
35
- "@xnetjs/history": "0.12.0",
36
- "@xnetjs/identity": "0.12.0",
37
- "@xnetjs/plugins": "0.12.0",
38
- "@xnetjs/storage": "0.12.0",
39
- "@xnetjs/sync": "0.12.0"
31
+ "@xnetjs/crypto": "2.0.0",
32
+ "@xnetjs/core": "2.0.0",
33
+ "@xnetjs/data": "2.0.0",
34
+ "@xnetjs/history": "2.0.0",
35
+ "@xnetjs/data-bridge": "2.0.0",
36
+ "@xnetjs/identity": "2.0.0",
37
+ "@xnetjs/plugins": "2.0.0",
38
+ "@xnetjs/storage": "2.0.0",
39
+ "@xnetjs/sync": "2.0.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "jsdom": "^26.0.0",