@templatical/core 0.48.0 → 0.50.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
@@ -1,6 +1,6 @@
1
- import { SdkError, assertNoSlotInContent, assertNoWrapperInContent, cloneBlock, createBlock, createDefaultTemplateContent, generateId, safeClone } from "@templatical/types";
2
- import { computed, reactive, readonly, ref, watch } from "@vue/reactivity";
3
- import { computed as computed$1, onScopeDispose, ref as ref$1, watch as watch$1 } from "vue";
1
+ import { OperationError, SdkError, applyOperations, applyOperationsInPlace, assertNoSlotInContent, assertNoWrapperInContent, cloneBlock, createBlock, createDefaultTemplateContent, deepEqual, generateId, getColumnCount, invertOperations, invertOperations as invertOperations$1, safeClone } from "@templatical/types";
2
+ import { computed, getCurrentScope, onScopeDispose, reactive, readonly, ref, shallowRef, watch } from "@vue/reactivity";
3
+ import { computed as computed$1, onScopeDispose as onScopeDispose$1, ref as ref$1, watch as watch$1 } from "vue";
4
4
  //#region src/error-reporting.ts
5
5
  /**
6
6
  * Turn a thrown value into an `Error`.
@@ -32,11 +32,6 @@ function notifyHandler(onError, run) {
32
32
  }
33
33
  //#endregion
34
34
  //#region src/editor.ts
35
- function getColumnCount(layout) {
36
- if (layout === "1") return 1;
37
- if (layout === "3") return 3;
38
- return 2;
39
- }
40
35
  function useEditor(options) {
41
36
  const seedContent = options.content ?? createDefaultTemplateContent(options.defaultFontFamily, options.templateDefaults);
42
37
  assertNoSlotInContent(seedContent);
@@ -53,6 +48,8 @@ function useEditor(options) {
53
48
  isLoading: false,
54
49
  uiTheme: "auto"
55
50
  });
51
+ const loadVersion = ref(0);
52
+ let loadingRequest = 0;
56
53
  /**
57
54
  * Bumped by every content mutation. `save()` captures it before awaiting the
58
55
  * provider and only clears `isDirty` if it is unchanged afterwards, so an edit
@@ -257,6 +254,7 @@ function useEditor(options) {
257
254
  const { create: providerCreate } = provider;
258
255
  if (typeof providerCreate !== "function") refuse("create()", "is disabled by the provider — its `create` is `false`.");
259
256
  let created;
257
+ const request = ++loadingRequest;
260
258
  state.isLoading = true;
261
259
  try {
262
260
  if (input?.content) state.content = input.content;
@@ -267,31 +265,35 @@ function useEditor(options) {
267
265
  } : { content: state.content });
268
266
  state.template = created;
269
267
  if (revision === revisionAtRequest) state.isDirty = false;
268
+ loadVersion.value++;
270
269
  } catch (error) {
271
270
  options.onError?.(error);
272
271
  throw error;
273
272
  } finally {
274
- state.isLoading = false;
273
+ if (loadingRequest === request) state.isLoading = false;
275
274
  }
276
275
  notify(() => provider.onCreated?.(created));
277
276
  return created;
278
277
  }
279
- async function load(templateId) {
278
+ async function load(templateId, shouldApply) {
280
279
  const provider = requireProvider("load()");
281
280
  let loaded;
281
+ const request = ++loadingRequest;
282
282
  state.isLoading = true;
283
283
  try {
284
284
  loaded = await provider.load(templateId);
285
285
  assertNoSlotInContent(loaded.content);
286
286
  assertNoWrapperInContent(loaded.content);
287
+ if (shouldApply && !shouldApply()) return loaded;
287
288
  state.template = loaded;
288
289
  state.content = loaded.content;
289
290
  state.isDirty = false;
291
+ loadVersion.value++;
290
292
  } catch (error) {
291
293
  options.onError?.(error);
292
294
  throw error;
293
295
  } finally {
294
- state.isLoading = false;
296
+ if (loadingRequest === request) state.isLoading = false;
295
297
  }
296
298
  notify(() => provider.onLoaded?.(loaded));
297
299
  return loaded;
@@ -333,6 +335,7 @@ function useEditor(options) {
333
335
  }
334
336
  return {
335
337
  state: readonly(state),
338
+ loadVersion: readonly(loadVersion),
336
339
  content,
337
340
  selectedBlock,
338
341
  isBlockLocked,
@@ -362,7 +365,7 @@ const MAX_STACK_SIZE = 50;
362
365
  const DEBOUNCE_MS = 300;
363
366
  const NAVIGATE_IDLE_MS = 1500;
364
367
  function useHistory(options) {
365
- const { content, setContent, isRemoteOperation, maxSize = MAX_STACK_SIZE } = options;
368
+ const { content, setContent, maxSize = MAX_STACK_SIZE } = options;
366
369
  const undoStack = ref([]);
367
370
  const redoStack = ref([]);
368
371
  const isNavigating = ref(false);
@@ -384,13 +387,11 @@ function useHistory(options) {
384
387
  }
385
388
  }
386
389
  function record() {
387
- if (isRemoteOperation?.()) return;
388
390
  flushPendingDebounce();
389
391
  pushToUndoStack(cloneContent());
390
392
  redoStack.value = [];
391
393
  }
392
394
  function recordDebounced(blockId) {
393
- if (isRemoteOperation?.()) return;
394
395
  if (pendingDebounce && pendingDebounce.blockId === blockId) {
395
396
  clearTimeout(pendingDebounce.timeoutId);
396
397
  pendingDebounce.timeoutId = setTimeout(() => {
@@ -622,8 +623,8 @@ function useDataSourceFetch(options) {
622
623
  * Wraps editor mutation methods to record history snapshots before each
623
624
  * operation. Mutates the editor object in place.
624
625
  *
625
- * Must be applied **after** any collaboration broadcast wrapping so the
626
- * call chain is: history.record() → broadcast → original mutation.
626
+ * Not installed with a collaboration provider: its controller captures
627
+ * changes by diffing the content and keeps its own undo history.
627
628
  */
628
629
  function useHistoryInterceptor(editor, history) {
629
630
  const originalAddBlock = editor.addBlock;
@@ -658,6 +659,1139 @@ function useHistoryInterceptor(editor, history) {
658
659
  };
659
660
  }
660
661
  //#endregion
662
+ //#region src/diff-content.ts
663
+ const ROOT_KEY = "root";
664
+ const BLOCK_SKIP = /* @__PURE__ */ new Set([
665
+ "id",
666
+ "type",
667
+ "children"
668
+ ]);
669
+ const RESIZED_SECTION_SKIP = /* @__PURE__ */ new Set([
670
+ "id",
671
+ "type",
672
+ "children",
673
+ "columns"
674
+ ]);
675
+ const NO_SKIP = /* @__PURE__ */ new Set();
676
+ function containerKey(parent) {
677
+ return parent === null ? ROOT_KEY : JSON.stringify([parent.sectionId, parent.column]);
678
+ }
679
+ function columnsOf(block) {
680
+ const children = block.children;
681
+ return block.type === "section" && Array.isArray(children) ? children : [];
682
+ }
683
+ /** Every block by id, with its container. The first occurrence of an id wins. */
684
+ function indexBlocks(blocks) {
685
+ const placements = /* @__PURE__ */ new Map();
686
+ const walk = (list, parent) => {
687
+ list.forEach((block, index) => {
688
+ if (!placements.has(block.id)) placements.set(block.id, {
689
+ block,
690
+ key: containerKey(parent),
691
+ index
692
+ });
693
+ columnsOf(block).forEach((column, columnIndex) => {
694
+ if (Array.isArray(column)) walk(column, {
695
+ sectionId: block.id,
696
+ column: columnIndex
697
+ });
698
+ });
699
+ });
700
+ };
701
+ walk(blocks, null);
702
+ return placements;
703
+ }
704
+ /**
705
+ * The positions in `values` of one longest strictly increasing run: the blocks
706
+ * that can stay where they are while the others move around them.
707
+ */
708
+ function longestIncreasingRun(values) {
709
+ const tails = [];
710
+ const previous = new Array(values.length).fill(-1);
711
+ values.forEach((value, position) => {
712
+ let low = 0;
713
+ let high = tails.length;
714
+ while (low < high) {
715
+ const middle = low + high >> 1;
716
+ if (values[tails[middle]] < value) low = middle + 1;
717
+ else high = middle;
718
+ }
719
+ if (low > 0) previous[position] = tails[low - 1];
720
+ tails[low] = position;
721
+ });
722
+ const run = /* @__PURE__ */ new Set();
723
+ let position = tails.length > 0 ? tails[tails.length - 1] : -1;
724
+ while (position !== -1) {
725
+ run.add(position);
726
+ position = previous[position];
727
+ }
728
+ return run;
729
+ }
730
+ /** Keys whose values differ, as `set` and `unset`. `undefined` counts as absent. */
731
+ function keyChanges(before, after, skip) {
732
+ const set = {};
733
+ const unset = [];
734
+ for (const key of Object.keys(after)) {
735
+ if (skip.has(key) || after[key] === void 0) continue;
736
+ if (!deepEqual(before[key], after[key])) set[key] = safeClone(after[key]);
737
+ }
738
+ for (const key of Object.keys(before)) {
739
+ if (skip.has(key) || before[key] === void 0) continue;
740
+ if (after[key] === void 0) unset.push(key);
741
+ }
742
+ return {
743
+ set,
744
+ unset
745
+ };
746
+ }
747
+ /**
748
+ * The operations that turn `before` into `after`. Blocks are matched by id, so
749
+ * a drag comes out as a `moveBlock` and a layout change as a `columns` change
750
+ * plus moves, never as deletions and re-additions.
751
+ *
752
+ * `applyOperations(before, diffContent(before, after))` deep-equals `after`
753
+ * when both are well formed: unique ids, sections only at the top level, and
754
+ * each section's `children` as long as its layout's column count. The order is
755
+ * what the engine needs:
756
+ *
757
+ * 1. Deletions that take nothing still needed with them.
758
+ * 2. The top level: additions (a new section carries its new children) and moves.
759
+ * 3. `columns` growth, so new columns exist before blocks move into them.
760
+ * 4. Every section column: additions and moves, out of shrinking columns too.
761
+ * 5. `columns` shrinkage, once the dropped columns are empty.
762
+ * 6. Deletion of sections whose surviving blocks have moved out.
763
+ * 7. Key changes on blocks, then on settings.
764
+ *
765
+ * No operation carries `expect`: an ordinary edit applies over anything that
766
+ * doesn't conflict with it, and locks keep two people off one block.
767
+ */
768
+ function diffContent(before, after) {
769
+ const was = indexBlocks(before.blocks);
770
+ const now = indexBlocks(after.blocks);
771
+ const ops = [];
772
+ const typeChanged = (id) => {
773
+ const next = now.get(id);
774
+ const previous = was.get(id);
775
+ return next !== void 0 && previous !== void 0 && next.block.type !== previous.block.type;
776
+ };
777
+ const survives = (block) => now.has(block.id) && !typeChanged(block.id);
778
+ const hasSurvivor = (block) => columnsOf(block).some((column) => Array.isArray(column) && column.some((child) => survives(child) || !typeChanged(child.id) && hasSurvivor(child)));
779
+ const gone = /* @__PURE__ */ new Set();
780
+ const deferred = [];
781
+ const visit = (block, fate) => {
782
+ let childFate = "kept";
783
+ if (fate === "dropped") {
784
+ gone.add(block.id);
785
+ childFate = "dropped";
786
+ } else if (typeChanged(block.id)) {
787
+ ops.push({
788
+ op: "deleteBlock",
789
+ blockId: block.id
790
+ });
791
+ gone.add(block.id);
792
+ childFate = "dropped";
793
+ } else if (!now.has(block.id)) {
794
+ if (fate === "deferred") childFate = "deferred";
795
+ else if (hasSurvivor(block)) {
796
+ deferred.push(block.id);
797
+ childFate = "deferred";
798
+ } else {
799
+ ops.push({
800
+ op: "deleteBlock",
801
+ blockId: block.id
802
+ });
803
+ gone.add(block.id);
804
+ childFate = "dropped";
805
+ }
806
+ }
807
+ for (const column of columnsOf(block)) if (Array.isArray(column)) for (const child of column) visit(child, childFate);
808
+ };
809
+ for (const block of before.blocks) visit(block, "kept");
810
+ const exists = (id) => was.has(id) && !gone.has(id);
811
+ const payloadOf = (block) => {
812
+ const payload = safeClone(block);
813
+ if (payload.type === "section") payload.children = columnsOf(block).map((column) => Array.isArray(column) ? column.filter((child) => !exists(child.id)).map((child) => safeClone(child)) : column);
814
+ return payload;
815
+ };
816
+ const place = (parent, finalList, arrivedWithParent) => {
817
+ const key = containerKey(parent);
818
+ const indexes = [];
819
+ const positions = [];
820
+ finalList.forEach((block, position) => {
821
+ const previous = was.get(block.id);
822
+ if (exists(block.id) && previous !== void 0 && previous.key === key) {
823
+ indexes.push(previous.index);
824
+ positions.push(position);
825
+ }
826
+ });
827
+ const staying = new Set([...longestIncreasingRun(indexes)].map((run) => positions[run]));
828
+ finalList.forEach((block, position) => {
829
+ if (staying.has(position) || arrivedWithParent(block)) return;
830
+ const afterId = position > 0 ? finalList[position - 1].id : null;
831
+ ops.push(exists(block.id) ? {
832
+ op: "moveBlock",
833
+ blockId: block.id,
834
+ parent,
835
+ after: afterId
836
+ } : {
837
+ op: "addBlock",
838
+ block: payloadOf(block),
839
+ parent,
840
+ after: afterId
841
+ });
842
+ });
843
+ };
844
+ place(null, after.blocks, () => false);
845
+ const sections = after.blocks.filter((block) => block.type === "section");
846
+ const resized = /* @__PURE__ */ new Set();
847
+ const resize = (grow) => {
848
+ for (const section of sections) {
849
+ if (!exists(section.id)) continue;
850
+ const fromCount = columnsOf(was.get(section.id).block).length;
851
+ const toCount = columnsOf(section).length;
852
+ if (grow ? toCount > fromCount : toCount < fromCount) {
853
+ ops.push({
854
+ op: "updateBlock",
855
+ blockId: section.id,
856
+ set: { columns: section.columns }
857
+ });
858
+ resized.add(section.id);
859
+ }
860
+ }
861
+ };
862
+ resize(true);
863
+ for (const section of sections) {
864
+ const isNew = !exists(section.id);
865
+ columnsOf(section).forEach((column, columnIndex) => {
866
+ if (!Array.isArray(column)) return;
867
+ place({
868
+ sectionId: section.id,
869
+ column: columnIndex
870
+ }, column, (child) => isNew && !exists(child.id));
871
+ });
872
+ }
873
+ resize(false);
874
+ for (const id of deferred) ops.push({
875
+ op: "deleteBlock",
876
+ blockId: id
877
+ });
878
+ for (const [id, placement] of now) {
879
+ if (!exists(id)) continue;
880
+ const { set, unset } = keyChanges(was.get(id).block, placement.block, resized.has(id) ? RESIZED_SECTION_SKIP : BLOCK_SKIP);
881
+ if (Object.keys(set).length === 0 && unset.length === 0) continue;
882
+ ops.push({
883
+ op: "updateBlock",
884
+ blockId: id,
885
+ ...Object.keys(set).length > 0 ? { set } : {},
886
+ ...unset.length > 0 ? { unset } : {}
887
+ });
888
+ }
889
+ const settings = keyChanges(before.settings, after.settings, NO_SKIP);
890
+ if (Object.keys(settings.set).length > 0 || settings.unset.length > 0) ops.push({
891
+ op: "updateSettings",
892
+ ...Object.keys(settings.set).length > 0 ? { set: settings.set } : {},
893
+ ...settings.unset.length > 0 ? { unset: settings.unset } : {}
894
+ });
895
+ return ops;
896
+ }
897
+ /** A revision that hasn't arrived after this long is fetched. */
898
+ const COLLABORATION_GAP_TIMEOUT_MS = 1e3;
899
+ /** A held lock is renewed this often; the backend lets it lapse after 30 s. */
900
+ const COLLABORATION_LOCK_RENEW_MS = 1e4;
901
+ /** Waits between attempts at a batch that failed in transit. */
902
+ const RETRY_DELAYS_MS = [
903
+ 1e3,
904
+ 2e3,
905
+ 5e3,
906
+ 1e4,
907
+ 3e4
908
+ ];
909
+ /** The snapshot history's depth, kept for the operation history too. */
910
+ const DEFAULT_MAX_UNDO = 50;
911
+ /** OperationBatch allows 1–100 operations. */
912
+ const MAX_BATCH_OPS = 100;
913
+ const REFUSAL_CODES = /* @__PURE__ */ new Set([
914
+ "block_exists",
915
+ "block_missing",
916
+ "anchor_missing",
917
+ "precondition_failed",
918
+ "lock_held",
919
+ "locked_by_others",
920
+ "invalid_operation",
921
+ "lock_lost",
922
+ "content_too_large"
923
+ ]);
924
+ function refusalCode(error) {
925
+ const code = error?.code;
926
+ return typeof code === "string" && REFUSAL_CODES.has(code) ? code : null;
927
+ }
928
+ function refusalHolder(error) {
929
+ const holder = error?.holder;
930
+ return typeof holder === "object" && holder !== null && typeof holder.sessionId === "string" ? holder : void 0;
931
+ }
932
+ /** Network failures, 5xx, 408 and 429 are worth retrying; other 4xx aren't. */
933
+ function isTransient(error) {
934
+ const status = error?.statusCode;
935
+ if (typeof status !== "number") return true;
936
+ return status >= 500 || status === 408 || status === 429;
937
+ }
938
+ function containsBlock(blocks, blockId) {
939
+ return blocks.some((block) => {
940
+ if (block.id === blockId) return true;
941
+ const children = block.children;
942
+ return block.type === "section" && Array.isArray(children) && children.some((column) => Array.isArray(column) && containsBlock(column, blockId));
943
+ });
944
+ }
945
+ /** Typing into one block, or one settings change: worth batching up. */
946
+ function isCoalescible(ops) {
947
+ if (ops.length === 1 && ops[0].op === "updateSettings") return true;
948
+ const first = ops[0];
949
+ return first.op === "updateBlock" && ops.every((op) => op.op === "updateBlock" && op.blockId === first.blockId);
950
+ }
951
+ /**
952
+ * A remote batch without its preconditions. The backend checked them against
953
+ * its own copy before it accepted the batch. This copy can differ from that one
954
+ * in merge-tag markup alone — tokens are normalized into tags on load and on
955
+ * arrival — and an `expect` compares content exactly, so checking it here
956
+ * refuses a batch the backend applied and reloads the template. Structural
957
+ * checks still run, and still catch a copy that really drifted.
958
+ */
959
+ function withoutPreconditions(ops) {
960
+ return ops.map((op) => {
961
+ if (!("expect" in op)) return op;
962
+ const { expect: _expect, ...rest } = op;
963
+ return rest;
964
+ });
965
+ }
966
+ /**
967
+ * Real-time collaboration over a {@link CollaborationProvider}.
968
+ *
969
+ * The controller keeps the content the backend confirmed at revision R, plus
970
+ * this session's pending batches; the canvas shows the confirmed content with
971
+ * the pending batches applied. Local changes are captured by diffing the
972
+ * canvas against that view: structural changes, selection changes and blur
973
+ * send at once, typing into one block after a short pause. One batch is in
974
+ * flight at a time. Batches from other sessions apply strictly by revision,
975
+ * with this session's pending batches replayed on top; a gap that isn't
976
+ * filled within a second, a re-sync, a reconnect and a refusal all fetch what
977
+ * was missed.
978
+ *
979
+ * Selecting a block takes its lock (when the backend has locks), renewed
980
+ * while selected and released on deselect, template switch and page hide.
981
+ * Undo and redo send inverse batches guarded by preconditions; one that no
982
+ * longer applies is dropped with a notice. Changes from others never enter the
983
+ * stacks.
984
+ */
985
+ function useCollaboration(options) {
986
+ const { provider } = options;
987
+ const sessionId = options.sessionId ?? crypto.randomUUID();
988
+ const maxUndo = options.maxUndo ?? DEFAULT_MAX_UNDO;
989
+ const locking = typeof provider.acquireLock === "function" && typeof provider.renewLock === "function" && typeof provider.releaseLock === "function";
990
+ const members = shallowRef([]);
991
+ const locks = shallowRef(/* @__PURE__ */ new Map());
992
+ const connection = ref(null);
993
+ const revision = ref(0);
994
+ const historyVersion = ref(0);
995
+ let editor = null;
996
+ let active = null;
997
+ let suspended = false;
998
+ let writing = false;
999
+ let captureQueued = false;
1000
+ let typingTimer = null;
1001
+ let typingSince = 0;
1002
+ let destroyed = false;
1003
+ /** The watchers `attach` set up; `destroy` stops them, scope or not. */
1004
+ const stopWatching = [];
1005
+ let wanted = null;
1006
+ let held = null;
1007
+ let lockQueue = Promise.resolve();
1008
+ let renewTimer = null;
1009
+ const canUndo = computed(() => {
1010
+ historyVersion.value;
1011
+ return (active?.undo.length ?? 0) > 0;
1012
+ });
1013
+ const canRedo = computed(() => {
1014
+ historyVersion.value;
1015
+ return (active?.redo.length ?? 0) > 0;
1016
+ });
1017
+ function reportError(error) {
1018
+ options.onError?.(error instanceof Error ? error : new Error(String(error)));
1019
+ }
1020
+ function notify(notice, session = active) {
1021
+ if (session !== active || destroyed) return;
1022
+ options.onNotice?.(notice);
1023
+ }
1024
+ function lockContext() {
1025
+ return {
1026
+ locks: [...locks.value.values()].map((lock) => ({
1027
+ blockId: lock.blockId,
1028
+ sessionId: lock.holder.sessionId
1029
+ })),
1030
+ session: sessionId
1031
+ };
1032
+ }
1033
+ function bumpHistory() {
1034
+ historyVersion.value++;
1035
+ }
1036
+ function pushCapped(stack, entry) {
1037
+ stack.push(entry);
1038
+ if (stack.length > maxUndo) stack.splice(0, stack.length - maxUndo);
1039
+ }
1040
+ /** Put `view` on the canvas, changing only what differs from it. */
1041
+ function renderView(view) {
1042
+ if (!editor || suspended) return;
1043
+ writing = true;
1044
+ try {
1045
+ const live = editor.content.value;
1046
+ const ops = diffContent(live, view);
1047
+ if (ops.length > 0) try {
1048
+ applyOperationsInPlace(live, ops);
1049
+ } catch {
1050
+ editor.setContent(safeClone(view), false);
1051
+ }
1052
+ if (!deepEqual(editor.content.value, view)) editor.setContent(safeClone(view), false);
1053
+ } finally {
1054
+ writing = false;
1055
+ }
1056
+ const selected = editor.state.selectedBlockId;
1057
+ if (selected && !containsBlock(view.blocks, selected)) editor.selectBlock(null);
1058
+ }
1059
+ function scheduleCapture() {
1060
+ if (writing || captureQueued) return;
1061
+ captureQueued = true;
1062
+ queueMicrotask(() => {
1063
+ captureQueued = false;
1064
+ capture(false);
1065
+ });
1066
+ }
1067
+ function clearTypingTimer() {
1068
+ if (typingTimer) {
1069
+ clearTimeout(typingTimer);
1070
+ typingTimer = null;
1071
+ }
1072
+ }
1073
+ /**
1074
+ * Diff the canvas against the view and send what changed. `duringLoad` is
1075
+ * for the moment a load or create starts: the canvas still shows this
1076
+ * template, and its unsent typing must go before the content is replaced.
1077
+ */
1078
+ function capture(force, duringLoad = false) {
1079
+ const session = active;
1080
+ if (!editor || !session || suspended || writing) return;
1081
+ if (editor.state.isLoading && !duringLoad) return;
1082
+ if (editor.state.template?.id !== session.templateId) return;
1083
+ const ops = diffContent(session.view, editor.content.value);
1084
+ if (ops.length === 0) {
1085
+ clearTypingTimer();
1086
+ return;
1087
+ }
1088
+ if (!force && isCoalescible(ops)) {
1089
+ const now = Date.now();
1090
+ if (typingTimer === null) typingSince = now;
1091
+ if (now - typingSince < 1e3) {
1092
+ if (typingTimer) clearTimeout(typingTimer);
1093
+ typingTimer = setTimeout(() => {
1094
+ typingTimer = null;
1095
+ capture(true);
1096
+ }, 300);
1097
+ return;
1098
+ }
1099
+ }
1100
+ clearTypingTimer();
1101
+ commit(session, ops);
1102
+ }
1103
+ function flush() {
1104
+ capture(true);
1105
+ }
1106
+ function commit(session, proposed) {
1107
+ let ops = proposed;
1108
+ let next;
1109
+ try {
1110
+ next = applyOperations(session.view, ops, lockContext());
1111
+ } catch (error) {
1112
+ if (!(error instanceof OperationError)) throw error;
1113
+ renderView(session.view);
1114
+ const holder = error.blockId === void 0 ? void 0 : locks.value.get(error.blockId)?.holder;
1115
+ notify({
1116
+ type: "editRefused",
1117
+ code: error.code,
1118
+ ...holder ? { holder } : {}
1119
+ });
1120
+ if (error.code === "invalid_operation") reportError(error);
1121
+ return;
1122
+ }
1123
+ const live = editor.content.value;
1124
+ if (!deepEqual(next, live)) {
1125
+ reportError(/* @__PURE__ */ new Error("[Templatical] Collaboration: a change couldn't be expressed as block operations, so the whole content was sent."));
1126
+ ops = [{
1127
+ op: "setContent",
1128
+ content: safeClone(live)
1129
+ }];
1130
+ try {
1131
+ next = applyOperations(session.view, ops, lockContext());
1132
+ } catch (error) {
1133
+ if (!(error instanceof OperationError)) throw error;
1134
+ renderView(session.view);
1135
+ notify({
1136
+ type: "editRefused",
1137
+ code: error.code
1138
+ });
1139
+ return;
1140
+ }
1141
+ }
1142
+ const entry = { ops: invertOperations$1(session.view, ops) };
1143
+ pushCapped(session.undo, entry);
1144
+ session.redo = [];
1145
+ bumpHistory();
1146
+ session.view = next;
1147
+ enqueue(session, ops, "edit", entry);
1148
+ }
1149
+ function enqueue(session, ops, origin, entry) {
1150
+ const group = { dropped: false };
1151
+ let offset = 0;
1152
+ do {
1153
+ session.pending.push({
1154
+ batchId: crypto.randomUUID(),
1155
+ ops: ops.slice(offset, offset + MAX_BATCH_OPS),
1156
+ origin,
1157
+ entry,
1158
+ group,
1159
+ sent: false,
1160
+ revision: null
1161
+ });
1162
+ offset += MAX_BATCH_OPS;
1163
+ } while (offset < ops.length);
1164
+ pump(session);
1165
+ }
1166
+ function pump(session) {
1167
+ if (session.inFlight) return;
1168
+ const next = session.pending.find((batch) => !batch.sent);
1169
+ if (!next) {
1170
+ settleWaiters(session);
1171
+ return;
1172
+ }
1173
+ next.sent = true;
1174
+ session.inFlight = next;
1175
+ send(session, next, 0);
1176
+ }
1177
+ function send(session, batch, attempt) {
1178
+ provider.submit(session.templateId, {
1179
+ batchId: batch.batchId,
1180
+ baseRevision: session.revision,
1181
+ ops: batch.ops,
1182
+ ...batch.origin === "ai" ? { origin: "ai" } : {}
1183
+ }).then(({ revision: confirmedAt }) => {
1184
+ session.inFlight = null;
1185
+ confirmOwn(session, batch, confirmedAt);
1186
+ pump(session);
1187
+ }, (error) => {
1188
+ if (refusalCode(error) !== null) {
1189
+ session.inFlight = null;
1190
+ refuse(session, batch, error);
1191
+ pump(session);
1192
+ return;
1193
+ }
1194
+ if (isTransient(error)) {
1195
+ rejectWaiters(session, error);
1196
+ const delay = RETRY_DELAYS_MS[Math.min(attempt, RETRY_DELAYS_MS.length - 1)];
1197
+ setTimeout(() => {
1198
+ if (!destroyed) send(session, batch, attempt + 1);
1199
+ }, delay);
1200
+ return;
1201
+ }
1202
+ session.inFlight = null;
1203
+ reportError(error);
1204
+ refuse(session, batch, error);
1205
+ pump(session);
1206
+ });
1207
+ }
1208
+ function confirmOwn(session, batch, at) {
1209
+ if (batch.revision !== null) return;
1210
+ batch.revision = at;
1211
+ if (at <= session.revision) {
1212
+ dropPending(session, batch);
1213
+ rebuild(session);
1214
+ return;
1215
+ }
1216
+ session.own.set(at, batch);
1217
+ drain(session);
1218
+ }
1219
+ function dropPending(session, batch) {
1220
+ session.pending = session.pending.filter((pending) => pending !== batch);
1221
+ settleWaiters(session);
1222
+ }
1223
+ function takeBackEntry(session, batch) {
1224
+ if (!batch.entry) return;
1225
+ for (const stack of [session.undo, session.redo]) {
1226
+ const position = stack.indexOf(batch.entry);
1227
+ if (position !== -1) stack.splice(position, 1);
1228
+ }
1229
+ bumpHistory();
1230
+ }
1231
+ function dropRefusedGroup(session, batch) {
1232
+ const firstRefusal = !batch.group.dropped;
1233
+ batch.group.dropped = true;
1234
+ session.pending = session.pending.filter((pending) => pending.group !== batch.group || pending.sent);
1235
+ if (firstRefusal) takeBackEntry(session, batch);
1236
+ settleWaiters(session);
1237
+ return firstRefusal;
1238
+ }
1239
+ function refuse(session, batch, error) {
1240
+ dropPending(session, batch);
1241
+ if (dropRefusedGroup(session, batch)) {
1242
+ const code = refusalCode(error) ?? "invalid_operation";
1243
+ if (batch.origin === "edit" || batch.origin === "ai") {
1244
+ const holder = refusalHolder(error);
1245
+ notify({
1246
+ type: "editRefused",
1247
+ code,
1248
+ ...holder ? { holder } : {}
1249
+ }, session);
1250
+ } else notify({
1251
+ type: "undoSkipped",
1252
+ code
1253
+ }, session);
1254
+ }
1255
+ rebuild(session);
1256
+ if (session === active) catchUp();
1257
+ }
1258
+ function settleWaiters(session) {
1259
+ if (session.pending.length > 0 || session.waiters.length === 0) return;
1260
+ for (const waiter of session.waiters.splice(0)) waiter.resolve();
1261
+ }
1262
+ function rejectWaiters(session, error) {
1263
+ const reason = error instanceof Error ? error : new Error(String(error));
1264
+ for (const waiter of session.waiters.splice(0)) waiter.reject(reason);
1265
+ }
1266
+ function receive(session, batch) {
1267
+ if (batch.revision <= session.revision) return;
1268
+ const mine = session.pending.find((pending) => pending.batchId === batch.batchId);
1269
+ if (mine) {
1270
+ confirmOwn(session, mine, batch.revision);
1271
+ return;
1272
+ }
1273
+ session.remote.set(batch.revision, batch);
1274
+ drain(session);
1275
+ }
1276
+ /**
1277
+ * The ops another session's batch applies here: rewritten by
1278
+ * `normalizeRemoteOperations`, then without their preconditions. A
1279
+ * normalizer that throws is reported, and the batch applies as it came.
1280
+ */
1281
+ function arriving(session, batch) {
1282
+ let ops = batch.ops;
1283
+ if (options.normalizeRemoteOperations) try {
1284
+ ops = options.normalizeRemoteOperations(batch.ops, session.confirmed);
1285
+ } catch (error) {
1286
+ reportError(error);
1287
+ }
1288
+ return withoutPreconditions(ops);
1289
+ }
1290
+ /** Apply whatever is next in revision order, own and remote alike. */
1291
+ function drain(session) {
1292
+ const applied = [];
1293
+ let confirmedDroppedChunk = false;
1294
+ for (;;) {
1295
+ const at = session.revision + 1;
1296
+ const remote = session.remote.get(at);
1297
+ const own = session.own.get(at);
1298
+ if (!remote && !own) break;
1299
+ try {
1300
+ session.confirmed = applyOperations(session.confirmed, remote ? arriving(session, remote) : own.ops);
1301
+ } catch {
1302
+ if (session.recovering) {
1303
+ session.remote.clear();
1304
+ session.own.clear();
1305
+ reportError(/* @__PURE__ */ new Error("[Templatical] Collaboration: this editor can't follow the template's changes. Reload the page."));
1306
+ return;
1307
+ }
1308
+ reload(session);
1309
+ return;
1310
+ }
1311
+ session.revision = at;
1312
+ if (remote) {
1313
+ session.remote.delete(at);
1314
+ applied.push(remote);
1315
+ } else {
1316
+ session.own.delete(at);
1317
+ if (own.group.dropped) confirmedDroppedChunk = true;
1318
+ dropPending(session, own);
1319
+ }
1320
+ }
1321
+ if (session === active) revision.value = session.revision;
1322
+ if (session.remote.size > 0 || session.own.size > 0) armGapTimer(session);
1323
+ else clearGapTimer(session);
1324
+ if (applied.length === 0 && !confirmedDroppedChunk) return;
1325
+ rebuild(session);
1326
+ if (session !== active) return;
1327
+ for (const batch of applied) try {
1328
+ options.onRemoteChange?.({
1329
+ actor: batch.actor,
1330
+ ops: batch.ops
1331
+ });
1332
+ } catch (error) {
1333
+ reportError(error);
1334
+ }
1335
+ }
1336
+ /** Recompute the view from `confirmed`, replaying what is still pending. */
1337
+ function rebuild(session) {
1338
+ if (session === active) flush();
1339
+ let view = session.confirmed;
1340
+ for (let index = 0; index < session.pending.length; index++) {
1341
+ const batch = session.pending[index];
1342
+ try {
1343
+ view = applyOperations(view, batch.ops);
1344
+ } catch (error) {
1345
+ if (batch.sent) continue;
1346
+ if (dropRefusedGroup(session, batch)) {
1347
+ const code = error instanceof OperationError ? error.code : "invalid_operation";
1348
+ notify(batch.origin === "edit" || batch.origin === "ai" ? {
1349
+ type: "editRefused",
1350
+ code
1351
+ } : {
1352
+ type: "undoSkipped",
1353
+ code
1354
+ }, session);
1355
+ }
1356
+ view = session.confirmed;
1357
+ index = -1;
1358
+ }
1359
+ }
1360
+ session.view = view;
1361
+ settleWaiters(session);
1362
+ if (session === active) renderView(view);
1363
+ }
1364
+ function armGapTimer(session) {
1365
+ if (session.gapTimer) return;
1366
+ session.gapTimer = setTimeout(() => {
1367
+ session.gapTimer = null;
1368
+ if (session === active) catchUp();
1369
+ }, COLLABORATION_GAP_TIMEOUT_MS);
1370
+ }
1371
+ function clearGapTimer(session) {
1372
+ if (session.gapTimer) {
1373
+ clearTimeout(session.gapTimer);
1374
+ session.gapTimer = null;
1375
+ }
1376
+ }
1377
+ function catchUp() {
1378
+ const session = active;
1379
+ if (!session) return Promise.resolve();
1380
+ if (session.catchingUp) return session.catchingUp;
1381
+ session.catchingUp = (async () => {
1382
+ try {
1383
+ const result = await provider.operationsAfter(session.templateId, session.revision);
1384
+ if (session.retired) return;
1385
+ if ("resync" in result) {
1386
+ await reload(session);
1387
+ return;
1388
+ }
1389
+ for (const batch of result.batches) receive(session, batch);
1390
+ } catch (error) {
1391
+ reportError(error);
1392
+ } finally {
1393
+ session.catchingUp = null;
1394
+ }
1395
+ })();
1396
+ return session.catchingUp;
1397
+ }
1398
+ async function reload(session) {
1399
+ session.recovering = true;
1400
+ try {
1401
+ const template = await options.loadTemplate(session.templateId);
1402
+ if (session.retired) return;
1403
+ const at = template.revision ?? session.revision;
1404
+ session.confirmed = safeClone(template.content);
1405
+ session.revision = at;
1406
+ session.remote = new Map([...session.remote].filter(([remoteAt]) => remoteAt > at));
1407
+ for (const [ownAt, batch] of [...session.own]) if (ownAt <= at) {
1408
+ session.own.delete(ownAt);
1409
+ dropPending(session, batch);
1410
+ }
1411
+ if (session === active) revision.value = at;
1412
+ rebuild(session);
1413
+ drain(session);
1414
+ } catch (error) {
1415
+ reportError(error);
1416
+ } finally {
1417
+ session.recovering = false;
1418
+ }
1419
+ }
1420
+ function handlersFor(session) {
1421
+ const live = () => session === active && !session.retired;
1422
+ return {
1423
+ onBatch: (batch) => {
1424
+ if (live()) receive(session, batch);
1425
+ },
1426
+ onResync: (at, actor) => {
1427
+ if (!live() || at <= session.revision) return;
1428
+ if (actor) try {
1429
+ options.onRemoteResync?.({
1430
+ revision: at,
1431
+ actor
1432
+ });
1433
+ } catch (error) {
1434
+ reportError(error);
1435
+ }
1436
+ catchUp();
1437
+ },
1438
+ onLock: (lock) => {
1439
+ if (live()) setLock(lock);
1440
+ },
1441
+ onUnlock: (blockId, holderSession) => {
1442
+ if (live()) removeLock(blockId, holderSession);
1443
+ },
1444
+ onMembers: (next) => {
1445
+ if (live()) setMembers(next);
1446
+ },
1447
+ onConnectionChange: (state) => {
1448
+ if (live()) changeConnection(session, state);
1449
+ }
1450
+ };
1451
+ }
1452
+ function join(session, refresh) {
1453
+ provider.join(session.templateId, { sessionId }, handlersFor(session)).then((joined) => {
1454
+ if (session.retired || session !== active) {
1455
+ if (!refresh) joined.leave();
1456
+ return;
1457
+ }
1458
+ session.leave = joined.leave;
1459
+ replaceLocks(joined.locks, !refresh);
1460
+ setMembers(joined.members);
1461
+ if (joined.members.length > 0) connection.value = "connected";
1462
+ if (joined.revision > session.revision) catchUp();
1463
+ if (refresh) reconcileLock();
1464
+ }, (error) => reportError(error));
1465
+ }
1466
+ function start(template) {
1467
+ const content = safeClone(template.content);
1468
+ const session = {
1469
+ templateId: template.id,
1470
+ confirmed: content,
1471
+ revision: template.revision ?? 0,
1472
+ pending: [],
1473
+ view: safeClone(content),
1474
+ remote: /* @__PURE__ */ new Map(),
1475
+ own: /* @__PURE__ */ new Map(),
1476
+ inFlight: null,
1477
+ gapTimer: null,
1478
+ catchingUp: null,
1479
+ leave: null,
1480
+ retired: false,
1481
+ recovering: false,
1482
+ undo: [],
1483
+ redo: [],
1484
+ waiters: []
1485
+ };
1486
+ active = session;
1487
+ revision.value = session.revision;
1488
+ bumpHistory();
1489
+ join(session, false);
1490
+ if (editor?.state.selectedBlockId) onSelection(editor.state.selectedBlockId);
1491
+ scheduleCapture();
1492
+ }
1493
+ function retire(session) {
1494
+ session.retired = true;
1495
+ clearGapTimer(session);
1496
+ session.leave?.();
1497
+ session.leave = null;
1498
+ rejectWaiters(session, /* @__PURE__ */ new Error("[Templatical] The template was closed."));
1499
+ }
1500
+ function changeConnection(session, state) {
1501
+ const previous = connection.value;
1502
+ connection.value = state;
1503
+ if (state === "connected" && previous !== null && previous !== "connected") join(session, true);
1504
+ }
1505
+ function switchTo(template) {
1506
+ if (active && active.templateId === template?.id) return;
1507
+ if (active) {
1508
+ clearTypingTimer();
1509
+ retire(active);
1510
+ active = null;
1511
+ }
1512
+ members.value = [];
1513
+ locks.value = /* @__PURE__ */ new Map();
1514
+ connection.value = null;
1515
+ wanted = null;
1516
+ reconcileLock();
1517
+ bumpHistory();
1518
+ if (template) start(template);
1519
+ }
1520
+ function setLock(lock) {
1521
+ if (lock.holder.sessionId === sessionId) return;
1522
+ const next = new Map([...locks.value].filter(([, existing]) => existing.holder.sessionId !== lock.holder.sessionId));
1523
+ next.set(lock.blockId, lock);
1524
+ locks.value = next;
1525
+ if (editor?.state.selectedBlockId === lock.blockId) {
1526
+ editor.selectBlock(null);
1527
+ notify({
1528
+ type: "selectionRefused",
1529
+ blockId: lock.blockId,
1530
+ holder: lock.holder
1531
+ });
1532
+ }
1533
+ }
1534
+ function removeLock(blockId, holderSession) {
1535
+ if (locks.value.get(blockId)?.holder.sessionId !== holderSession) return;
1536
+ const next = new Map(locks.value);
1537
+ next.delete(blockId);
1538
+ locks.value = next;
1539
+ }
1540
+ function replaceLocks(list, merge) {
1541
+ const next = merge ? new Map(locks.value) : /* @__PURE__ */ new Map();
1542
+ for (const lock of list) {
1543
+ if (lock.holder.sessionId === sessionId) continue;
1544
+ if (merge && next.has(lock.blockId)) continue;
1545
+ next.set(lock.blockId, lock);
1546
+ }
1547
+ locks.value = next;
1548
+ }
1549
+ function setMembers(next) {
1550
+ const present = new Set(next.map((member) => member.sessionId));
1551
+ const departed = new Set(members.value.map((member) => member.sessionId).filter((id) => !present.has(id)));
1552
+ members.value = next;
1553
+ if (departed.size === 0) return;
1554
+ const remaining = new Map([...locks.value].filter(([, lock]) => !departed.has(lock.holder.sessionId)));
1555
+ if (remaining.size !== locks.value.size) locks.value = remaining;
1556
+ }
1557
+ function queueLock(task) {
1558
+ lockQueue = lockQueue.then(task).catch(reportError);
1559
+ }
1560
+ /** Move what the backend granted toward what is selected, one call at a time. */
1561
+ function reconcileLock() {
1562
+ if (!locking) return;
1563
+ queueLock(async () => {
1564
+ if (held && (wanted === null || held.templateId !== wanted.templateId)) {
1565
+ const releasing = held;
1566
+ held = null;
1567
+ stopRenewing();
1568
+ await provider.releaseLock(releasing.templateId, releasing.blockId);
1569
+ }
1570
+ const target = wanted;
1571
+ if (!target || held?.templateId === target.templateId && held.blockId === target.blockId) return;
1572
+ try {
1573
+ await provider.acquireLock(target.templateId, target.blockId);
1574
+ held = target;
1575
+ startRenewing();
1576
+ forgetLock(target.blockId);
1577
+ } catch (error) {
1578
+ if (held?.templateId === target.templateId) {
1579
+ const releasing = held;
1580
+ held = null;
1581
+ stopRenewing();
1582
+ await provider.releaseLock(releasing.templateId, releasing.blockId).catch(() => {});
1583
+ }
1584
+ if (wanted !== target) return;
1585
+ refuseSelection(target.blockId, error);
1586
+ }
1587
+ });
1588
+ }
1589
+ function forgetLock(blockId) {
1590
+ if (!locks.value.has(blockId)) return;
1591
+ const next = new Map(locks.value);
1592
+ next.delete(blockId);
1593
+ locks.value = next;
1594
+ }
1595
+ function refuseSelection(blockId, error) {
1596
+ const code = refusalCode(error);
1597
+ if (code === "lock_held" || code === "block_missing") {
1598
+ wanted = null;
1599
+ const holder = refusalHolder(error);
1600
+ if (holder) setLock({
1601
+ blockId,
1602
+ holder,
1603
+ expiresAt: ""
1604
+ });
1605
+ else if (editor?.state.selectedBlockId === blockId) editor.selectBlock(null);
1606
+ return;
1607
+ }
1608
+ reportError(error);
1609
+ setTimeout(() => {
1610
+ if (wanted?.blockId === blockId) reconcileLock();
1611
+ }, RETRY_DELAYS_MS[0]);
1612
+ }
1613
+ function startRenewing() {
1614
+ if (renewTimer) return;
1615
+ renewTimer = setInterval(() => {
1616
+ queueLock(async () => {
1617
+ const current = held;
1618
+ if (!current) return;
1619
+ try {
1620
+ await provider.renewLock(current.templateId, current.blockId);
1621
+ } catch (error) {
1622
+ if (refusalCode(error) === "lock_lost") {
1623
+ held = null;
1624
+ stopRenewing();
1625
+ reconcileLock();
1626
+ return;
1627
+ }
1628
+ reportError(error);
1629
+ }
1630
+ });
1631
+ }, COLLABORATION_LOCK_RENEW_MS);
1632
+ }
1633
+ function stopRenewing() {
1634
+ if (renewTimer) {
1635
+ clearInterval(renewTimer);
1636
+ renewTimer = null;
1637
+ }
1638
+ }
1639
+ function onSelection(blockId) {
1640
+ flush();
1641
+ if (!active) return;
1642
+ wanted = blockId ? {
1643
+ templateId: active.templateId,
1644
+ blockId
1645
+ } : null;
1646
+ reconcileLock();
1647
+ }
1648
+ /** The page is going away: send what's typed, let go of the lock, leave. */
1649
+ function onPageHide() {
1650
+ flush();
1651
+ if (held && locking) {
1652
+ provider.releaseLock(held.templateId, held.blockId).catch(() => {});
1653
+ held = null;
1654
+ stopRenewing();
1655
+ }
1656
+ active?.leave?.();
1657
+ if (active) active.leave = null;
1658
+ }
1659
+ function step(direction) {
1660
+ const session = active;
1661
+ if (!session || suspended) return;
1662
+ flush();
1663
+ const entry = (direction === "undo" ? session.undo : session.redo).pop();
1664
+ if (!entry) return;
1665
+ bumpHistory();
1666
+ let next;
1667
+ try {
1668
+ next = applyOperations(session.view, entry.ops, lockContext());
1669
+ } catch (error) {
1670
+ if (!(error instanceof OperationError)) throw error;
1671
+ notify({
1672
+ type: "undoSkipped",
1673
+ code: error.code
1674
+ });
1675
+ return;
1676
+ }
1677
+ const counter = { ops: invertOperations$1(session.view, entry.ops) };
1678
+ pushCapped(direction === "undo" ? session.redo : session.undo, counter);
1679
+ session.view = next;
1680
+ renderView(next);
1681
+ editor?.markDirty();
1682
+ enqueue(session, entry.ops, direction, counter);
1683
+ }
1684
+ function applyFromTurn(ops, options) {
1685
+ const session = active;
1686
+ if (!editor || !session || suspended) return {
1687
+ applied: false,
1688
+ error: new OperationError("invalid_operation", 0, "No template is open for editing.")
1689
+ };
1690
+ flush();
1691
+ let next;
1692
+ let inverse;
1693
+ try {
1694
+ next = applyOperations(session.view, ops, lockContext());
1695
+ inverse = invertOperations$1(session.view, ops);
1696
+ } catch (error) {
1697
+ if (error instanceof OperationError) return {
1698
+ applied: false,
1699
+ error
1700
+ };
1701
+ throw error;
1702
+ }
1703
+ const top = session.undo.at(-1);
1704
+ let entry;
1705
+ if (top && top.turn === options.turn) {
1706
+ top.ops = [...inverse, ...top.ops];
1707
+ entry = top;
1708
+ } else {
1709
+ entry = {
1710
+ ops: inverse,
1711
+ turn: options.turn
1712
+ };
1713
+ pushCapped(session.undo, entry);
1714
+ }
1715
+ session.redo = [];
1716
+ bumpHistory();
1717
+ session.view = next;
1718
+ renderView(next);
1719
+ editor.markDirty();
1720
+ enqueue(session, ops, options.origin, entry);
1721
+ return {
1722
+ applied: true,
1723
+ inverse
1724
+ };
1725
+ }
1726
+ function attach(target) {
1727
+ editor = target;
1728
+ stopWatching.push(watch(() => target.state.template, (template) => switchTo(template), { immediate: true }), watch(() => target.content.value, scheduleCapture, { deep: true }), watch(() => target.state.selectedBlockId, onSelection), watch(() => target.state.isLoading, (loading) => loading ? capture(true, true) : scheduleCapture()));
1729
+ if (typeof window !== "undefined") {
1730
+ window.addEventListener("pagehide", onPageHide);
1731
+ window.addEventListener("blur", flush);
1732
+ }
1733
+ if (getCurrentScope()) onScopeDispose(destroy);
1734
+ }
1735
+ function settle() {
1736
+ flush();
1737
+ const session = active;
1738
+ if (!session || session.pending.length === 0) return Promise.resolve();
1739
+ return new Promise((resolve, reject) => session.waiters.push({
1740
+ resolve,
1741
+ reject
1742
+ }));
1743
+ }
1744
+ function suspend() {
1745
+ flush();
1746
+ suspended = true;
1747
+ }
1748
+ function resume() {
1749
+ suspended = false;
1750
+ if (active) renderView(active.view);
1751
+ }
1752
+ function destroy() {
1753
+ if (destroyed) return;
1754
+ flush();
1755
+ for (const stop of stopWatching.splice(0)) stop();
1756
+ clearTypingTimer();
1757
+ onPageHide();
1758
+ destroyed = true;
1759
+ if (active) retire(active);
1760
+ active = null;
1761
+ stopRenewing();
1762
+ if (typeof window !== "undefined") {
1763
+ window.removeEventListener("pagehide", onPageHide);
1764
+ window.removeEventListener("blur", flush);
1765
+ }
1766
+ }
1767
+ return {
1768
+ sessionId,
1769
+ members,
1770
+ locks,
1771
+ connection,
1772
+ revision,
1773
+ canUndo,
1774
+ canRedo,
1775
+ attach,
1776
+ undo: () => step("undo"),
1777
+ redo: () => step("redo"),
1778
+ clearHistory: () => {
1779
+ if (!active) return;
1780
+ active.undo = [];
1781
+ active.redo = [];
1782
+ bumpHistory();
1783
+ },
1784
+ flush,
1785
+ settle,
1786
+ suspend,
1787
+ resume,
1788
+ catchUp,
1789
+ hasConfirmedBlock: (blockId) => active ? containsBlock(active.confirmed.blocks, blockId) : false,
1790
+ applyOperations: applyFromTurn,
1791
+ destroy
1792
+ };
1793
+ }
1794
+ //#endregion
661
1795
  //#region src/saved-blocks.ts
662
1796
  /**
663
1797
  * Reactive state over a {@link SavedBlocksProvider}.
@@ -1402,9 +2536,9 @@ function useCommentListener(options) {
1402
2536
  }
1403
2537
  });
1404
2538
  }, { immediate: true });
1405
- onScopeDispose(stop);
2539
+ onScopeDispose$1(stop);
1406
2540
  }
1407
2541
  //#endregion
1408
- export { DEFAULT_AUTO_SAVE_DEBOUNCE_MS, createLocalStorageMediaProvider, createLocalStorageSavedBlocksProvider, useAutoSave, useBlockActions, useCommentListener, useComments, useConditionPreview, useDataSourceFetch, useEditor, useHistory, useHistoryInterceptor, useSavedBlocks, useVersionHistory };
2542
+ export { DEFAULT_AUTO_SAVE_DEBOUNCE_MS, createLocalStorageMediaProvider, createLocalStorageSavedBlocksProvider, diffContent, invertOperations, useAutoSave, useBlockActions, useCollaboration, useCommentListener, useComments, useConditionPreview, useDataSourceFetch, useEditor, useHistory, useHistoryInterceptor, useSavedBlocks, useVersionHistory };
1409
2543
 
1410
2544
  //# sourceMappingURL=index.js.map