@xnetjs/react 1.0.0 → 2.1.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.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  useInfiniteQuery,
6
6
  useIsOffline,
7
7
  useNode
8
- } from "./chunk-Q3NDLI6Z.js";
8
+ } from "./chunk-VZMZQTR7.js";
9
9
  import {
10
10
  useCell,
11
11
  useDatabase,
@@ -14,7 +14,7 @@ import {
14
14
  useDatabaseSchema,
15
15
  useRelatedRows,
16
16
  useReverseRelations
17
- } from "./chunk-WL7OCTSM.js";
17
+ } from "./chunk-HTFAHJQK.js";
18
18
  import {
19
19
  AuthErrorScreen,
20
20
  AuthenticatingScreen,
@@ -94,7 +94,7 @@ import {
94
94
  useUndo,
95
95
  useVerification,
96
96
  useVisibleComments
97
- } from "./chunk-MEIYFD6R.js";
97
+ } from "./chunk-ANCTAEML.js";
98
98
  import {
99
99
  EMPTY_PAGE_INFO,
100
100
  computeFallbackPageInfo,
@@ -106,10 +106,10 @@ import {
106
106
  useMutate,
107
107
  useQuery,
108
108
  useSyncManager
109
- } from "./chunk-EJEGIEIC.js";
109
+ } from "./chunk-ZGP56IIJ.js";
110
110
  import {
111
111
  useUndoScope
112
- } from "./chunk-BDBDZXYS.js";
112
+ } from "./chunk-SZQNGRTO.js";
113
113
  import {
114
114
  InstrumentationContext,
115
115
  useInstrumentation
@@ -122,6 +122,7 @@ import {
122
122
  TracingContext,
123
123
  XNetContext,
124
124
  XNetProvider,
125
+ mergeEditorContributions,
125
126
  useCommand,
126
127
  useCommands,
127
128
  useContributions,
@@ -129,6 +130,7 @@ import {
129
130
  useEditorExtensions,
130
131
  useEditorExtensionsSafe,
131
132
  useImporters,
133
+ useMergedEditorContributions,
132
134
  useNodeStore,
133
135
  usePluginRegistry,
134
136
  usePluginRegistryOptional,
@@ -142,7 +144,7 @@ import {
142
144
  useView,
143
145
  useViews,
144
146
  useXNet
145
- } from "./chunk-RIN2MXZK.js";
147
+ } from "./chunk-H44ZSIH4.js";
146
148
 
147
149
  // src/hooks/useEffectiveSchema.ts
148
150
  import {
@@ -4375,7 +4377,7 @@ function dropSortKey(ordered, movedId, targetIndex) {
4375
4377
  }
4376
4378
  var formulaService = new FormulaService();
4377
4379
  function useGridDatabase(databaseId, options = {}) {
4378
- const { viewId, search, pageSize = 500 } = options;
4380
+ const { viewId, search, pageSize = 500, maxLoaded = 2e3 } = options;
4379
4381
  const mutate = useMutate();
4380
4382
  const { did } = useIdentity();
4381
4383
  const { data: database, status: dbStatus } = useQuery(DatabaseSchema, databaseId);
@@ -4391,10 +4393,22 @@ function useGridDatabase(databaseId, options = {}) {
4391
4393
  where: { database: databaseId },
4392
4394
  orderBy: { sortKey: "asc" }
4393
4395
  });
4394
- const { data: rowNodes, status: rowStatus } = useQuery(DatabaseRowSchema, {
4396
+ const {
4397
+ data: rowNodes,
4398
+ // Masked while a window grow is in flight (previous rows stay visible):
4399
+ // consumers must not fall back to a skeleton mid-grow or scroll position
4400
+ // is lost — `isFetchingMoreRows` reports the in-flight grow instead.
4401
+ loading: rowsLoading,
4402
+ totalCount: totalRowCount,
4403
+ hasMore: hasMoreRows,
4404
+ isFetchingNextPage: isFetchingMoreRows,
4405
+ fetchNextPage: fetchMoreRows
4406
+ } = useInfiniteQuery(DatabaseRowSchema, {
4395
4407
  where: { database: databaseId },
4396
4408
  orderBy: { sortKey: "asc" },
4397
- limit: pageSize,
4409
+ pageSize,
4410
+ maxLoaded,
4411
+ page: { count: "exact" },
4398
4412
  materializedView: `db:${databaseId}${viewId ? `:view:${viewId}` : ""}`,
4399
4413
  ...search ? { search } : {}
4400
4414
  });
@@ -4482,11 +4496,22 @@ function useGridDatabase(databaseId, options = {}) {
4482
4496
  cancelled = true;
4483
4497
  };
4484
4498
  }, [rowNodes, fields, store]);
4499
+ const rowModelCacheRef = useRef4(
4500
+ { deps: [], map: /* @__PURE__ */ new WeakMap() }
4501
+ );
4485
4502
  const rows = useMemo5(() => {
4503
+ const cacheDeps = [fields, rollupValues, databaseId];
4504
+ if (cacheDeps.length !== rowModelCacheRef.current.deps.length || cacheDeps.some((d, i) => d !== rowModelCacheRef.current.deps[i])) {
4505
+ rowModelCacheRef.current = { deps: cacheDeps, map: /* @__PURE__ */ new WeakMap() };
4506
+ }
4507
+ const cache = rowModelCacheRef.current.map;
4486
4508
  const autoFields = fields.filter(
4487
4509
  (f) => ["created", "createdBy", "updated", "updatedBy"].includes(f.type)
4488
4510
  );
4489
- const base = (rowNodes ?? []).map((node) => {
4511
+ const formulaFields = fields.filter((f) => f.type === "formula");
4512
+ const rollupFields = fields.filter((f) => f.type === "rollup");
4513
+ const columnDefs = formulaFields.length > 0 || activeView ? fieldsToColumnDefinitions(fields) : [];
4514
+ const buildRow = (node) => {
4490
4515
  const cells = fromCellProperties(node);
4491
4516
  for (const field of autoFields) {
4492
4517
  switch (field.type) {
@@ -4504,47 +4529,43 @@ function useGridDatabase(databaseId, options = {}) {
4504
4529
  break;
4505
4530
  }
4506
4531
  }
4507
- return {
4532
+ const row = {
4508
4533
  id: node.id,
4509
4534
  sortKey: node.sortKey ?? "",
4510
4535
  cells
4511
4536
  };
4512
- });
4513
- const formulaFields = fields.filter((f) => f.type === "formula");
4514
- if (formulaFields.length > 0) {
4515
- const columnDefs = fieldsToColumnDefinitions(fields);
4516
- for (const row of base) {
4517
- for (const field of formulaFields) {
4518
- const columnDef = columnDefs.find((c) => c.id === field.id);
4519
- if (!columnDef) continue;
4520
- try {
4521
- row.cells[field.id] = formulaService.compute(
4522
- { id: row.id, databaseId, cells: row.cells },
4523
- columnDef,
4524
- columnDefs
4525
- );
4526
- } catch {
4527
- row.cells[field.id] = null;
4528
- }
4537
+ for (const field of formulaFields) {
4538
+ const columnDef = columnDefs.find((c) => c.id === field.id);
4539
+ if (!columnDef) continue;
4540
+ try {
4541
+ row.cells[field.id] = formulaService.compute(
4542
+ { id: row.id, databaseId, cells: row.cells },
4543
+ columnDef,
4544
+ columnDefs
4545
+ );
4546
+ } catch {
4547
+ row.cells[field.id] = null;
4529
4548
  }
4530
4549
  }
4531
- }
4532
- if (rollupValues.size > 0) {
4533
- for (const row of base) {
4534
- for (const field of fields) {
4535
- if (field.type !== "rollup") continue;
4536
- const computed = rollupValues.get(`${row.id}:${field.id}`);
4537
- if (computed !== void 0) row.cells[field.id] = computed;
4538
- }
4550
+ for (const field of rollupFields) {
4551
+ const computed = rollupValues.get(`${row.id}:${field.id}`);
4552
+ if (computed !== void 0) row.cells[field.id] = computed;
4539
4553
  }
4540
- }
4554
+ return row;
4555
+ };
4556
+ const base = (rowNodes ?? []).map((node) => {
4557
+ const hit = cache.get(node);
4558
+ if (hit) return hit;
4559
+ const row = buildRow(node);
4560
+ cache.set(node, row);
4561
+ return row;
4562
+ });
4541
4563
  base.sort((a, b) => compareSortKeys(a.sortKey, b.sortKey));
4542
4564
  if (!activeView) return base;
4543
- const columns = fieldsToColumnDefinitions(fields);
4544
- const filtered = filterRows(base, columns, activeView.filters);
4545
- return sortRows(filtered, columns, activeView.sorts);
4565
+ const filtered = filterRows(base, columnDefs, activeView.filters);
4566
+ return sortRows(filtered, columnDefs, activeView.sorts);
4546
4567
  }, [rowNodes, activeView, fields, databaseId, rollupValues]);
4547
- const loading = dbStatus === "loading" || fieldStatus === "loading" || viewStatus === "loading" || rowStatus === "loading";
4568
+ const loading = dbStatus === "loading" || fieldStatus === "loading" || viewStatus === "loading" || rowsLoading;
4548
4569
  const rowTailRef = useRef4(void 0);
4549
4570
  const fieldTailRef = useRef4(void 0);
4550
4571
  const viewTailRef = useRef4(void 0);
@@ -4897,6 +4918,10 @@ function useGridDatabase(databaseId, options = {}) {
4897
4918
  activeView,
4898
4919
  rows,
4899
4920
  loading,
4921
+ totalRowCount,
4922
+ hasMoreRows,
4923
+ isFetchingMoreRows,
4924
+ fetchMoreRows,
4900
4925
  updateCell,
4901
4926
  clearCells,
4902
4927
  addRow,
@@ -4929,8 +4954,531 @@ function useGridDatabase(databaseId, options = {}) {
4929
4954
  };
4930
4955
  }
4931
4956
 
4957
+ // src/hooks/useTimeMachine.ts
4958
+ import { CHECKPOINT_SCHEMA_IRI } from "@xnetjs/data";
4959
+ import {
4960
+ HistoryEngine,
4961
+ HistoryHorizonError,
4962
+ MemorySnapshotStorage,
4963
+ ScopeTimeline,
4964
+ SnapshotCache,
4965
+ createCheckpoint,
4966
+ listCheckpoints,
4967
+ restoreToFrontier
4968
+ } from "@xnetjs/history";
4969
+ import { useCallback as useCallback4, useEffect as useEffect7, useMemo as useMemo6, useRef as useRef5, useState as useState7 } from "react";
4970
+ function useTimeMachine(nodeId, options) {
4971
+ const { store, isReady } = useNodeStore();
4972
+ const memberKey = options?.memberIds?.join(",") ?? "";
4973
+ const members = useMemo6(() => {
4974
+ if (!nodeId) return [];
4975
+ const extra = options?.memberIds ?? [];
4976
+ return [nodeId, ...extra.filter((id) => id !== nodeId)];
4977
+ }, [nodeId, memberKey]);
4978
+ const [timeline, setTimeline] = useState7([]);
4979
+ const [checkpoints, setCheckpoints] = useState7([]);
4980
+ const [horizon, setHorizon] = useState7(null);
4981
+ const [docSnapshotCount, setDocSnapshotCount] = useState7(null);
4982
+ const [loading, setLoading] = useState7(false);
4983
+ const [error, setError] = useState7(null);
4984
+ const [rawPosition, setRawPosition] = useState7(null);
4985
+ const position = rawPosition === null ? Math.max(0, timeline.length - 1) : Math.max(0, Math.min(rawPosition, timeline.length - 1));
4986
+ const atLatest = timeline.length === 0 || position === timeline.length - 1;
4987
+ const [preview, setPreview] = useState7(null);
4988
+ const [diffs, setDiffs] = useState7([]);
4989
+ const engineRef = useRef5(null);
4990
+ const getEngines = useCallback4(() => {
4991
+ if (!store) return null;
4992
+ const storage = store.getStorageAdapter();
4993
+ if (!storage) return null;
4994
+ if (!engineRef.current || engineRef.current.storage !== storage) {
4995
+ const snapshots = new SnapshotCache(new MemorySnapshotStorage(), { interval: 50 });
4996
+ engineRef.current = {
4997
+ engine: new HistoryEngine(storage, snapshots),
4998
+ scope: new ScopeTimeline(storage),
4999
+ storage
5000
+ };
5001
+ }
5002
+ return engineRef.current;
5003
+ }, [store]);
5004
+ const reload = useCallback4(async () => {
5005
+ if (!nodeId || !isReady || !store) return;
5006
+ const engines = getEngines();
5007
+ if (!engines) return;
5008
+ setLoading(true);
5009
+ setError(null);
5010
+ try {
5011
+ const [line, nodeHorizon, named] = await Promise.all([
5012
+ engines.scope.getMergedTimeline(members),
5013
+ engines.engine.getHorizon(nodeId),
5014
+ listCheckpoints(store, nodeId)
5015
+ ]);
5016
+ setTimeline(line);
5017
+ setHorizon(nodeHorizon);
5018
+ setCheckpoints(named);
5019
+ const snapshotStorage = engines.storage;
5020
+ if (typeof snapshotStorage.getYjsSnapshots === "function") {
5021
+ const snapshots = await snapshotStorage.getYjsSnapshots(nodeId);
5022
+ setDocSnapshotCount(snapshots.length);
5023
+ } else {
5024
+ setDocSnapshotCount(null);
5025
+ }
5026
+ } catch (err) {
5027
+ setError(err instanceof Error ? err : new Error(String(err)));
5028
+ } finally {
5029
+ setLoading(false);
5030
+ }
5031
+ }, [nodeId, isReady, store, members, getEngines]);
5032
+ useEffect7(() => {
5033
+ setRawPosition(null);
5034
+ void reload();
5035
+ }, [reload]);
5036
+ useEffect7(() => {
5037
+ if (!store || !nodeId) return;
5038
+ const memberSet = new Set(members);
5039
+ const unsub = store.subscribe((event) => {
5040
+ if (memberSet.has(event.change.payload.nodeId) || event.change.payload.schemaId === CHECKPOINT_SCHEMA_IRI) {
5041
+ void reload();
5042
+ }
5043
+ });
5044
+ return unsub;
5045
+ }, [store, nodeId, members, reload]);
5046
+ useEffect7(() => {
5047
+ let cancelled = false;
5048
+ const engines = getEngines();
5049
+ if (!nodeId || !engines || timeline.length === 0) {
5050
+ setPreview(null);
5051
+ setDiffs([]);
5052
+ return;
5053
+ }
5054
+ let hash = null;
5055
+ for (let i = position; i >= 0; i--) {
5056
+ if (timeline[i].nodeId === nodeId) {
5057
+ hash = timeline[i].change.hash;
5058
+ break;
5059
+ }
5060
+ }
5061
+ if (!hash) {
5062
+ setPreview(null);
5063
+ setDiffs([]);
5064
+ return;
5065
+ }
5066
+ const target = { type: "hash", hash };
5067
+ Promise.all([
5068
+ engines.engine.materializeAt(nodeId, target),
5069
+ engines.engine.diff(nodeId, target, { type: "latest" })
5070
+ ]).then(([state, propertyDiffs]) => {
5071
+ if (cancelled) return;
5072
+ setPreview(state.node);
5073
+ setDiffs(propertyDiffs);
5074
+ }).catch((err) => {
5075
+ if (cancelled) return;
5076
+ setPreview(null);
5077
+ setDiffs([]);
5078
+ if (err instanceof HistoryHorizonError) {
5079
+ setHorizon(err.horizon);
5080
+ } else {
5081
+ setError(err instanceof Error ? err : new Error(String(err)));
5082
+ }
5083
+ });
5084
+ return () => {
5085
+ cancelled = true;
5086
+ };
5087
+ }, [nodeId, timeline, position, getEngines]);
5088
+ const setPosition = useCallback4(
5089
+ (index) => {
5090
+ const max = timeline.length - 1;
5091
+ const clamped = Math.max(0, Math.min(index, max));
5092
+ setRawPosition(clamped >= max ? null : clamped);
5093
+ },
5094
+ [timeline.length]
5095
+ );
5096
+ const stepBack = useCallback4(() => setPosition(position - 1), [setPosition, position]);
5097
+ const stepForward = useCallback4(() => setPosition(position + 1), [setPosition, position]);
5098
+ const goToLatest = useCallback4(() => setRawPosition(null), []);
5099
+ const createNamedVersion = useCallback4(
5100
+ async (name, note) => {
5101
+ if (!nodeId || !store) return null;
5102
+ const engines = getEngines();
5103
+ if (!engines) return null;
5104
+ try {
5105
+ const checkpoint = await createCheckpoint(store, engines.storage, {
5106
+ name,
5107
+ ...note !== void 0 ? { note } : {},
5108
+ nodeIds: members,
5109
+ scopeId: nodeId
5110
+ });
5111
+ await reload();
5112
+ return checkpoint;
5113
+ } catch (err) {
5114
+ setError(err instanceof Error ? err : new Error(String(err)));
5115
+ return null;
5116
+ }
5117
+ },
5118
+ [nodeId, store, members, getEngines, reload]
5119
+ );
5120
+ const positionOfCheckpoint = useCallback4(
5121
+ (checkpoint) => {
5122
+ const frontier = checkpoint.properties.frontier;
5123
+ if (!frontier) return null;
5124
+ let best = -1;
5125
+ for (let i = 0; i < timeline.length; i++) {
5126
+ const pinned = frontier[timeline[i].nodeId];
5127
+ if (pinned && pinned.hash === timeline[i].change.hash) {
5128
+ best = Math.max(best, i);
5129
+ }
5130
+ }
5131
+ return best >= 0 ? best : null;
5132
+ },
5133
+ [timeline]
5134
+ );
5135
+ const frontierAt = useCallback4(
5136
+ (index) => {
5137
+ const scope = getEngines()?.scope;
5138
+ if (!scope) return {};
5139
+ return scope.frontierAtPosition(timeline, index);
5140
+ },
5141
+ [getEngines, timeline]
5142
+ );
5143
+ const restore = useCallback4(async () => {
5144
+ if (!nodeId || !store || timeline.length === 0) return null;
5145
+ const engines = getEngines();
5146
+ if (!engines) return null;
5147
+ try {
5148
+ const frontier = engines.scope.frontierAtPosition(timeline, position);
5149
+ const result = await restoreToFrontier(store, engines.engine, frontier, members);
5150
+ setRawPosition(null);
5151
+ await reload();
5152
+ return result;
5153
+ } catch (err) {
5154
+ if (err instanceof HistoryHorizonError) setHorizon(err.horizon);
5155
+ setError(err instanceof Error ? err : new Error(String(err)));
5156
+ return null;
5157
+ }
5158
+ }, [nodeId, store, timeline, position, members, getEngines, reload]);
5159
+ return {
5160
+ timeline,
5161
+ changeCount: timeline.length,
5162
+ position,
5163
+ atLatest,
5164
+ setPosition,
5165
+ stepBack,
5166
+ stepForward,
5167
+ goToLatest,
5168
+ preview,
5169
+ diffs,
5170
+ horizon,
5171
+ docSnapshotCount,
5172
+ checkpoints,
5173
+ createNamedVersion,
5174
+ positionOfCheckpoint,
5175
+ frontierAt,
5176
+ restore,
5177
+ loading,
5178
+ error,
5179
+ reload
5180
+ };
5181
+ }
5182
+
5183
+ // src/hooks/useDraft.ts
5184
+ import { DRAFT_SCHEMA_IRI, DatabaseRowSchema as DatabaseRowSchema2, DatabaseSchema as DatabaseSchema2 } from "@xnetjs/data";
5185
+ import {
5186
+ HistoryEngine as HistoryEngine2,
5187
+ MemorySnapshotStorage as MemorySnapshotStorage2,
5188
+ SnapshotCache as SnapshotCache2,
5189
+ createDraft as createDraftNode,
5190
+ discardDraft,
5191
+ draftEntries,
5192
+ forkNodeIntoDraft,
5193
+ listDrafts,
5194
+ mergeDraft,
5195
+ refreshDraftFromMain,
5196
+ threeWayPropertyMerge
5197
+ } from "@xnetjs/history";
5198
+ import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef6, useState as useState8 } from "react";
5199
+ var baseIRI = (schemaId) => schemaId.split("@")[0];
5200
+ async function resolveMembers(store, hostId) {
5201
+ const members = [hostId];
5202
+ const host = await store.getRaw(hostId);
5203
+ if (host && baseIRI(host.schemaId) === baseIRI(DatabaseSchema2.schema["@id"])) {
5204
+ const rows = await store.list({ schemaId: DatabaseRowSchema2.schema["@id"] });
5205
+ for (const row of rows) {
5206
+ if (row.properties.database === hostId) members.push(row.id);
5207
+ }
5208
+ }
5209
+ return members;
5210
+ }
5211
+ function clonesFromEntries(draft) {
5212
+ const clones = {};
5213
+ for (const [originalId, entry] of Object.entries(draftEntries(draft))) {
5214
+ clones[originalId] = entry.cloneId;
5215
+ }
5216
+ return clones;
5217
+ }
5218
+ function useDraft(hostId) {
5219
+ const { store, isReady } = useNodeStore();
5220
+ const [drafts, setDrafts] = useState8([]);
5221
+ const [checkedOut, setCheckedOut] = useState8(null);
5222
+ const [loading, setLoading] = useState8(false);
5223
+ const [error, setError] = useState8(null);
5224
+ const engineRef = useRef6(null);
5225
+ const getEngines = useCallback5(() => {
5226
+ if (!store) return null;
5227
+ const storage = store.getStorageAdapter();
5228
+ if (!storage) return null;
5229
+ if (!engineRef.current || engineRef.current.storage !== storage) {
5230
+ engineRef.current = {
5231
+ engine: new HistoryEngine2(
5232
+ storage,
5233
+ new SnapshotCache2(new MemorySnapshotStorage2(), { interval: 50 })
5234
+ ),
5235
+ storage
5236
+ };
5237
+ }
5238
+ return engineRef.current;
5239
+ }, [store]);
5240
+ const draftIdsRef = useRef6(/* @__PURE__ */ new Set());
5241
+ const reload = useCallback5(async () => {
5242
+ if (!hostId || !isReady || !store) return;
5243
+ setLoading(true);
5244
+ setError(null);
5245
+ try {
5246
+ const open = await listDrafts(store, hostId);
5247
+ setDrafts(open);
5248
+ for (const draft of open) draftIdsRef.current.add(draft.id);
5249
+ const overlay = store.getCheckedOutDraft();
5250
+ if (overlay) {
5251
+ const draftNode = await store.getRaw(overlay.draftId);
5252
+ setCheckedOut(
5253
+ draftNode && draftNode.properties.status === "open" && draftNode.properties.target === hostId ? draftNode : null
5254
+ );
5255
+ } else {
5256
+ setCheckedOut(null);
5257
+ }
5258
+ } catch (err) {
5259
+ setError(err instanceof Error ? err : new Error(String(err)));
5260
+ } finally {
5261
+ setLoading(false);
5262
+ }
5263
+ }, [hostId, isReady, store]);
5264
+ useEffect8(() => {
5265
+ void reload();
5266
+ }, [reload]);
5267
+ useEffect8(() => {
5268
+ if (!store || !hostId) return;
5269
+ const unsubStore = store.subscribe((event) => {
5270
+ if (event.change.payload.schemaId === DRAFT_SCHEMA_IRI || draftIdsRef.current.has(event.change.payload.nodeId)) {
5271
+ void reload();
5272
+ }
5273
+ });
5274
+ const unsubOverlay = store.subscribeToDraftOverlay(() => void reload());
5275
+ return () => {
5276
+ unsubStore();
5277
+ unsubOverlay();
5278
+ };
5279
+ }, [store, hostId, reload]);
5280
+ const checkout = useCallback5(
5281
+ async (draftId) => {
5282
+ if (!hostId || !store) return false;
5283
+ const engines = getEngines();
5284
+ if (!engines) return false;
5285
+ try {
5286
+ const draft = await store.getRaw(draftId);
5287
+ if (!draft || draft.properties.status !== "open") return false;
5288
+ const members = await resolveMembers(store, hostId);
5289
+ store.setCheckedOutDraft({
5290
+ draftId,
5291
+ members,
5292
+ clones: clonesFromEntries(draft),
5293
+ // Lazy copy-on-write: the first write to an unforked member clones
5294
+ // it. Returning null declines — the write then targets the
5295
+ // original (never-fork schemas stay live by policy).
5296
+ onMissingMember: async (originalId) => {
5297
+ try {
5298
+ const entry = await forkNodeIntoDraft(store, engines.storage, draftId, originalId);
5299
+ return entry.cloneId;
5300
+ } catch {
5301
+ return null;
5302
+ }
5303
+ }
5304
+ });
5305
+ await reload();
5306
+ return true;
5307
+ } catch (err) {
5308
+ setError(err instanceof Error ? err : new Error(String(err)));
5309
+ return false;
5310
+ }
5311
+ },
5312
+ [hostId, store, getEngines, reload]
5313
+ );
5314
+ const returnToMain = useCallback5(() => {
5315
+ if (!store) return;
5316
+ store.setCheckedOutDraft(null);
5317
+ }, [store]);
5318
+ const createDraft = useCallback5(
5319
+ async (name) => {
5320
+ if (!hostId || !store) return null;
5321
+ try {
5322
+ const draft = await createDraftNode(store, { name, targetId: hostId });
5323
+ await checkout(draft.id);
5324
+ return draft;
5325
+ } catch (err) {
5326
+ setError(err instanceof Error ? err : new Error(String(err)));
5327
+ return null;
5328
+ }
5329
+ },
5330
+ [hostId, store, checkout]
5331
+ );
5332
+ const discard = useCallback5(
5333
+ async (draftId) => {
5334
+ if (!store) return false;
5335
+ const engines = getEngines();
5336
+ if (!engines) return false;
5337
+ try {
5338
+ if (store.getCheckedOutDraft()?.draftId === draftId) {
5339
+ store.setCheckedOutDraft(null);
5340
+ }
5341
+ await discardDraft(store, engines.storage, draftId);
5342
+ await reload();
5343
+ return true;
5344
+ } catch (err) {
5345
+ setError(err instanceof Error ? err : new Error(String(err)));
5346
+ return false;
5347
+ }
5348
+ },
5349
+ [store, getEngines, reload]
5350
+ );
5351
+ const merge = useCallback5(
5352
+ async (draftId) => {
5353
+ if (!store) return null;
5354
+ const engines = getEngines();
5355
+ if (!engines) return null;
5356
+ try {
5357
+ const result = await mergeDraft(store, engines.storage, engines.engine, draftId);
5358
+ if (result.status === "merged" && store.getCheckedOutDraft()?.draftId === draftId) {
5359
+ store.setCheckedOutDraft(null);
5360
+ }
5361
+ await reload();
5362
+ return result;
5363
+ } catch (err) {
5364
+ setError(err instanceof Error ? err : new Error(String(err)));
5365
+ return null;
5366
+ }
5367
+ },
5368
+ [store, getEngines, reload]
5369
+ );
5370
+ const refresh = useCallback5(
5371
+ async (draftId) => {
5372
+ if (!store) return null;
5373
+ const engines = getEngines();
5374
+ if (!engines) return null;
5375
+ try {
5376
+ const result = await refreshDraftFromMain(store, engines.storage, engines.engine, draftId);
5377
+ await reload();
5378
+ return result;
5379
+ } catch (err) {
5380
+ setError(err instanceof Error ? err : new Error(String(err)));
5381
+ return null;
5382
+ }
5383
+ },
5384
+ [store, getEngines, reload]
5385
+ );
5386
+ const setReviewRequested = useCallback5(
5387
+ async (draftId, requested) => {
5388
+ if (!store) return;
5389
+ try {
5390
+ await store.update(draftId, { properties: { reviewRequested: requested } });
5391
+ } catch (err) {
5392
+ setError(err instanceof Error ? err : new Error(String(err)));
5393
+ }
5394
+ },
5395
+ [store]
5396
+ );
5397
+ const computeReview = useCallback5(
5398
+ async (draftId) => {
5399
+ if (!store) return null;
5400
+ const engines = getEngines();
5401
+ if (!engines) return null;
5402
+ try {
5403
+ const draft = await store.getRaw(draftId);
5404
+ if (!draft) return null;
5405
+ const cards = [];
5406
+ const members = [];
5407
+ for (const [id, entry] of Object.entries(draftEntries(draft))) {
5408
+ const originalId = id;
5409
+ const cloneId = entry.cloneId;
5410
+ const [base, ours, theirs] = await Promise.all([
5411
+ engines.engine.materializeAt(originalId, {
5412
+ type: "hash",
5413
+ hash: entry.forkedAtHash
5414
+ }),
5415
+ store.getRaw(originalId),
5416
+ store.getRaw(cloneId)
5417
+ ]);
5418
+ if (!ours || !theirs) continue;
5419
+ const result = threeWayPropertyMerge(
5420
+ base.node.properties,
5421
+ ours.properties,
5422
+ theirs.properties
5423
+ );
5424
+ for (const [property, draftValue] of Object.entries(result.patch)) {
5425
+ cards.push({
5426
+ originalId,
5427
+ property,
5428
+ base: base.node.properties[property],
5429
+ main: ours.properties[property],
5430
+ draft: draftValue,
5431
+ conflict: false
5432
+ });
5433
+ }
5434
+ for (const conflict of result.conflicts) {
5435
+ cards.push({
5436
+ originalId,
5437
+ property: conflict.property,
5438
+ base: conflict.base,
5439
+ main: conflict.ours,
5440
+ draft: conflict.theirs,
5441
+ conflict: true
5442
+ });
5443
+ }
5444
+ const hasDocument = Boolean(entry.forkedAtYjsStateVector);
5445
+ let documentDiffers = false;
5446
+ if (hasDocument) {
5447
+ const [cloneBlob, mainBlob] = await Promise.all([
5448
+ engines.storage.getDocumentContent(cloneId),
5449
+ engines.storage.getDocumentContent(originalId)
5450
+ ]);
5451
+ documentDiffers = (cloneBlob?.length ?? 0) !== (mainBlob?.length ?? 0);
5452
+ }
5453
+ members.push({ originalId, cloneId, hasDocument, documentDiffers });
5454
+ }
5455
+ return { cards, members };
5456
+ } catch (err) {
5457
+ setError(err instanceof Error ? err : new Error(String(err)));
5458
+ return null;
5459
+ }
5460
+ },
5461
+ [store, getEngines]
5462
+ );
5463
+ return {
5464
+ drafts,
5465
+ checkedOut,
5466
+ createDraft,
5467
+ checkout,
5468
+ returnToMain,
5469
+ discard,
5470
+ merge,
5471
+ refresh,
5472
+ setReviewRequested,
5473
+ computeReview,
5474
+ loading,
5475
+ error,
5476
+ reload
5477
+ };
5478
+ }
5479
+
4932
5480
  // src/hooks/useGlobalUndo.ts
4933
- import { useCallback as useCallback4, useContext, useEffect as useEffect7, useReducer } from "react";
5481
+ import { useCallback as useCallback6, useContext, useEffect as useEffect9, useReducer } from "react";
4934
5482
  function readUndoContext(ctx) {
4935
5483
  if (!ctx) return { undoManager: null, nodeStore: null };
4936
5484
  return { undoManager: ctx.undoManager, nodeStore: ctx.nodeStore };
@@ -4939,17 +5487,17 @@ function useGlobalUndo() {
4939
5487
  const ctx = useContext(XNetContext);
4940
5488
  const { undoManager, nodeStore } = readUndoContext(ctx);
4941
5489
  const [, bump] = useReducer((n) => n + 1, 0);
4942
- useEffect7(() => {
5490
+ useEffect9(() => {
4943
5491
  if (!nodeStore) return;
4944
5492
  return nodeStore.subscribe(() => bump());
4945
5493
  }, [nodeStore]);
4946
- const undo = useCallback4(async () => {
5494
+ const undo = useCallback6(async () => {
4947
5495
  if (!undoManager) return false;
4948
5496
  const ok = await undoManager.undoLatest();
4949
5497
  bump();
4950
5498
  return ok;
4951
5499
  }, [undoManager]);
4952
- const redo = useCallback4(async () => {
5500
+ const redo = useCallback6(async () => {
4953
5501
  if (!undoManager) return false;
4954
5502
  const ok = await undoManager.redoLatest();
4955
5503
  bump();
@@ -4964,7 +5512,7 @@ function useGlobalUndo() {
4964
5512
  }
4965
5513
 
4966
5514
  // src/hooks/useCanCreate.ts
4967
- import { useEffect as useEffect8, useMemo as useMemo6, useRef as useRef5, useState as useState7 } from "react";
5515
+ import { useEffect as useEffect10, useMemo as useMemo7, useRef as useRef7, useState as useState9 } from "react";
4968
5516
  var INITIAL_STATE = {
4969
5517
  canCreate: false,
4970
5518
  loading: true,
@@ -4972,10 +5520,10 @@ var INITIAL_STATE = {
4972
5520
  };
4973
5521
  function useCanCreate(schemaId, properties) {
4974
5522
  const { store, isReady } = useNodeStore();
4975
- const [state, setState] = useState7(INITIAL_STATE);
4976
- const requestRef = useRef5(0);
4977
- const propertiesKey = useMemo6(() => JSON.stringify(properties ?? {}), [properties]);
4978
- useEffect8(() => {
5523
+ const [state, setState] = useState9(INITIAL_STATE);
5524
+ const requestRef = useRef7(0);
5525
+ const propertiesKey = useMemo7(() => JSON.stringify(properties ?? {}), [properties]);
5526
+ useEffect10(() => {
4979
5527
  if (!store || !isReady) {
4980
5528
  setState((prev) => ({ ...prev, loading: true }));
4981
5529
  return;
@@ -5044,7 +5592,7 @@ import {
5044
5592
  } from "@xnetjs/runtime";
5045
5593
 
5046
5594
  // src/hooks/useBilling.ts
5047
- import { useCallback as useCallback5, useContext as useContext2, useEffect as useEffect9, useMemo as useMemo7, useState as useState8 } from "react";
5595
+ import { useCallback as useCallback7, useContext as useContext2, useEffect as useEffect11, useMemo as useMemo8, useState as useState10 } from "react";
5048
5596
  var toHttpUrl = (hubUrl) => {
5049
5597
  try {
5050
5598
  const url = new URL(hubUrl);
@@ -5081,20 +5629,20 @@ function deriveBilling(state) {
5081
5629
  }
5082
5630
  function useBilling() {
5083
5631
  const context = useContext2(XNetContext);
5084
- const [state, setState] = useState8(null);
5085
- const [loading, setLoading] = useState8(
5632
+ const [state, setState] = useState10(null);
5633
+ const [loading, setLoading] = useState10(
5086
5634
  Boolean(context?.hubUrl || context?.billing?.apiBase)
5087
5635
  );
5088
- const [error, setError] = useState8(null);
5089
- const apiBase = useMemo7(() => {
5636
+ const [error, setError] = useState10(null);
5637
+ const apiBase = useMemo8(() => {
5090
5638
  if (context?.billing?.apiBase) return context.billing.apiBase.replace(/\/$/, "");
5091
5639
  return context?.hubUrl ? toHttpUrl(context.hubUrl) : null;
5092
5640
  }, [context?.billing?.apiBase, context?.hubUrl]);
5093
- const authHeaders = useCallback5(async () => {
5641
+ const authHeaders = useCallback7(async () => {
5094
5642
  const token = context?.getHubAuthToken ? await context.getHubAuthToken() : "";
5095
5643
  return token ? { Authorization: `Bearer ${token}` } : {};
5096
5644
  }, [context]);
5097
- const reload = useCallback5(async () => {
5645
+ const reload = useCallback7(async () => {
5098
5646
  if (!apiBase) {
5099
5647
  setState(null);
5100
5648
  setLoading(false);
@@ -5112,10 +5660,10 @@ function useBilling() {
5112
5660
  setLoading(false);
5113
5661
  }
5114
5662
  }, [apiBase, authHeaders]);
5115
- useEffect9(() => {
5663
+ useEffect11(() => {
5116
5664
  void reload();
5117
5665
  }, [reload]);
5118
- const redirectVia = useCallback5(
5666
+ const redirectVia = useCallback7(
5119
5667
  async (path, body) => {
5120
5668
  if (!apiBase) throw new Error("Hub URL not configured");
5121
5669
  const res = await fetch(`${apiBase}${path}`, {
@@ -5129,11 +5677,11 @@ function useBilling() {
5129
5677
  },
5130
5678
  [apiBase, authHeaders]
5131
5679
  );
5132
- const openCheckout = useCallback5(
5680
+ const openCheckout = useCallback7(
5133
5681
  (priceRef, options = {}) => redirectVia("/billing/checkout", buildCheckoutBody(priceRef, options)),
5134
5682
  [redirectVia]
5135
5683
  );
5136
- const openPortal = useCallback5(
5684
+ const openPortal = useCallback7(
5137
5685
  (returnUrl) => redirectVia("/billing/portal", returnUrl ? { returnUrl } : {}),
5138
5686
  [redirectVia]
5139
5687
  );
@@ -5239,6 +5787,7 @@ export {
5239
5787
  isReactionVisible,
5240
5788
  isSavedViewVisualPreviewEmbeddable,
5241
5789
  isTaskOverdue,
5790
+ mergeEditorContributions,
5242
5791
  mergeSavedViewFeedEnrichment,
5243
5792
  moderateThread,
5244
5793
  onboardingReducer,
@@ -5274,6 +5823,7 @@ export {
5274
5823
  useDatabaseSchema,
5275
5824
  useDemoMode,
5276
5825
  useDiff,
5826
+ useDraft,
5277
5827
  useEditorExtensions,
5278
5828
  useEditorExtensionsSafe,
5279
5829
  useEffectiveSchema,
@@ -5290,6 +5840,7 @@ export {
5290
5840
  useInfiniteQuery,
5291
5841
  useInstrumentation,
5292
5842
  useIsOffline,
5843
+ useMergedEditorContributions,
5293
5844
  useMessageRequests,
5294
5845
  useModeratedThread,
5295
5846
  useMutate,
@@ -5317,6 +5868,7 @@ export {
5317
5868
  useTaskProjectionSync,
5318
5869
  useTasks,
5319
5870
  useTelemetryReporter,
5871
+ useTimeMachine,
5320
5872
  useTracingReporter,
5321
5873
  useUndo,
5322
5874
  useVerification,