@xnetjs/data 1.0.0 → 2.0.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.
@@ -1,12 +1,12 @@
1
1
  import { AllowExpr, DenyExpr, AuthExpression, AndExpr, OrExpr, NotExpr, PublicExpr, AuthenticatedExpr, CreatorRoleResolver, PropertyRoleResolver, RelationRoleResolver, MembershipRoleResolver, AuthorizationDefinition, SerializedAuthorization, SerializedAuthExpression, DID } from '@xnetjs/core';
2
2
  export { AUTH_ACTIONS, AllowExpr, AndExpr, AuthAction, AuthCheckInput, AuthDecision, AuthDenyReason, AuthExpression, AuthTrace, AuthTraceStep, AuthenticatedExpr, AuthorizationDefinition, CreatorRoleResolver, DenyExpr, MembershipRoleResolver, NotExpr, OrExpr, PolicyEvaluator, PropertyRoleResolver, PublicExpr, RelationRoleResolver, RoleRefExpr, RoleResolver, SerializedAuthExpression, SerializedAuthorization, SerializedRoleResolver } from '@xnetjs/core';
3
- import { E as PropertyDefinition, a as Schema, h as NodeState, S as SchemaIRI } from '../types-B5rrBwRI.js';
4
- export { aX as DEFAULT_OFFLINE_POLICY, aV as GRANT_SCHEMA_IRI, b1 as Grant, w as GrantIndex, b6 as GrantIndexOptions, b5 as GrantIndexStore, b0 as GrantInput, b4 as GrantNode, aU as GrantRateLimiter, b9 as GrantRateLimiterOptions, O as OfflineAuthPolicy, b8 as RevocationConfig, b7 as RevocationConsistency, aT as StoreAuth, e as StoreAuthAPI, b2 as StoreAuthError, b3 as StoreAuthErrorCode, a$ as StoreAuthKeyManager, aZ as StoreAuthOptions, a_ as StoreAuthStore, aW as isGrantActive, aY as mergeOfflinePolicy } from '../types-B5rrBwRI.js';
5
- export { A as ActionPermission, m as AuthDecisionEvent, b as DecisionCache, g as DecisionCacheOptions, D as DefaultPolicyEvaluator, j as DefaultPolicyEvaluatorOptions, a as DefaultRoleResolver, i as DefaultRoleResolverOptions, k as DefinedSchemaLike, G as GrantExpirationCleaner, o as GrantExpirationCleanerOptions, n as GrantExpirationStore, H as HubPolicy, N as NodeStoreReader, P as PermissionMatrix, l as RevalidationEvent, R as RoleSummary, S as SchemaRegistryReader, c as buildPermissionMatrix, f as createPolicyEvaluator, d as describeRoleResolver, e as evaluateExpression, h as hubActionsForSpaceRole, s as schemaToHubPolicy } from '../grant-expiration-cleaner-BrMW0ffX.js';
3
+ import { F as PropertyDefinition, a as Schema, h as NodeState, S as SchemaIRI } from '../types-C64g-IXg.js';
4
+ export { a_ as DEFAULT_OFFLINE_POLICY, aY as GRANT_SCHEMA_IRI, b4 as Grant, x as GrantIndex, b9 as GrantIndexOptions, b8 as GrantIndexStore, b3 as GrantInput, b7 as GrantNode, aX as GrantRateLimiter, bc as GrantRateLimiterOptions, O as OfflineAuthPolicy, bb as RevocationConfig, ba as RevocationConsistency, aW as StoreAuth, e as StoreAuthAPI, b5 as StoreAuthError, b6 as StoreAuthErrorCode, b2 as StoreAuthKeyManager, b0 as StoreAuthOptions, b1 as StoreAuthStore, aZ as isGrantActive, a$ as mergeOfflinePolicy } from '../types-C64g-IXg.js';
5
+ export { A as ActionPermission, m as AuthDecisionEvent, b as DecisionCache, g as DecisionCacheOptions, D as DefaultPolicyEvaluator, j as DefaultPolicyEvaluatorOptions, a as DefaultRoleResolver, i as DefaultRoleResolverOptions, k as DefinedSchemaLike, G as GrantExpirationCleaner, o as GrantExpirationCleanerOptions, n as GrantExpirationStore, H as HubPolicy, N as NodeStoreReader, P as PermissionMatrix, l as RevalidationEvent, R as RoleSummary, S as SchemaRegistryReader, c as buildPermissionMatrix, f as createPolicyEvaluator, d as describeRoleResolver, e as evaluateExpression, h as hubActionsForSpaceRole, s as schemaToHubPolicy } from '../grant-expiration-cleaner-1by9wRlb.js';
6
6
  import { PublicKeyResolver } from '@xnetjs/crypto';
7
7
  import '@xnetjs/sqlite';
8
8
  import '@xnetjs/sync';
9
- import '../registry-BGD80QCd.js';
9
+ import '../registry-s1fYgCSj.js';
10
10
 
11
11
  /**
12
12
  * Authorization expression builders for schema definitions.
@@ -0,0 +1,12 @@
1
+ import {
2
+ CHECKPOINT_SCHEMA_IRI,
3
+ CheckpointSchema
4
+ } from "./chunk-6ZUJID52.js";
5
+ import "./chunk-JA56EQJO.js";
6
+ import "./chunk-LK5ZIAIE.js";
7
+ import "./chunk-RRXJNZX5.js";
8
+ import "./chunk-D4LUUZYD.js";
9
+ export {
10
+ CHECKPOINT_SCHEMA_IRI,
11
+ CheckpointSchema
12
+ };
@@ -1,14 +1,14 @@
1
- import {
2
- SYSTEM_SCHEMA_BASE_IRIS,
3
- isSystemNamespaceResource,
4
- isSystemSchemaIri
5
- } from "./chunk-XJBDESCX.js";
6
1
  import {
7
2
  applyNodeQueryDescriptor,
8
3
  getNodeQuerySearchTokens,
9
4
  withoutNodeQueryMaterializedView,
10
5
  withoutNodeQueryPagination
11
6
  } from "./chunk-S5RP5RKY.js";
7
+ import {
8
+ SYSTEM_SCHEMA_BASE_IRIS,
9
+ isSystemNamespaceResource,
10
+ isSystemSchemaIri
11
+ } from "./chunk-XJBDESCX.js";
12
12
  import {
13
13
  createNodeId,
14
14
  getBaseSchemaIRI
@@ -1393,6 +1393,14 @@ var NodeStore = class {
1393
1393
  listeners = /* @__PURE__ */ new Set();
1394
1394
  nodeListeners = /* @__PURE__ */ new Map();
1395
1395
  batchListeners = /* @__PURE__ */ new Set();
1396
+ // Draft overlay (exploration 0329)
1397
+ checkedOutDraft = null;
1398
+ cloneToOriginal = /* @__PURE__ */ new Map();
1399
+ draftMemberSet = /* @__PURE__ */ new Set();
1400
+ draftDynamicMembers = false;
1401
+ /** null = conservative "all schemas" (memberSchemaIds omitted). */
1402
+ draftMemberSchemaSet = null;
1403
+ draftOverlayListeners = /* @__PURE__ */ new Set();
1396
1404
  schemaLookup;
1397
1405
  propertyLookup;
1398
1406
  lensRegistry;
@@ -1472,6 +1480,7 @@ var NodeStore = class {
1472
1480
  });
1473
1481
  this.emit(change2, node2, null, false);
1474
1482
  this.authEvaluator?.invalidate(node2.id);
1483
+ this.notifyDraftNodeCreated(node2.id, node2.schemaId);
1475
1484
  if (this.telemetry) {
1476
1485
  this.telemetry.reportPerformance("data.create", Date.now() - start);
1477
1486
  this.telemetry.reportUsage("data.create", 1);
@@ -1507,6 +1516,7 @@ var NodeStore = class {
1507
1516
  await this.persistEncryptedNodeSnapshot(node);
1508
1517
  this.emit(change, node, null, false);
1509
1518
  this.authEvaluator?.invalidate(node.id);
1519
+ this.notifyDraftNodeCreated(node.id, node.schemaId);
1510
1520
  if (this.telemetry) {
1511
1521
  this.telemetry.reportPerformance("data.create", Date.now() - start);
1512
1522
  this.telemetry.reportUsage("data.create", 1);
@@ -1523,10 +1533,186 @@ var NodeStore = class {
1523
1533
  * Get a Node by ID.
1524
1534
  */
1525
1535
  async get(id) {
1536
+ const cloneId = this.checkedOutDraft?.clones[id];
1537
+ if (cloneId) {
1538
+ const clone = await this.getRaw(cloneId);
1539
+ if (clone) return { ...clone, id };
1540
+ }
1541
+ return this.getRaw(id);
1542
+ }
1543
+ /**
1544
+ * Read a node WITHOUT the draft overlay (exploration 0329) — merge review
1545
+ * and diff surfaces use this to see main's true state while checked out.
1546
+ */
1547
+ async getRaw(id) {
1526
1548
  const node = await this.storage.getNode(id);
1527
1549
  const decrypted = await this.decryptNodeSnapshotIfPresent(node);
1528
1550
  return await this.canReadNode(decrypted) ? decrypted : null;
1529
1551
  }
1552
+ // ==========================================================================
1553
+ // Draft overlay (exploration 0329)
1554
+ // ==========================================================================
1555
+ /**
1556
+ * Check a draft out (or pass null to return to main). While checked out,
1557
+ * member reads swap to clone content under original ids, member writes
1558
+ * redirect to clones (lazily forking via `onMissingMember`), and clone
1559
+ * change events mirror to original-id subscribers.
1560
+ */
1561
+ setCheckedOutDraft(overlay) {
1562
+ this.checkedOutDraft = overlay;
1563
+ this.cloneToOriginal = new Map(
1564
+ overlay ? Object.entries(overlay.clones).map(([orig, clone]) => [clone, orig]) : []
1565
+ );
1566
+ this.draftMemberSet = overlay && overlay.members !== "dynamic" ? new Set(overlay.members) : /* @__PURE__ */ new Set();
1567
+ this.draftDynamicMembers = overlay?.members === "dynamic";
1568
+ this.draftMemberSchemaSet = overlay?.memberSchemaIds ? new Set(overlay.memberSchemaIds) : null;
1569
+ this.notifyDraftOverlayListeners();
1570
+ }
1571
+ /** Report a locally-created node to the checkout's draft-born hook (0329 P4). */
1572
+ notifyDraftNodeCreated(nodeId, schemaId) {
1573
+ const overlay = this.checkedOutDraft;
1574
+ if (!overlay?.onNodeCreated) return;
1575
+ if (nodeId === overlay.draftId || this.cloneToOriginal.has(nodeId)) return;
1576
+ try {
1577
+ overlay.onNodeCreated(nodeId, schemaId);
1578
+ } catch (err) {
1579
+ console.error("Error in draft onNodeCreated hook:", err);
1580
+ }
1581
+ }
1582
+ notifyDraftOverlayListeners() {
1583
+ for (const listener of this.draftOverlayListeners) {
1584
+ try {
1585
+ listener();
1586
+ } catch (err) {
1587
+ console.error("Error in draft overlay listener:", err);
1588
+ }
1589
+ }
1590
+ }
1591
+ getCheckedOutDraft() {
1592
+ return this.checkedOutDraft;
1593
+ }
1594
+ /**
1595
+ * Device-local draft privacy (exploration 0329): ids marked here (draft
1596
+ * bookkeeping nodes, clones, draft-born nodes) are excluded from outbound
1597
+ * sync publication by the personal node-sync room's `shouldPublish`
1598
+ * predicate. In-memory only — the draft module rehydrates it from open
1599
+ * Draft nodes at boot, BEFORE sync starts.
1600
+ */
1601
+ draftPrivateNodeIds = /* @__PURE__ */ new Set();
1602
+ markDraftPrivate(ids) {
1603
+ for (const id of ids) this.draftPrivateNodeIds.add(id);
1604
+ }
1605
+ unmarkDraftPrivate(ids) {
1606
+ for (const id of ids) this.draftPrivateNodeIds.delete(id);
1607
+ }
1608
+ isDraftPrivate(id) {
1609
+ return this.draftPrivateNodeIds.has(id);
1610
+ }
1611
+ /** Notified on every checkout change (including clone-map refreshes). */
1612
+ subscribeToDraftOverlay(listener) {
1613
+ this.draftOverlayListeners.add(listener);
1614
+ return () => this.draftOverlayListeners.delete(listener);
1615
+ }
1616
+ /**
1617
+ * Swap checked-out members' content to their clones (original ids kept, so
1618
+ * MV id lists and grid ordering are untouched). No-op without a checkout.
1619
+ */
1620
+ async overlayStates(states) {
1621
+ const overlay = this.checkedOutDraft;
1622
+ if (!overlay) return states;
1623
+ const visible = this.cloneToOriginal.size > 0 ? states.filter((state) => !this.cloneToOriginal.has(state.id)) : states;
1624
+ const cloneIds = visible.map((s) => overlay.clones[s.id]).filter((cloneId) => cloneId !== void 0);
1625
+ if (cloneIds.length === 0) return visible;
1626
+ const cloneMap = await this.getNodesById(cloneIds, this.storage);
1627
+ return Promise.all(
1628
+ visible.map(async (state) => {
1629
+ const cloneId = overlay.clones[state.id];
1630
+ const clone = cloneId ? cloneMap.get(cloneId) : void 0;
1631
+ if (!clone) return state;
1632
+ const decrypted = await this.decryptNodeSnapshotIfPresent(clone);
1633
+ return decrypted ? { ...decrypted, id: state.id } : state;
1634
+ })
1635
+ );
1636
+ }
1637
+ /**
1638
+ * Draft-aware query path (0329 P5): candidates from storage WITHOUT
1639
+ * pagination, unioned with checked-out members the predicate may have
1640
+ * missed (their clone values can change membership), content-swapped,
1641
+ * clone rows hidden, then the full descriptor re-applied in JS so
1642
+ * filter/sort/pagination reflect draft values.
1643
+ */
1644
+ async draftAwareQuery(descriptor, start) {
1645
+ const overlay = this.checkedOutDraft;
1646
+ const unpaginated = withoutNodeQueryMaterializedView(withoutNodeQueryPagination(descriptor));
1647
+ let candidates;
1648
+ if (this.storage.queryNodes && !this.nodeContentCipher) {
1649
+ candidates = (await this.storage.queryNodes(unpaginated)).nodes;
1650
+ } else {
1651
+ const fallback = await this.loadQueryFallbackCandidates(unpaginated);
1652
+ const decrypted = await Promise.all(
1653
+ fallback.nodes.map((node) => this.decryptNodeSnapshotIfPresent(node))
1654
+ );
1655
+ candidates = decrypted.filter((node) => node !== null);
1656
+ }
1657
+ if (overlay) {
1658
+ const present = new Set(candidates.map((node) => node.id));
1659
+ const missing = Object.keys(overlay.clones).filter(
1660
+ (id) => !present.has(id)
1661
+ );
1662
+ if (missing.length > 0) {
1663
+ const extra = await this.getNodesById(missing, this.storage);
1664
+ for (const node of extra.values()) {
1665
+ const decrypted = await this.decryptNodeSnapshotIfPresent(node);
1666
+ if (!decrypted) continue;
1667
+ if (decrypted.schemaId !== descriptor.schemaId) continue;
1668
+ if (decrypted.deleted && !descriptor.includeDeleted) continue;
1669
+ candidates.push(decrypted);
1670
+ }
1671
+ }
1672
+ }
1673
+ const readable = await this.filterReadableNodes(candidates);
1674
+ const swapped = await this.overlayStates(readable);
1675
+ const filtered = applyNodeQueryDescriptor(swapped, withoutNodeQueryPagination(descriptor));
1676
+ const result = applyNodeQueryDescriptor(swapped, descriptor);
1677
+ return {
1678
+ nodes: result,
1679
+ totalCount: filtered.length,
1680
+ plan: {
1681
+ strategy: "draft-overlay",
1682
+ candidateNodeCount: readable.length,
1683
+ hydratedNodeCount: swapped.length,
1684
+ returnedNodeCount: result.length,
1685
+ durationMs: Date.now() - start
1686
+ }
1687
+ };
1688
+ }
1689
+ /**
1690
+ * Resolve where a write to `id` lands under the checkout: an existing
1691
+ * clone, a lazily-forked one (`onMissingMember`), or — for non-members,
1692
+ * clone ids, and declined forks — the id itself.
1693
+ */
1694
+ async resolveDraftWriteTarget(id) {
1695
+ const overlay = this.checkedOutDraft;
1696
+ if (!overlay) return id;
1697
+ const existing = overlay.clones[id];
1698
+ if (existing) return existing;
1699
+ if (this.draftDynamicMembers) {
1700
+ if (id === overlay.draftId || this.cloneToOriginal.has(id)) return id;
1701
+ } else if (!this.draftMemberSet.has(id)) {
1702
+ return id;
1703
+ }
1704
+ if (overlay.onMissingMember) {
1705
+ const cloneId = await overlay.onMissingMember(id);
1706
+ if (cloneId && this.checkedOutDraft === overlay) {
1707
+ this.checkedOutDraft = { ...overlay, clones: { ...overlay.clones, [id]: cloneId } };
1708
+ this.cloneToOriginal.set(cloneId, id);
1709
+ this.notifyDraftOverlayListeners();
1710
+ return cloneId;
1711
+ }
1712
+ if (cloneId) return cloneId;
1713
+ }
1714
+ return id;
1715
+ }
1530
1716
  /**
1531
1717
  * Return the subset of IDs that already exist in materialized storage.
1532
1718
  *
@@ -1606,6 +1792,7 @@ var NodeStore = class {
1606
1792
  */
1607
1793
  async update(id, options) {
1608
1794
  const start = this.telemetry ? Date.now() : 0;
1795
+ id = await this.resolveDraftWriteTarget(id);
1609
1796
  try {
1610
1797
  if (this.canUseSingleWriteFastPath()) {
1611
1798
  const { node: node2, previousNode, change: change2 } = await this.applySingleNodeWrite({
@@ -1688,6 +1875,7 @@ var NodeStore = class {
1688
1875
  */
1689
1876
  async delete(id) {
1690
1877
  const start = this.telemetry ? Date.now() : 0;
1878
+ id = await this.resolveDraftWriteTarget(id);
1691
1879
  try {
1692
1880
  if (this.canUseSingleWriteFastPath()) {
1693
1881
  const { node, previousNode, change: change2 } = await this.applySingleNodeWrite({
@@ -1753,6 +1941,7 @@ var NodeStore = class {
1753
1941
  * Restore a deleted Node.
1754
1942
  */
1755
1943
  async restore(id) {
1944
+ id = await this.resolveDraftWriteTarget(id);
1756
1945
  if (this.canUseSingleWriteFastPath()) {
1757
1946
  const { node: node2, previousNode, change: change2 } = await this.applySingleNodeWrite({
1758
1947
  nodeId: id,
@@ -1821,7 +2010,8 @@ var NodeStore = class {
1821
2010
  const readable = await this.filterReadableNodes(
1822
2011
  decrypted.filter((node) => node !== null)
1823
2012
  );
1824
- const result = this.authEvaluator ? this.applyListPagination(readable, options) : readable;
2013
+ const paged = this.authEvaluator ? this.applyListPagination(readable, options) : readable;
2014
+ const result = await this.overlayStates(paged);
1825
2015
  if (this.telemetry) {
1826
2016
  this.telemetry.reportPerformance("data.list", Date.now() - start);
1827
2017
  this.telemetry.reportUsage("data.list", 1);
@@ -1840,10 +2030,13 @@ var NodeStore = class {
1840
2030
  async query(descriptor) {
1841
2031
  const start = Date.now();
1842
2032
  try {
2033
+ if (this.checkedOutDraft && Object.keys(this.checkedOutDraft.clones).length > 0 && (this.draftMemberSchemaSet === null || this.draftMemberSchemaSet.has(descriptor.schemaId))) {
2034
+ return this.draftAwareQuery(descriptor, start);
2035
+ }
1843
2036
  if (this.storage.queryNodes && !this.nodeContentCipher && !this.authEvaluator) {
1844
2037
  const result2 = await this.storage.queryNodes(descriptor);
1845
2038
  return {
1846
- nodes: result2.nodes,
2039
+ nodes: await this.overlayStates(result2.nodes),
1847
2040
  totalCount: result2.totalCount,
1848
2041
  plan: {
1849
2042
  ...result2.plan,
@@ -1855,7 +2048,7 @@ var NodeStore = class {
1855
2048
  const authFingerprint = await this.authFingerprint();
1856
2049
  const result2 = await this.storage.queryNodes({ ...descriptor, authFingerprint });
1857
2050
  return {
1858
- nodes: result2.nodes,
2051
+ nodes: await this.overlayStates(result2.nodes),
1859
2052
  totalCount: result2.totalCount,
1860
2053
  plan: {
1861
2054
  ...result2.plan,
@@ -1866,7 +2059,7 @@ var NodeStore = class {
1866
2059
  if (this.storage.queryNodes && !this.nodeContentCipher && this.authEvaluator && !descriptor.nodeId && descriptor.materializedView === void 0) {
1867
2060
  const candidates = await this.storage.queryNodes(withoutNodeQueryPagination(descriptor));
1868
2061
  const readable2 = await this.filterReadableNodes(candidates.nodes);
1869
- const result2 = applyNodeQueryDescriptor(readable2, descriptor);
2062
+ const result2 = await this.overlayStates(applyNodeQueryDescriptor(readable2, descriptor));
1870
2063
  return {
1871
2064
  nodes: result2,
1872
2065
  totalCount: readable2.length,
@@ -1890,7 +2083,9 @@ var NodeStore = class {
1890
2083
  const readable = await this.filterReadableNodes(
1891
2084
  decrypted.filter((node) => node !== null)
1892
2085
  );
1893
- const result = applyNodeQueryDescriptor(readable, fallback.postFilterDescriptor);
2086
+ const result = await this.overlayStates(
2087
+ applyNodeQueryDescriptor(readable, fallback.postFilterDescriptor)
2088
+ );
1894
2089
  const totalCount = fallback.totalCount ?? (fallback.paginatedInStorage ? void 0 : applyNodeQueryDescriptor(
1895
2090
  readable,
1896
2091
  withoutNodeQueryPagination(fallback.postFilterDescriptor)
@@ -2685,6 +2880,24 @@ var NodeStore = class {
2685
2880
  }
2686
2881
  }
2687
2882
  }
2883
+ const originalId = this.cloneToOriginal.get(change.payload.nodeId);
2884
+ if (originalId) {
2885
+ const originalListeners = this.nodeListeners.get(originalId);
2886
+ if (originalListeners) {
2887
+ const mirrored = {
2888
+ ...event,
2889
+ node: event.node ? { ...event.node, id: originalId } : event.node,
2890
+ previousNode: event.previousNode ? { ...event.previousNode, id: originalId } : event.previousNode
2891
+ };
2892
+ for (const listener of originalListeners) {
2893
+ try {
2894
+ listener(mirrored);
2895
+ } catch (err) {
2896
+ console.error("Error in NodeStore draft-mirror listener:", err);
2897
+ }
2898
+ }
2899
+ }
2900
+ }
2688
2901
  }
2689
2902
  // ==========================================================================
2690
2903
  // Internal Methods
@@ -3415,6 +3628,41 @@ var MemoryNodeStorageAdapter = class {
3415
3628
  this.yjsSnapshotStore = this.yjsSnapshotStore.filter((s) => s.nodeId !== nodeId);
3416
3629
  }
3417
3630
  // ==========================================================================
3631
+ // Pin Registry (exploration 0329)
3632
+ // ==========================================================================
3633
+ /** key -> ownerId -> entry */
3634
+ pinStore = /* @__PURE__ */ new Map();
3635
+ pins = {
3636
+ addPins: async (pins) => {
3637
+ for (const pin of pins) {
3638
+ let owners = this.pinStore.get(pin.key);
3639
+ if (!owners) {
3640
+ owners = /* @__PURE__ */ new Map();
3641
+ this.pinStore.set(pin.key, owners);
3642
+ }
3643
+ owners.set(pin.ownerId, { ...pin });
3644
+ }
3645
+ },
3646
+ removePinsByOwner: async (ownerId) => {
3647
+ for (const [key, owners] of this.pinStore) {
3648
+ owners.delete(ownerId);
3649
+ if (owners.size === 0) this.pinStore.delete(key);
3650
+ }
3651
+ },
3652
+ getPinnedKeysAmong: async (keys) => {
3653
+ const pinned = /* @__PURE__ */ new Set();
3654
+ for (const key of keys) {
3655
+ if (this.pinStore.has(key)) pinned.add(key);
3656
+ }
3657
+ return pinned;
3658
+ },
3659
+ countPins: async () => {
3660
+ let count2 = 0;
3661
+ for (const owners of this.pinStore.values()) count2 += owners.size;
3662
+ return count2;
3663
+ }
3664
+ };
3665
+ // ==========================================================================
3418
3666
  // Utility Methods (for testing)
3419
3667
  // ==========================================================================
3420
3668
  /**
@@ -3426,6 +3674,7 @@ var MemoryNodeStorageAdapter = class {
3426
3674
  this.nodes.clear();
3427
3675
  this.documentContentStore.clear();
3428
3676
  this.yjsSnapshotStore = [];
3677
+ this.pinStore.clear();
3429
3678
  this.lastLamportTime = 0;
3430
3679
  this.syncCursors.clear();
3431
3680
  }
@@ -3465,6 +3714,9 @@ var MemoryNodeStorageAdapter = class {
3465
3714
  ])
3466
3715
  ),
3467
3716
  yjsSnapshotStore: structuredClone(this.yjsSnapshotStore),
3717
+ pinStore: new Map(
3718
+ Array.from(this.pinStore.entries(), ([key, owners]) => [key, new Map(owners)])
3719
+ ),
3468
3720
  lastLamportTime: this.lastLamportTime
3469
3721
  };
3470
3722
  }
@@ -3474,6 +3726,7 @@ var MemoryNodeStorageAdapter = class {
3474
3726
  this.nodes = snapshot.nodes;
3475
3727
  this.documentContentStore = snapshot.documentContentStore;
3476
3728
  this.yjsSnapshotStore = snapshot.yjsSnapshotStore;
3729
+ this.pinStore = snapshot.pinStore;
3477
3730
  this.lastLamportTime = snapshot.lastLamportTime;
3478
3731
  }
3479
3732
  };
@@ -4653,7 +4906,7 @@ var SQLiteNodeStorageAdapter = class {
4653
4906
  nodeReadAuthorizer;
4654
4907
  /** One-time guard: ensure the `auth_fingerprint` column exists on upgraded DBs. */
4655
4908
  materializationColumnsReady = false;
4656
- nodePropertyColumnsReady = false;
4909
+ nodePropertyColumnsReady = null;
4657
4910
  /**
4658
4911
  * Descriptor→SQL compilation lives in `query-compiler.ts` (exploration
4659
4912
  * 0276). Flags are read per-compile so the compiler never captures stale
@@ -4948,12 +5201,14 @@ var SQLiteNodeStorageAdapter = class {
4948
5201
  }
4949
5202
  // ─── Materialized State Operations ────────────────────────────────────────
4950
5203
  async getNode(id) {
5204
+ await this.ensureNodePropertyColumns();
4951
5205
  const nodes = await hydrateNodesByIds(this.db, [id], this.aggregatedHydration);
4952
5206
  return nodes[0] ?? null;
4953
5207
  }
4954
5208
  async getNodes(ids) {
4955
5209
  const uniqueIds = Array.from(new Set(ids));
4956
5210
  if (uniqueIds.length === 0) return [];
5211
+ await this.ensureNodePropertyColumns();
4957
5212
  return hydrateNodesByIds(this.db, uniqueIds, this.aggregatedHydration);
4958
5213
  }
4959
5214
  async getExistingNodeIds(ids) {
@@ -5075,6 +5330,7 @@ var SQLiteNodeStorageAdapter = class {
5075
5330
  * Fetches nodes and properties in one query for better performance with large datasets.
5076
5331
  */
5077
5332
  async listNodesOptimized(options) {
5333
+ await this.ensureNodePropertyColumns();
5078
5334
  let whereClause = "1=1";
5079
5335
  const params = [];
5080
5336
  if (options?.schemaId) {
@@ -5136,6 +5392,7 @@ var SQLiteNodeStorageAdapter = class {
5136
5392
  }
5137
5393
  async queryNodes(descriptor) {
5138
5394
  const start = Date.now();
5395
+ await this.ensureNodePropertyColumns();
5139
5396
  if (descriptor.materializedView) {
5140
5397
  return this.queryMaterializedView(descriptor, start);
5141
5398
  }
@@ -5326,6 +5583,47 @@ var SQLiteNodeStorageAdapter = class {
5326
5583
  [nodeId, content, Date.now()]
5327
5584
  );
5328
5585
  }
5586
+ // ─── Pin Registry (exploration 0329) ──────────────────────────────────────
5587
+ pins = {
5588
+ addPins: async (pins) => {
5589
+ if (pins.length === 0) return;
5590
+ await this.enqueueWrite(async () => {
5591
+ const now = Date.now();
5592
+ for (const chunk of chunkItems(pins, 200)) {
5593
+ const placeholders = chunk.map(() => "(?, ?, ?, ?)").join(", ");
5594
+ const params = [];
5595
+ for (const pin of chunk) params.push(pin.key, pin.ownerId, pin.reason, now);
5596
+ await this.db.run(
5597
+ `INSERT INTO pinned_changes (pin_key, owner_id, reason, created_at)
5598
+ VALUES ${placeholders}
5599
+ ON CONFLICT(pin_key, owner_id) DO NOTHING`,
5600
+ params
5601
+ );
5602
+ }
5603
+ });
5604
+ },
5605
+ removePinsByOwner: async (ownerId) => {
5606
+ await this.enqueueWrite(async () => {
5607
+ await this.db.run(`DELETE FROM pinned_changes WHERE owner_id = ?`, [ownerId]);
5608
+ });
5609
+ },
5610
+ getPinnedKeysAmong: async (keys) => {
5611
+ const pinned = /* @__PURE__ */ new Set();
5612
+ for (const chunk of chunkItems(keys, 500)) {
5613
+ const placeholders = chunk.map(() => "?").join(", ");
5614
+ const rows = await this.db.query(
5615
+ `SELECT DISTINCT pin_key FROM pinned_changes WHERE pin_key IN (${placeholders})`,
5616
+ [...chunk]
5617
+ );
5618
+ for (const row of rows) pinned.add(row.pin_key);
5619
+ }
5620
+ return pinned;
5621
+ },
5622
+ countPins: async () => {
5623
+ const row = await this.db.queryOne(`SELECT COUNT(*) AS n FROM pinned_changes`);
5624
+ return row?.n ?? 0;
5625
+ }
5626
+ };
5329
5627
  // ─── Yjs Snapshots (Extended) ─────────────────────────────────────────────
5330
5628
  /**
5331
5629
  * Save a Yjs snapshot for time travel.
@@ -5953,15 +6251,16 @@ var SQLiteNodeStorageAdapter = class {
5953
6251
  * just falls back to the author-DID tiebreak.
5954
6252
  */
5955
6253
  async ensureNodePropertyColumns() {
5956
- if (this.nodePropertyColumnsReady) return;
5957
- try {
5958
- const columns = await this.db.query(`PRAGMA table_info(node_properties)`);
5959
- if (!columns.some((column) => column.name === "tiebreak_key")) {
5960
- await this.db.run("ALTER TABLE node_properties ADD COLUMN tiebreak_key TEXT");
6254
+ this.nodePropertyColumnsReady ??= (async () => {
6255
+ try {
6256
+ const columns = await this.db.query(`PRAGMA table_info(node_properties)`);
6257
+ if (!columns.some((column) => column.name === "tiebreak_key")) {
6258
+ await this.db.run("ALTER TABLE node_properties ADD COLUMN tiebreak_key TEXT");
6259
+ }
6260
+ } catch {
5961
6261
  }
5962
- } catch {
5963
- }
5964
- this.nodePropertyColumnsReady = true;
6262
+ })();
6263
+ return this.nodePropertyColumnsReady;
5965
6264
  }
5966
6265
  setNodeReadAuthorizer(authorizer) {
5967
6266
  this.nodeReadAuthorizer = authorizer;
@@ -0,0 +1,41 @@
1
+ import {
2
+ spaceContributorAuthorization
3
+ } from "./chunk-JA56EQJO.js";
4
+ import {
5
+ created,
6
+ createdBy,
7
+ defineSchema,
8
+ json,
9
+ relation,
10
+ text
11
+ } from "./chunk-RRXJNZX5.js";
12
+
13
+ // src/schema/schemas/checkpoint.ts
14
+ var CHECKPOINT_SCHEMA_IRI = "xnet://xnet.fyi/Checkpoint@1.0.0";
15
+ var CheckpointSchema = defineSchema({
16
+ name: "Checkpoint",
17
+ namespace: "xnet://xnet.fyi/",
18
+ properties: {
19
+ /** User-given version name ("Draft sent to review", "Before the rewrite") */
20
+ name: text({ required: true, maxLength: 200 }),
21
+ /** Optional longer note shown in the Time Machine */
22
+ note: text({ maxLength: 2e3 }),
23
+ /** Per-node frontier: nodeId -> { hash, yjsSnapshotRef? } */
24
+ frontier: json({}),
25
+ /** The scope this checkpoint was taken over (page, database, space, …) */
26
+ scope: relation({}),
27
+ /** Space containment (single-valued) — drives the authorization roles */
28
+ space: relation({ target: "xnet://xnet.fyi/Space@1.0.0" }),
29
+ createdAt: created(),
30
+ createdBy: createdBy()
31
+ },
32
+ document: void 0,
33
+ // Checkpoints of shared content are visible to the space that content
34
+ // lives in; contributors can create their own.
35
+ authorization: spaceContributorAuthorization()
36
+ });
37
+
38
+ export {
39
+ CHECKPOINT_SCHEMA_IRI,
40
+ CheckpointSchema
41
+ };
@@ -1,3 +1,6 @@
1
+ import {
2
+ createNodeQueryDescriptor
3
+ } from "./chunk-S5RP5RKY.js";
1
4
  import {
2
5
  extKey
3
6
  } from "./chunk-PMUQACPY.js";
@@ -26,9 +29,6 @@ import {
26
29
  import {
27
30
  DatabaseViewSchema
28
31
  } from "./chunk-TRPXLCND.js";
29
- import {
30
- createNodeQueryDescriptor
31
- } from "./chunk-S5RP5RKY.js";
32
32
 
33
33
  // src/database/cell-types.ts
34
34
  var CELL_PREFIX = "cell_";
@@ -264,6 +264,30 @@ function getRichTextPlainText(doc, columnId) {
264
264
  const fragment = doc.getXmlFragment(`${RICHTEXT_PREFIX}${columnId}`);
265
265
  return extractPlainText(fragment);
266
266
  }
267
+ var BLOCK_ELEMENT_NAMES = /* @__PURE__ */ new Set([
268
+ // HTML-ish / legacy names
269
+ "p",
270
+ "h1",
271
+ "h2",
272
+ "h3",
273
+ "h4",
274
+ "h5",
275
+ "h6",
276
+ "li",
277
+ "blockquote",
278
+ // TipTap / BlockNote shared names
279
+ "paragraph",
280
+ "heading",
281
+ "codeBlock",
282
+ "listItem",
283
+ "taskItem",
284
+ // BlockNote (v4) block-content names
285
+ "bulletListItem",
286
+ "numberedListItem",
287
+ "checkListItem",
288
+ "toggleListItem",
289
+ "quote"
290
+ ]);
267
291
  function extractPlainText(fragment) {
268
292
  const parts = [];
269
293
  for (let i = 0; i < fragment.length; i++) {
@@ -276,7 +300,33 @@ function extractPlainText(fragment) {
276
300
  }
277
301
  return parts.join("");
278
302
  }
303
+ function inlineAtomText(element) {
304
+ const attr = (key) => {
305
+ const value = element.getAttribute(key);
306
+ return typeof value === "string" ? value : "";
307
+ };
308
+ switch (element.nodeName) {
309
+ case "mention": {
310
+ const label = attr("label") || attr("id");
311
+ return label ? `@${label}` : "";
312
+ }
313
+ case "hashtag": {
314
+ const name = attr("name");
315
+ return name ? `#${name}` : "";
316
+ }
317
+ case "wikilink":
318
+ return element.length === 0 ? attr("title") : null;
319
+ case "inlineMath":
320
+ return attr("latex");
321
+ default:
322
+ return null;
323
+ }
324
+ }
279
325
  function extractPlainTextFromElement(element) {
326
+ const atomText = inlineAtomText(element);
327
+ if (atomText !== null) {
328
+ return atomText;
329
+ }
280
330
  const parts = [];
281
331
  for (let i = 0; i < element.length; i++) {
282
332
  const item = element.get(i);
@@ -286,8 +336,7 @@ function extractPlainTextFromElement(element) {
286
336
  parts.push(extractPlainTextFromElement(item));
287
337
  }
288
338
  }
289
- const blockElements = ["p", "h1", "h2", "h3", "h4", "h5", "h6", "li", "blockquote"];
290
- if (blockElements.includes(element.nodeName)) {
339
+ if (BLOCK_ELEMENT_NAMES.has(element.nodeName)) {
291
340
  parts.push("\n");
292
341
  }
293
342
  return parts.join("");