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