@formicoidea/labre-core 0.58.0 → 0.59.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.
Files changed (167) hide show
  1. package/architecture/code.c4 +94 -65
  2. package/architecture/domain.c4 +11 -5
  3. package/dist/_pkgs/affine-block-attachment/edgeless-clipboard-config.d.ts +3 -2
  4. package/dist/_pkgs/affine-block-attachment/edgeless-clipboard-config.js +18 -13
  5. package/dist/_pkgs/affine-block-callout/adapters/markdown.js +4 -1
  6. package/dist/_pkgs/affine-block-code/adapters/html.js +1 -1
  7. package/dist/_pkgs/affine-block-code/adapters/markdown/preprocessor.js +13 -1
  8. package/dist/_pkgs/affine-block-code/adapters/notion-html.js +1 -1
  9. package/dist/_pkgs/affine-block-code/code-block.d.ts +17 -2
  10. package/dist/_pkgs/affine-block-code/code-block.js +28 -23
  11. package/dist/_pkgs/affine-block-code/code-toolbar/components/lang-button.js +17 -4
  12. package/dist/_pkgs/affine-block-code/code-toolbar/components/preview-button.js +1 -1
  13. package/dist/_pkgs/affine-block-data-view/data-source.js +12 -5
  14. package/dist/_pkgs/affine-block-edgeless-text/edgeless-clipboard-config.d.ts +3 -2
  15. package/dist/_pkgs/affine-block-edgeless-text/edgeless-clipboard-config.js +8 -5
  16. package/dist/_pkgs/affine-block-image/asset-picker.d.ts +23 -2
  17. package/dist/_pkgs/affine-block-image/asset-picker.js +52 -12
  18. package/dist/_pkgs/affine-block-image/configs/replace-action.d.ts +8 -5
  19. package/dist/_pkgs/affine-block-image/configs/replace-action.js +8 -8
  20. package/dist/_pkgs/affine-block-image/configs/toolbar.js +2 -1
  21. package/dist/_pkgs/affine-block-image/crop/crop-controller.d.ts +15 -0
  22. package/dist/_pkgs/affine-block-image/crop/crop-controller.js +40 -2
  23. package/dist/_pkgs/affine-block-image/crop/crop-math.d.ts +22 -0
  24. package/dist/_pkgs/affine-block-image/crop/crop-math.js +55 -2
  25. package/dist/_pkgs/affine-block-image/crop/crop-overlay.d.ts +17 -3
  26. package/dist/_pkgs/affine-block-image/crop/crop-overlay.js +64 -6
  27. package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.d.ts +3 -2
  28. package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.js +20 -14
  29. package/dist/_pkgs/affine-block-image/image-block.js +1 -0
  30. package/dist/_pkgs/affine-block-image/image-edgeless-block.js +3 -0
  31. package/dist/_pkgs/affine-block-image/index.d.ts +2 -0
  32. package/dist/_pkgs/affine-block-image/index.js +2 -0
  33. package/dist/_pkgs/affine-block-image/replace/commands.d.ts +68 -0
  34. package/dist/_pkgs/affine-block-image/replace/commands.js +200 -0
  35. package/dist/_pkgs/affine-block-image/replace/replace-model.d.ts +44 -0
  36. package/dist/_pkgs/affine-block-image/replace/replace-model.js +168 -0
  37. package/dist/_pkgs/affine-block-image/replace/toolbar.d.ts +21 -0
  38. package/dist/_pkgs/affine-block-image/replace/toolbar.js +159 -0
  39. package/dist/_pkgs/affine-block-image/translations.d.ts +15 -2
  40. package/dist/_pkgs/affine-block-image/translations.js +35 -2
  41. package/dist/_pkgs/affine-block-image/utils.d.ts +7 -0
  42. package/dist/_pkgs/affine-block-image/utils.js +13 -0
  43. package/dist/_pkgs/affine-block-image/view.js +7 -0
  44. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.d.ts +14 -5
  45. package/dist/_pkgs/affine-block-note/components/edgeless-page-block-title.js +18 -11
  46. package/dist/_pkgs/affine-block-note/config.d.ts +9 -2
  47. package/dist/_pkgs/affine-block-note/edgeless-clipboard-config.d.ts +3 -2
  48. package/dist/_pkgs/affine-block-note/edgeless-clipboard-config.js +8 -5
  49. package/dist/_pkgs/affine-block-paragraph/adapters/markdown.js +11 -0
  50. package/dist/_pkgs/affine-block-root/commands/invisible-characters-commands.d.ts +17 -0
  51. package/dist/_pkgs/affine-block-root/commands/invisible-characters-commands.js +123 -0
  52. package/dist/_pkgs/affine-block-root/edgeless/clipboard/command.js +82 -65
  53. package/dist/_pkgs/affine-block-root/edgeless/edgeless-keyboard.js +2 -0
  54. package/dist/_pkgs/affine-block-root/index.d.ts +1 -0
  55. package/dist/_pkgs/affine-block-root/index.js +1 -0
  56. package/dist/_pkgs/affine-block-root/translations.d.ts +6 -0
  57. package/dist/_pkgs/affine-block-root/translations.js +10 -0
  58. package/dist/_pkgs/affine-block-root/view.js +6 -1
  59. package/dist/_pkgs/affine-block-surface/commands/index.d.ts +1 -1
  60. package/dist/_pkgs/affine-block-surface/commands/index.js +1 -1
  61. package/dist/_pkgs/affine-block-surface/commands/reassociate-connectors.d.ts +9 -0
  62. package/dist/_pkgs/affine-block-surface/commands/reassociate-connectors.js +21 -12
  63. package/dist/_pkgs/affine-block-surface/extensions/clipboard-config.d.ts +33 -0
  64. package/dist/_pkgs/affine-block-surface/extensions/clipboard-config.js +53 -0
  65. package/dist/_pkgs/affine-block-surface/framework-background/background-view.js +2 -1
  66. package/dist/_pkgs/affine-block-surface/framework-background/band-widths.d.ts +104 -0
  67. package/dist/_pkgs/affine-block-surface/framework-background/band-widths.js +156 -0
  68. package/dist/_pkgs/affine-block-surface/framework-background/def.d.ts +87 -4
  69. package/dist/_pkgs/affine-block-surface/framework-background/def.js +26 -7
  70. package/dist/_pkgs/affine-block-surface/framework-background/ellipsis.d.ts +58 -0
  71. package/dist/_pkgs/affine-block-surface/framework-background/ellipsis.js +110 -0
  72. package/dist/_pkgs/affine-block-surface/framework-background/index.d.ts +3 -0
  73. package/dist/_pkgs/affine-block-surface/framework-background/index.js +3 -0
  74. package/dist/_pkgs/affine-block-surface/framework-background/labels.d.ts +12 -2
  75. package/dist/_pkgs/affine-block-surface/framework-background/labels.js +29 -5
  76. package/dist/_pkgs/affine-block-surface/framework-background/measure-text.d.ts +14 -0
  77. package/dist/_pkgs/affine-block-surface/framework-background/measure-text.js +31 -0
  78. package/dist/_pkgs/affine-block-surface/framework-background/renderer.d.ts +14 -1
  79. package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +84 -31
  80. package/dist/_pkgs/affine-block-table/adapters/html.js +26 -14
  81. package/dist/_pkgs/affine-block-table/adapters/utils.d.ts +1 -0
  82. package/dist/_pkgs/affine-block-table/adapters/utils.js +23 -5
  83. package/dist/_pkgs/affine-block-table/header-menu.d.ts +13 -0
  84. package/dist/_pkgs/affine-block-table/header-menu.js +25 -0
  85. package/dist/_pkgs/affine-block-table/selection-controller.d.ts +15 -0
  86. package/dist/_pkgs/affine-block-table/selection-controller.js +62 -0
  87. package/dist/_pkgs/affine-block-table/table-cell-css.d.ts +7 -0
  88. package/dist/_pkgs/affine-block-table/table-cell-css.js +10 -0
  89. package/dist/_pkgs/affine-block-table/table-cell.d.ts +8 -0
  90. package/dist/_pkgs/affine-block-table/table-cell.js +35 -3
  91. package/dist/_pkgs/affine-block-table/table-data-manager.d.ts +14 -0
  92. package/dist/_pkgs/affine-block-table/table-data-manager.js +44 -3
  93. package/dist/_pkgs/affine-block-table/translations.d.ts +14 -0
  94. package/dist/_pkgs/affine-block-table/translations.js +22 -0
  95. package/dist/_pkgs/affine-gfx-mindmap/toolbar/mindmap-tool-button.d.ts +2 -2
  96. package/dist/_pkgs/affine-gfx-template/builtin/other.d.ts +4 -0
  97. package/dist/_pkgs/affine-gfx-template/builtin/other.js +1 -1
  98. package/dist/_pkgs/affine-gfx-template/builtin/slides/build.d.ts +81 -0
  99. package/dist/_pkgs/affine-gfx-template/builtin/slides/build.js +276 -0
  100. package/dist/_pkgs/affine-gfx-template/builtin/slides/consts.d.ts +126 -0
  101. package/dist/_pkgs/affine-gfx-template/builtin/slides/consts.js +101 -0
  102. package/dist/_pkgs/affine-gfx-template/builtin/slides/index.d.ts +8 -0
  103. package/dist/_pkgs/affine-gfx-template/builtin/slides/index.js +27 -0
  104. package/dist/_pkgs/affine-gfx-template/builtin/slides/masks.d.ts +83 -0
  105. package/dist/_pkgs/affine-gfx-template/builtin/slides/masks.js +674 -0
  106. package/dist/_pkgs/affine-gfx-template/contribute.js +12 -17
  107. package/dist/_pkgs/affine-gfx-template/make-snapshot.d.ts +14 -2
  108. package/dist/_pkgs/affine-gfx-template/make-snapshot.js +6 -2
  109. package/dist/_pkgs/affine-gfx-template/services/template-middlewares.d.ts +12 -0
  110. package/dist/_pkgs/affine-gfx-template/services/template-middlewares.js +48 -0
  111. package/dist/_pkgs/affine-gfx-template/services/template.js +2 -1
  112. package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.d.ts +15 -1
  113. package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.js +33 -37
  114. package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +6 -0
  115. package/dist/_pkgs/affine-gfx-template/translations.d.ts +94 -0
  116. package/dist/_pkgs/affine-gfx-template/translations.js +441 -2
  117. package/dist/_pkgs/affine-model/blocks/table/table-model.d.ts +4 -0
  118. package/dist/_pkgs/affine-model/blocks/table/table-model.js +4 -0
  119. package/dist/_pkgs/affine-model/elements/bpmn/node.d.ts +46 -3
  120. package/dist/_pkgs/affine-model/elements/bpmn/node.js +47 -4
  121. package/dist/_pkgs/affine-model/elements/bpmn/pool.d.ts +20 -0
  122. package/dist/_pkgs/affine-model/elements/bpmn/pool.js +33 -4
  123. package/dist/_pkgs/affine-shared/services/edit-props-store.d.ts +3 -16
  124. package/dist/_pkgs/affine-shared/services/edit-props-store.js +16 -16
  125. package/dist/_pkgs/affine-shared/services/editor-setting-service.d.ts +3 -0
  126. package/dist/_pkgs/affine-shared/services/editor-setting-service.js +4 -0
  127. package/dist/_pkgs/affine-shared/services/index.d.ts +1 -0
  128. package/dist/_pkgs/affine-shared/services/index.js +1 -0
  129. package/dist/_pkgs/affine-shared/services/invisible-characters.d.ts +33 -0
  130. package/dist/_pkgs/affine-shared/services/invisible-characters.js +63 -0
  131. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-cache.d.ts +0 -9
  132. package/dist/_pkgs/affine-shared/services/link-preview-service/link-preview-cache.js +3 -9
  133. package/dist/_pkgs/affine-shared/services/telemetry-service/lifecycle.d.ts +26 -0
  134. package/dist/_pkgs/affine-shared/services/telemetry-service/telemetry-service.d.ts +4 -3
  135. package/dist/_pkgs/affine-shared/services/telemetry-service/types.d.ts +11 -0
  136. package/dist/_pkgs/affine-shared/utils/index.d.ts +1 -0
  137. package/dist/_pkgs/affine-shared/utils/index.js +1 -0
  138. package/dist/_pkgs/affine-shared/utils/invisible-characters.d.ts +42 -0
  139. package/dist/_pkgs/affine-shared/utils/invisible-characters.js +81 -0
  140. package/dist/_pkgs/affine-widget-edgeless-zoom-toolbar/zoom-toolbar.js +13 -2
  141. package/dist/_pkgs/data-view/view-presets/table/pc/header/column-header.js +6 -0
  142. package/dist/_pkgs/data-view/view-presets/table/pc/table-view-ui-logic.js +5 -2
  143. package/dist/_pkgs/std/clipboard/clipboard.d.ts +15 -1
  144. package/dist/_pkgs/std/clipboard/clipboard.js +18 -0
  145. package/dist/_pkgs/std/inline/components/embed-gap.js +1 -1
  146. package/dist/_pkgs/std/inline/components/v-line.js +1 -1
  147. package/dist/_pkgs/std/inline/components/v-text.d.ts +15 -2
  148. package/dist/_pkgs/std/inline/components/v-text.js +147 -15
  149. package/dist/_pkgs/std/inline/consts.d.ts +5 -0
  150. package/dist/_pkgs/std/inline/consts.js +5 -0
  151. package/dist/_pkgs/std/inline/utils/index.d.ts +1 -0
  152. package/dist/_pkgs/std/inline/utils/index.js +1 -0
  153. package/dist/_pkgs/std/inline/utils/invisible-characters.d.ts +57 -0
  154. package/dist/_pkgs/std/inline/utils/invisible-characters.js +162 -0
  155. package/dist/_pkgs/std/inline/utils/point-conversion.js +3 -3
  156. package/dist/_pkgs/std/inline/utils/text.d.ts +8 -0
  157. package/dist/_pkgs/std/inline/utils/text.js +11 -2
  158. package/dist/_pkgs/store/model/store/crud.d.ts +28 -0
  159. package/dist/_pkgs/store/model/store/crud.js +97 -30
  160. package/dist/_pkgs/store/model/store/query.js +3 -2
  161. package/dist/_pkgs/store/model/store/store.d.ts +21 -0
  162. package/dist/_pkgs/store/model/store/store.js +88 -21
  163. package/dist/_pkgs/store/transformer/transformer.d.ts +46 -0
  164. package/dist/_pkgs/store/transformer/transformer.js +72 -0
  165. package/dist/commands.js +6 -1
  166. package/dist/translations.js +2 -0
  167. package/package.json +3 -4
@@ -4,21 +4,95 @@ import { isPureObject, native2Y } from '../../reactive/index.js';
4
4
  import { internalPrimitives } from '../block/zod.js';
5
5
  export class DocCRUD {
6
6
  get root() {
7
- let rootId = null;
8
- this._yBlocks.forEach(yBlock => {
9
- const flavour = yBlock.get('sys:flavour');
10
- const schema = this._schema.flavourSchemaMap.get(flavour);
11
- if (!schema)
12
- return;
13
- if (schema.model.role === 'root') {
14
- rootId = yBlock.get('sys:id');
15
- }
16
- });
17
- return rootId;
7
+ return this._ensureIndex().root;
18
8
  }
19
9
  constructor(_yBlocks, _schema) {
20
10
  this._yBlocks = _yBlocks;
21
11
  this._schema = _schema;
12
+ /**
13
+ * In-memory index of the tree: `childId -> parentId` plus the root id,
14
+ * rebuilt lazily after any structural change. Nothing here is written to Yjs
15
+ * (ADR 0041).
16
+ *
17
+ * Why: `root` scanned every block and `getParent` walked the tree from the
18
+ * root, so a load path calling `getParent` once per block was quadratic
19
+ * (#507: 11 s for 4000 blocks under a query with `viewType: 'hidden'`, whose
20
+ * `setAncestorsToDisplayIfHidden` climbs the ancestors of every block). #508
21
+ * only skipped that walk for queries that cannot hide blocks; the index
22
+ * makes the walk itself linear.
23
+ *
24
+ * Invalidation contract:
25
+ * - every transaction, local, remote (`Y.applyUpdate`) or undo, reaches
26
+ * `beforeObserverCalls` before any observer runs, so observers and
27
+ * everything after them read a fresh index;
28
+ * - inside a transaction, before that event, only the mutators below write
29
+ * the tree (`sys:children`, the blocks map): each drops the index before
30
+ * its first write, so a throw half-way leaves no stale index, and again
31
+ * after its last one, so a read in between cannot cache a half-written
32
+ * tree. A new writer of `sys:children` must do the same.
33
+ */
34
+ this._index = null;
35
+ /** Once deaf to remote changes, every read rebuilds (the pre-#507 cost). */
36
+ this._live = false;
37
+ this._onBeforeObserverCalls = (transaction) => {
38
+ if (!this._index)
39
+ return;
40
+ // `changed` only lists types that existed before the transaction: a block
41
+ // created in it shows up through the blocks map, a children array created
42
+ // in it through its block's `sys:children` key.
43
+ for (const [type, keys] of transaction.changed) {
44
+ if (type === this._yBlocks ||
45
+ type._item?.parentSub === 'sys:children' ||
46
+ keys.has('sys:children') ||
47
+ keys.has('sys:flavour') ||
48
+ keys.has('sys:id')) {
49
+ this._index = null;
50
+ return;
51
+ }
52
+ }
53
+ };
54
+ if (!_yBlocks.doc)
55
+ return;
56
+ _yBlocks.doc.on('beforeObserverCalls', this._onBeforeObserverCalls);
57
+ this._live = true;
58
+ }
59
+ dispose() {
60
+ this._yBlocks.doc?.off('beforeObserverCalls', this._onBeforeObserverCalls);
61
+ this._live = false;
62
+ this._index = null;
63
+ }
64
+ // Same answers as the former scan + DFS: the last root in map order, and the
65
+ // first parent met in a pre-order walk from it. Orphans have none, nor does
66
+ // the root even when some block lists it as a child. A block listed twice
67
+ // keeps its first parent and its subtree is walked once, which also ends a
68
+ // cycle where the former DFS overflowed the stack.
69
+ _ensureIndex() {
70
+ if (this._index)
71
+ return this._index;
72
+ let root = null;
73
+ this._yBlocks.forEach(yBlock => {
74
+ const schema = this._schema.flavourSchemaMap.get(yBlock.get('sys:flavour'));
75
+ if (schema?.model.role === 'root')
76
+ root = yBlock.get('sys:id');
77
+ });
78
+ const parentOf = new Map();
79
+ const walk = (parentId) => {
80
+ const children = this._yBlocks.get(parentId)?.get('sys:children');
81
+ if (!children)
82
+ return;
83
+ for (const childId of children.toArray()) {
84
+ if (childId === root || parentOf.has(childId))
85
+ continue;
86
+ parentOf.set(childId, parentId);
87
+ walk(childId);
88
+ }
89
+ };
90
+ if (root)
91
+ walk(root);
92
+ const index = { root, parentOf };
93
+ if (this._live)
94
+ this._index = index;
95
+ return index;
22
96
  }
23
97
  _getSiblings(id, fn) {
24
98
  const parentId = this.getParent(id);
@@ -46,6 +120,10 @@ export class DocCRUD {
46
120
  ? this._yBlocks.get(parent)?.get('sys:flavour')
47
121
  : undefined;
48
122
  this._schema.validate(flavour, parentFlavour);
123
+ // Read the index before the first write; a root-role block never has a
124
+ // default parent, so the new block cannot change the answer.
125
+ const parentId = parent ?? (schema.model.role === 'root' ? null : this.root);
126
+ this._index = null;
49
127
  const yBlock = new Y.Map();
50
128
  this._yBlocks.set(id, yBlock);
51
129
  const version = schema.version;
@@ -84,7 +162,6 @@ export class DocCRUD {
84
162
  yBlock.set(`prop:${key}`, native2Y(value));
85
163
  });
86
164
  }
87
- const parentId = parent ?? (schema.model.role === 'root' ? null : this.root);
88
165
  if (!parentId)
89
166
  return;
90
167
  const yParent = this._yBlocks.get(parentId);
@@ -97,6 +174,7 @@ export class DocCRUD {
97
174
  : parentIndex
98
175
  : yParentChildren.length;
99
176
  yParentChildren.insert(index, [id]);
177
+ this._index = null;
100
178
  }
101
179
  deleteBlock(id, options = {
102
180
  deleteChildren: true,
@@ -116,6 +194,7 @@ export class DocCRUD {
116
194
  const yParent = this._yBlocks.get(parent);
117
195
  const yParentChildren = yParent.get('sys:children');
118
196
  const modelIndex = yParentChildren.toArray().indexOf(id);
197
+ this._index = null;
119
198
  if (modelIndex > -1) {
120
199
  yParentChildren.delete(modelIndex, 1);
121
200
  }
@@ -162,6 +241,7 @@ export class DocCRUD {
162
241
  };
163
242
  apply();
164
243
  this._yBlocks.delete(id);
244
+ this._index = null;
165
245
  }
166
246
  getNext(id) {
167
247
  return this._getSiblings(id, (index, parent) => parent
@@ -170,24 +250,7 @@ export class DocCRUD {
170
250
  .at(index + 1) ?? null);
171
251
  }
172
252
  getParent(targetId) {
173
- const root = this.root;
174
- if (!root || root === targetId)
175
- return null;
176
- const findParent = (parentId) => {
177
- const parentYBlock = this._yBlocks.get(parentId);
178
- if (!parentYBlock)
179
- return null;
180
- const children = parentYBlock.get('sys:children');
181
- for (const childId of children.toArray()) {
182
- if (childId === targetId)
183
- return parentId;
184
- const parent = findParent(childId);
185
- if (parent != null)
186
- return parent;
187
- }
188
- return null;
189
- };
190
- return findParent(root);
253
+ return this._ensureIndex().parentOf.get(targetId) ?? null;
191
254
  }
192
255
  getPrev(id) {
193
256
  return this._getSiblings(id, (index, parent) => {
@@ -241,6 +304,7 @@ export class DocCRUD {
241
304
  children.push(blockId);
242
305
  });
243
306
  let insertIndex = 0;
307
+ this._index = null;
244
308
  Array.from(childBlocksPerParent.entries()).forEach(([parentBlock, blocksToMove], index) => {
245
309
  const targetParentBlock = this._yBlocks.get(newParent);
246
310
  if (!targetParentBlock)
@@ -280,13 +344,16 @@ export class DocCRUD {
280
344
  updateInsertIndex();
281
345
  targetParentChildren.insert(insertIndex, blocksToMove);
282
346
  });
347
+ this._index = null;
283
348
  }
284
349
  updateBlockChildren(id, children) {
285
350
  const yBlock = this._yBlocks.get(id);
286
351
  if (!yBlock)
287
352
  return;
288
353
  const yChildrenArray = yBlock.get('sys:children');
354
+ this._index = null;
289
355
  yChildrenArray.delete(0, yChildrenArray.length);
290
356
  yChildrenArray.push(children);
357
+ this._index = null;
291
358
  }
292
359
  }
@@ -10,9 +10,10 @@ export function runQuery(query, block) {
10
10
  /**
11
11
  * A loose query hides a block only through a `viewType: 'hidden'` entry, so
12
12
  * without one no ancestor can be hidden and the walk below is a no-op. The
13
- * walk costs one O(n) `getParent` per ancestor, which made opening a store
13
+ * walk cost one O(n) `getParent` per ancestor, which made opening a store
14
14
  * with the default query quadratic in the block count (#507, guard
15
- * `__tests__/query.unit.spec.ts`).
15
+ * `__tests__/query.unit.spec.ts`); `getParent` is now an index lookup
16
+ * (ADR 0041), and skipping the no-op walk still saves the calls.
16
17
  */
17
18
  function canHideBlocks(query) {
18
19
  if (query.mode !== 'loose')
@@ -172,6 +172,27 @@ export declare class Store {
172
172
  private readonly _runQuery;
173
173
  private readonly _doc;
174
174
  private readonly _blocks;
175
+ /**
176
+ * The blocks record while a batch runs, `null` otherwise (#507, ADR 0041).
177
+ *
178
+ * Adding a block used to copy the whole record into a new one, once per
179
+ * block: quadratic when a store opens on n blocks, and far slower with real
180
+ * string ids than with integer-like ones, which V8 copies on a fast path
181
+ * (2000 string ids: 2.6 s under any query). A batch copies the record once,
182
+ * adds and removes blocks in this draft, and assigns `_blocks` once at the
183
+ * end, so its subscribers are notified once.
184
+ *
185
+ * The store's own reads (`getBlock`, `hasBlock`, `getParent`,
186
+ * `blockSize`…) go through `_peekBlocks()` and see each block as soon as
187
+ * its `add` is handled, as before; `blockUpdated` still fires per block.
188
+ * Only a direct read of `store.blocks` sees the batch at its end. Batches:
189
+ * the init loop of the constructor, and a transaction that adds or removes
190
+ * more than one block; a single block keeps the per-block assignment.
191
+ */
192
+ private _blocksDraft;
193
+ private _blocksDraftChanged;
194
+ private _peekBlocks;
195
+ private _batchBlocks;
175
196
  private readonly _crud;
176
197
  private readonly _query;
177
198
  private readonly _readonly;
@@ -24,6 +24,28 @@ const internalExtensions = [StoreSelectionExtension, HistoryExtension];
24
24
  * @category Store
25
25
  */
26
26
  export class Store {
27
+ _peekBlocks() {
28
+ return this._blocksDraft ?? this._blocks.peek();
29
+ }
30
+ _batchBlocks(fn) {
31
+ // A batch opened inside another one is part of it: the outer one assigns.
32
+ if (this._blocksDraft) {
33
+ fn();
34
+ return;
35
+ }
36
+ const draft = { ...this._blocks.peek() };
37
+ this._blocksDraft = draft;
38
+ this._blocksDraftChanged = false;
39
+ try {
40
+ fn();
41
+ }
42
+ finally {
43
+ this._blocksDraft = null;
44
+ // An unchanged batch notifies nobody, like the per-block path.
45
+ if (this._blocksDraftChanged)
46
+ this._blocks.value = draft;
47
+ }
48
+ }
27
49
  /**
28
50
  * Get the id of the store.
29
51
  *
@@ -73,7 +95,7 @@ export class Store {
73
95
  * @category Block CRUD
74
96
  */
75
97
  get blockSize() {
76
- return Object.values(this._blocks.peek()).length;
98
+ return Object.values(this._peekBlocks()).length;
77
99
  }
78
100
  /**
79
101
  * Check if the store can redo
@@ -271,6 +293,25 @@ export class Store {
271
293
  runQuery(this._query, block);
272
294
  };
273
295
  this._blocks = signal({});
296
+ /**
297
+ * The blocks record while a batch runs, `null` otherwise (#507, ADR 0041).
298
+ *
299
+ * Adding a block used to copy the whole record into a new one, once per
300
+ * block: quadratic when a store opens on n blocks, and far slower with real
301
+ * string ids than with integer-like ones, which V8 copies on a fast path
302
+ * (2000 string ids: 2.6 s under any query). A batch copies the record once,
303
+ * adds and removes blocks in this draft, and assigns `_blocks` once at the
304
+ * end, so its subscribers are notified once.
305
+ *
306
+ * The store's own reads (`getBlock`, `hasBlock`, `getParent`,
307
+ * `blockSize`…) go through `_peekBlocks()` and see each block as soon as
308
+ * its `add` is handled, as before; `blockUpdated` still fires per block.
309
+ * Only a direct read of `store.blocks` sees the batch at its end. Batches:
310
+ * the init loop of the constructor, and a transaction that adds or removes
311
+ * more than one block; a single block keeps the per-block assignment.
312
+ */
313
+ this._blocksDraft = null;
314
+ this._blocksDraftChanged = false;
274
315
  this._query = {
275
316
  match: [],
276
317
  mode: 'loose',
@@ -350,7 +391,16 @@ export class Store {
350
391
  this.disposableGroup.add(this.slots.rootDeleted);
351
392
  };
352
393
  this._handleYEvents = (events) => {
353
- events.forEach(event => this._handleYEvent(event));
394
+ const handle = () => events.forEach(event => this._handleYEvent(event));
395
+ // One transaction that adds or removes several blocks (a remote load, a
396
+ // paste, a subtree deletion) assigns the blocks record once (#507); a
397
+ // single block keeps the per-block path unchanged.
398
+ const keys = events.reduce((count, event) => event.target === this._yBlocks ? count + event.keys.size : count, 0);
399
+ if (keys > 1) {
400
+ this._batchBlocks(handle);
401
+ return;
402
+ }
403
+ handle();
354
404
  };
355
405
  this.slots = {
356
406
  ready: new Subject(),
@@ -383,12 +433,14 @@ export class Store {
383
433
  this._query = query;
384
434
  }
385
435
  this._yBlocks.observeDeep(this._handleYEvents);
386
- this._yBlocks.forEach((_, id) => {
387
- this._handleYBlockAdd(id, false);
388
- if (id in this._blocks.peek()) {
389
- return;
390
- }
391
- this._onBlockAdded(id, false, true);
436
+ this._batchBlocks(() => {
437
+ this._yBlocks.forEach((_, id) => {
438
+ this._handleYBlockAdd(id, false);
439
+ if (id in this._peekBlocks()) {
440
+ return;
441
+ }
442
+ this._onBlockAdded(id, false, true);
443
+ });
392
444
  });
393
445
  this._subscribeToSlots();
394
446
  }
@@ -406,7 +458,7 @@ export class Store {
406
458
  }
407
459
  _onBlockAdded(id, isLocal, init) {
408
460
  try {
409
- if (id in this._blocks.peek()) {
461
+ if (id in this._peekBlocks()) {
410
462
  return;
411
463
  }
412
464
  const yBlock = this._yBlocks.get(id);
@@ -430,10 +482,16 @@ export class Store {
430
482
  };
431
483
  const block = new Block(this._schema, yBlock, this, options);
432
484
  this._runQuery(block);
433
- this._blocks.value = {
434
- ...this._blocks.value,
435
- [id]: block,
436
- };
485
+ if (this._blocksDraft) {
486
+ this._blocksDraft[id] = block;
487
+ this._blocksDraftChanged = true;
488
+ }
489
+ else {
490
+ this._blocks.value = {
491
+ ...this._blocks.value,
492
+ [id]: block,
493
+ };
494
+ }
437
495
  block.model.created.next();
438
496
  if (block.model.role === 'root') {
439
497
  this.slots.rootAdded.next(id);
@@ -468,8 +526,14 @@ export class Store {
468
526
  model: block.model,
469
527
  isLocal,
470
528
  });
471
- const { [id]: _, ...blocks } = this._blocks.peek();
472
- this._blocks.value = blocks;
529
+ if (this._blocksDraft) {
530
+ delete this._blocksDraft[id];
531
+ this._blocksDraftChanged = true;
532
+ }
533
+ else {
534
+ const { [id]: _, ...blocks } = this._blocks.peek();
535
+ this._blocks.value = blocks;
536
+ }
473
537
  block.model.deleted.next();
474
538
  block.model.dispose();
475
539
  }
@@ -633,7 +697,7 @@ export class Store {
633
697
  * @category Block CRUD
634
698
  */
635
699
  getBlock(id) {
636
- return this._blocks.peek()[id];
700
+ return this._peekBlocks()[id];
637
701
  }
638
702
  /**
639
703
  * Gets a block by its ID
@@ -643,7 +707,9 @@ export class Store {
643
707
  * @category Block CRUD
644
708
  */
645
709
  getBlock$(id) {
646
- return this._blocks.value[id];
710
+ // Subscribes to the record, answers from the draft during a batch.
711
+ const blocks = this._blocks.value;
712
+ return (this._blocksDraft ?? blocks)[id];
647
713
  }
648
714
  /**
649
715
  * Get a model by its ID
@@ -664,7 +730,7 @@ export class Store {
664
730
  */
665
731
  getBlocksByFlavour(blockFlavour) {
666
732
  const flavours = typeof blockFlavour === 'string' ? [blockFlavour] : blockFlavour;
667
- return Object.values(this._blocks.peek()).filter(({ flavour }) => flavours.includes(flavour));
733
+ return Object.values(this._peekBlocks()).filter(({ flavour }) => flavours.includes(flavour));
668
734
  }
669
735
  /**
670
736
  * Get all models in the store
@@ -673,7 +739,7 @@ export class Store {
673
739
  * @category Block CRUD
674
740
  */
675
741
  getAllModels() {
676
- return Object.values(this._blocks.peek()).map(block => block.model);
742
+ return Object.values(this._peekBlocks()).map(block => block.model);
677
743
  }
678
744
  /**
679
745
  * Get all models of specified flavour(s)
@@ -697,7 +763,7 @@ export class Store {
697
763
  const parentId = this._crud.getParent(targetId);
698
764
  if (!parentId)
699
765
  return null;
700
- const parent = this._blocks.peek()[parentId];
766
+ const parent = this._peekBlocks()[parentId];
701
767
  if (!parent)
702
768
  return null;
703
769
  return parent.model;
@@ -750,7 +816,7 @@ export class Store {
750
816
  * @category Block CRUD
751
817
  */
752
818
  hasBlock(id) {
753
- return id in this._blocks.peek();
819
+ return id in this._peekBlocks();
754
820
  }
755
821
  /**
756
822
  * Move blocks to a new parent block
@@ -858,6 +924,7 @@ export class Store {
858
924
  this.slots.blockUpdated.complete();
859
925
  this.slots.yBlockUpdated.complete();
860
926
  this.disposableGroup.dispose();
927
+ this._crud.dispose();
861
928
  this._isDisposed = true;
862
929
  }
863
930
  _handleYBlockAdd(id, isLocal) {
@@ -10,6 +10,32 @@ export type TransformerOptions = {
10
10
  docCRUD: DocCRUD;
11
11
  middlewares?: TransformerMiddleware[];
12
12
  };
13
+ interface DraftBlockTreeNode {
14
+ draft: DraftModel;
15
+ snapshot: BlockSnapshot;
16
+ children: Array<DraftBlockTreeNode>;
17
+ }
18
+ /**
19
+ * A block snapshot converted to draft models and not written yet: what
20
+ * {@link Transformer.prepareSnapshotToBlock} returns and
21
+ * {@link Transformer.insertPrepared} writes. `id` is the id the root block
22
+ * will have, after the `beforeImport` middlewares (a replaced one).
23
+ */
24
+ export interface PreparedBlockSnapshot {
25
+ readonly id: string;
26
+ readonly tree: DraftBlockTreeNode;
27
+ readonly parent?: string;
28
+ readonly index?: number;
29
+ }
30
+ /**
31
+ * What {@link Transformer.insertPrepared} wrote. A block model only exists
32
+ * once the transaction that wrote it has ended, so the `afterImport` events
33
+ * wait for `flushAfterImport`, which returns the root model.
34
+ */
35
+ export interface InsertedBlockSnapshot {
36
+ readonly id: string;
37
+ flushAfterImport(): BlockModel | null;
38
+ }
13
39
  export declare class Transformer {
14
40
  private readonly _adapterConfigs;
15
41
  private readonly _transformerConfigs;
@@ -22,6 +48,24 @@ export declare class Transformer {
22
48
  docToSnapshot: (doc: Store) => DocSnapshot | undefined;
23
49
  sliceToSnapshot: (slice: Slice) => SliceSnapshot | undefined;
24
50
  snapshotToBlock: (snapshot: BlockSnapshot, doc: Store, parent?: string, index?: number) => Promise<BlockModel | undefined>;
51
+ /**
52
+ * The first half of {@link snapshotToBlock}: fires `beforeImport` and
53
+ * converts every node (the block transformers await their blobs here), and
54
+ * writes nothing. Pair it with {@link insertPrepared} on the SAME
55
+ * transformer: a middleware keeps its state per transformer
56
+ * (`replaceIdMiddleware`'s id map). Split so that a gesture can await every
57
+ * preparation first, then write them all in one synchronous transaction —
58
+ * one undo step whatever the preparation took (ADR 0042).
59
+ */
60
+ prepareSnapshotToBlock: (snapshot: BlockSnapshot, parent?: string, index?: number) => Promise<PreparedBlockSnapshot | undefined>;
61
+ /**
62
+ * The second half of {@link snapshotToBlock}: adds the prepared blocks to
63
+ * `doc` synchronously, without the yield every `BATCH_SIZE` blocks, so the
64
+ * caller may run it inside its own `store.transact`. The `afterImport`
65
+ * events (middlewares write props from them) wait for `flushAfterImport()`,
66
+ * to call once that transaction has ended and the models exist.
67
+ */
68
+ insertPrepared: (prepared: PreparedBlockSnapshot, doc: Store) => InsertedBlockSnapshot | undefined;
25
69
  snapshotToDoc: (snapshot: DocSnapshot) => Promise<Store | undefined>;
26
70
  snapshotToModelData: (snapshot: BlockSnapshot) => Promise<import("./base.js").SnapshotNode<object> | undefined>;
27
71
  snapshotToSlice: (snapshot: SliceSnapshot, doc: Store, parent?: string, index?: number) => Promise<Slice | undefined>;
@@ -40,9 +84,11 @@ export declare class Transformer {
40
84
  private _getSchema;
41
85
  private _getTransformer;
42
86
  private _insertBlockTree;
87
+ private _insertBlockTreeSync;
43
88
  private _rebuildBlockTree;
44
89
  private _snapshotToBlock;
45
90
  private _triggerBeforeImportEvent;
46
91
  reset(): void;
47
92
  [Symbol.dispose](): void;
48
93
  }
94
+ export {};
@@ -134,6 +134,65 @@ export class Transformer {
134
134
  return;
135
135
  }
136
136
  };
137
+ /**
138
+ * The first half of {@link snapshotToBlock}: fires `beforeImport` and
139
+ * converts every node (the block transformers await their blobs here), and
140
+ * writes nothing. Pair it with {@link insertPrepared} on the SAME
141
+ * transformer: a middleware keeps its state per transformer
142
+ * (`replaceIdMiddleware`'s id map). Split so that a gesture can await every
143
+ * preparation first, then write them all in one synchronous transaction —
144
+ * one undo step whatever the preparation took (ADR 0042).
145
+ */
146
+ this.prepareSnapshotToBlock = async (snapshot, parent, index) => {
147
+ try {
148
+ BlockSnapshotSchema.parse(snapshot);
149
+ this._triggerBeforeImportEvent(snapshot, parent, index);
150
+ const flatSnapshots = [];
151
+ this._flattenSnapshot(snapshot, flatSnapshots, parent, index);
152
+ const tree = await this._convertFlatSnapshots(flatSnapshots);
153
+ return { id: tree.draft.id, tree, parent, index };
154
+ }
155
+ catch (error) {
156
+ console.error(`Error when preparing snapshot to block:`);
157
+ console.error(error);
158
+ return;
159
+ }
160
+ };
161
+ /**
162
+ * The second half of {@link snapshotToBlock}: adds the prepared blocks to
163
+ * `doc` synchronously, without the yield every `BATCH_SIZE` blocks, so the
164
+ * caller may run it inside its own `store.transact`. The `afterImport`
165
+ * events (middlewares write props from them) wait for `flushAfterImport()`,
166
+ * to call once that transaction has ended and the models exist.
167
+ */
168
+ this.insertPrepared = (prepared, doc) => {
169
+ const inserted = [];
170
+ try {
171
+ this._insertBlockTreeSync([prepared.tree], doc, inserted, prepared.parent, prepared.index);
172
+ }
173
+ catch (error) {
174
+ console.error(`Error when inserting prepared snapshot:`);
175
+ console.error(error);
176
+ if (inserted.length === 0)
177
+ return;
178
+ }
179
+ return {
180
+ id: prepared.id,
181
+ flushAfterImport: () => {
182
+ for (const node of inserted) {
183
+ const model = doc.getBlock(node.draft.id)?.model;
184
+ if (!model)
185
+ continue;
186
+ this._slots.afterImport.next({
187
+ type: 'block',
188
+ model,
189
+ snapshot: node.snapshot,
190
+ });
191
+ }
192
+ return doc.getBlock(prepared.id)?.model ?? null;
193
+ },
194
+ };
195
+ };
137
196
  this.snapshotToDoc = async (snapshot) => {
138
197
  try {
139
198
  this._slots.beforeImport.next({
@@ -407,6 +466,17 @@ export class Transformer {
407
466
  }
408
467
  return counter;
409
468
  }
469
+ _insertBlockTreeSync(nodes, doc, inserted, parentId, startIndex) {
470
+ nodes.forEach((node, index) => {
471
+ const { id, flavour, props } = node.draft;
472
+ const actualIndex = startIndex !== undefined ? startIndex + index : undefined;
473
+ doc.addBlock(flavour, { id, ...props }, parentId, actualIndex);
474
+ inserted.push(node);
475
+ if (node.children.length > 0) {
476
+ this._insertBlockTreeSync(node.children, doc, inserted, id);
477
+ }
478
+ });
479
+ }
410
480
  _rebuildBlockTree(draftModels) {
411
481
  const nodeMap = new Map();
412
482
  // First pass: create nodes and add them to the map
@@ -436,6 +506,8 @@ export class Transformer {
436
506
  const flatSnapshots = [];
437
507
  this._flattenSnapshot(snapshot, flatSnapshots, parent, index);
438
508
  const blockTree = await this._convertFlatSnapshots(flatSnapshots);
509
+ // Not `insertPrepared`: an import may hold thousands of blocks, and this
510
+ // path yields to the main thread every BATCH_SIZE of them.
439
511
  await this._insertBlockTree([blockTree], doc, parent, index);
440
512
  return doc.getBlock(snapshot.id)?.model ?? null;
441
513
  }
package/dist/commands.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { frameCommands } from './_pkgs/affine-block-frame/index.js';
2
- import { copyLinkCommands, coreCommands, pageExportCommands, pivotCommands, readingCommands, tagCommands, } from './_pkgs/affine-block-root/index.js';
2
+ import { imageReplaceCommands } from './_pkgs/affine-block-image/index.js';
3
+ import { copyLinkCommands, coreCommands, invisibleCharactersCommands, pageExportCommands, pivotCommands, readingCommands, tagCommands, } from './_pkgs/affine-block-root/index.js';
3
4
  import { auditCommands, canvasDrawioExportCommands, canvasExportCommands, canvasGridCommands, canvasSvgExportCommands, exportSvgCommands, mapQualityCommands, selectionPaneCommands, userLayerCommands, } from './_pkgs/affine-block-surface/index.js';
4
5
  import { edgeDirectionCommands } from './_pkgs/affine-gfx-connector/index.js';
5
6
  import { shapeCommands } from './_pkgs/affine-gfx-shape/index.js';
@@ -50,6 +51,7 @@ export function getCommands(flags) {
50
51
  ...coreCommands,
51
52
  ...copyLinkCommands,
52
53
  ...pageExportCommands,
54
+ ...invisibleCharactersCommands,
53
55
  ...pivotCommands,
54
56
  ...tagCommands,
55
57
  ...readingCommands,
@@ -58,6 +60,9 @@ export function getCommands(flags) {
58
60
  ...exportSvgCommands,
59
61
  ...selectionPaneCommands,
60
62
  ...frameCommands,
63
+ // ADR 0044: registered by the image view extension, so they go with
64
+ // the `image` flag here too.
65
+ ...(isBlockEnabled(flags, 'image') ? imageReplaceCommands : []),
61
66
  ...canvasGridCommands,
62
67
  ...canvasExportCommands,
63
68
  ...canvasDrawioExportCommands,
@@ -29,6 +29,7 @@ import { PRESET_WORDINGS } from './_pkgs/affine-inline-preset/translations.js';
29
29
  import { REFERENCE_WORDINGS } from './_pkgs/affine-inline-reference/translations.js';
30
30
  import { CHROME_WORDINGS, } from './_pkgs/affine-shared/services/index.js';
31
31
  import { CALLOUT_WORDINGS } from './_pkgs/affine-block-callout/translations.js';
32
+ import { TABLE_WORDINGS } from './_pkgs/affine-block-table/translations.js';
32
33
  import { CODE_WORDINGS } from './_pkgs/affine-block-code/translations.js';
33
34
  import { LATEX_WORDINGS } from './_pkgs/affine-block-latex/translations.js';
34
35
  import { NOTE_WORDINGS } from './_pkgs/affine-block-note/translations.js';
@@ -295,6 +296,7 @@ const PACKAGE_WORDINGS = [
295
296
  CODE_WORDINGS,
296
297
  LATEX_WORDINGS,
297
298
  CALLOUT_WORDINGS,
299
+ TABLE_WORDINGS,
298
300
  PARAGRAPH_WORDINGS,
299
301
  ADAPTER_PANEL_WORDINGS,
300
302
  DOC_TITLE_WORDINGS,