@replohq/sandbox-runtime 1.19.4 → 1.19.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod";
2
+ import type { IframeRpcVersionMismatch } from "./iframeRpcChannel";
2
3
  export declare const EXTERNAL_NAVIGATION_IFRAME_RPC_CHANNEL_ID: "replo:external-navigation-iframe-rpc";
3
4
  export declare const EXTERNAL_NAVIGATION_IFRAME_RPC_CHANNEL_VERSION: 1;
4
5
  export declare const sandboxToParentExternalNavigationMethodConfig: {
@@ -11,7 +12,8 @@ export declare const sandboxToParentExternalNavigationMethodConfig: {
11
12
  };
12
13
  export declare const parentToSandboxExternalNavigationMethodConfig: {};
13
14
  /** Opens what the frame hands up in a new tab. Null `frameOrigin` accepts any origin. */
14
- export declare function createExternalNavigationParentChannel({ getFrameWindow, frameOrigin, }: {
15
+ export declare function createExternalNavigationParentChannel({ getFrameWindow, frameOrigin, onPeerVersionMismatch, }: {
15
16
  getFrameWindow: () => Window | null;
16
17
  frameOrigin: string | null;
18
+ onPeerVersionMismatch?: (mismatch: IframeRpcVersionMismatch) => void;
17
19
  }): import("./iframeRpcChannel").IframeRpcChannel<{}>;
@@ -1,4 +1,4 @@
1
- import { createIframeRpcChannel } from '../../chunk-36IPO47A.mjs';
1
+ import { createIframeRpcChannel } from '../../chunk-6WREP7C2.mjs';
2
2
  import '../../chunk-ZTT7SNYN.mjs';
3
3
  import '../../chunk-THOBCJJA.mjs';
4
4
  import '../../chunk-4ITJNSXZ.mjs';
@@ -17,7 +17,8 @@ var sandboxToParentExternalNavigationMethodConfig = {
17
17
  var parentToSandboxExternalNavigationMethodConfig = {};
18
18
  function createExternalNavigationParentChannel({
19
19
  getFrameWindow,
20
- frameOrigin
20
+ frameOrigin,
21
+ onPeerVersionMismatch
21
22
  }) {
22
23
  return createIframeRpcChannel({
23
24
  subscriberWindow: window,
@@ -30,6 +31,7 @@ function createExternalNavigationParentChannel({
30
31
  },
31
32
  outgoingMethods: parentToSandboxExternalNavigationMethodConfig,
32
33
  incomingMethods: sandboxToParentExternalNavigationMethodConfig,
34
+ onPeerVersionMismatch,
33
35
  handlers: {
34
36
  openExternal: ({ url: url2 }) => {
35
37
  window.open(url2, "_blank", "noopener,noreferrer");
@@ -50,6 +50,11 @@ type SubscriberWindow = {
50
50
  type TargetIframeWindow = {
51
51
  postMessage: (message: unknown, targetOrigin: string) => void;
52
52
  };
53
+ export type IframeRpcVersionMismatch = {
54
+ channelIdentifier: string;
55
+ ownVersion: number;
56
+ peerVersion: number;
57
+ };
53
58
  type IframeRpcChannelOptions<OutgoingMethods extends IframeRpcChannelMethodConfig, IncomingMethods extends IframeRpcChannelMethodConfig> = {
54
59
  subscriberWindow: SubscriberWindow;
55
60
  channelIdentifier: string;
@@ -60,6 +65,13 @@ type IframeRpcChannelOptions<OutgoingMethods extends IframeRpcChannelMethodConfi
60
65
  outgoingMethods: OutgoingMethods;
61
66
  incomingMethods: IncomingMethods;
62
67
  handlers: IframeRpcChannelHandlers<IncomingMethods, OutgoingMethods>;
68
+ /**
69
+ * Called for every message from the expected peer on another version of
70
+ * this channel. The message itself is dropped: neither side can parse the
71
+ * other's protocol, so recovering (e.g. reloading the older side) is the
72
+ * caller's job.
73
+ */
74
+ onPeerVersionMismatch?: (mismatch: IframeRpcVersionMismatch) => void;
63
75
  };
64
76
  export type IframeRpcChannel<OutgoingMethods extends IframeRpcChannelMethodConfig = IframeRpcChannelMethodConfig> = {
65
77
  subscribe: () => () => void;
@@ -1,4 +1,4 @@
1
- export { createIframeRpcChannel, createIframeRpcRemote, iframeRpcCallSchema } from '../../chunk-36IPO47A.mjs';
1
+ export { createIframeRpcChannel, createIframeRpcRemote, iframeRpcCallSchema } from '../../chunk-6WREP7C2.mjs';
2
2
  import '../../chunk-ZTT7SNYN.mjs';
3
3
  import '../../chunk-THOBCJJA.mjs';
4
4
  import '../../chunk-4ITJNSXZ.mjs';
@@ -1,4 +1,5 @@
1
1
  import * as z from "zod";
2
+ import type { IframeRpcVersionMismatch } from "./iframeRpcChannel";
2
3
  export declare const PREVIEW_HEALTH_IFRAME_RPC_CHANNEL_ID: "replo:preview-health-iframe-rpc";
3
4
  export declare const PREVIEW_HEALTH_IFRAME_RPC_CHANNEL_VERSION: 1;
4
5
  export declare const previewErrorDetailSchema: z.ZodObject<{
@@ -147,7 +148,7 @@ export declare const editorToPreviewHealthMethodConfig: {};
147
148
  * status from current runtimes and null from older runtimes, which only say
148
149
  * ok or error.
149
150
  */
150
- export declare function subscribeToPreviewHealth({ getFrameWindow, onDocumentArrived, onStatus, onDiagnostic, }: {
151
+ export declare function subscribeToPreviewHealth({ getFrameWindow, onDocumentArrived, onStatus, onDiagnostic, onPeerVersionMismatch, }: {
151
152
  getFrameWindow: () => Window | null;
152
153
  onDocumentArrived?: () => void;
153
154
  onStatus: (update: {
@@ -155,5 +156,6 @@ export declare function subscribeToPreviewHealth({ getFrameWindow, onDocumentArr
155
156
  report: PreviewStatusReport | null;
156
157
  }) => void;
157
158
  onDiagnostic?: (report: PreviewDiagnosticReport) => void;
159
+ onPeerVersionMismatch?: (mismatch: IframeRpcVersionMismatch) => void;
158
160
  }): () => void;
159
161
  export {};
@@ -1,4 +1,4 @@
1
- import { createIframeRpcChannel } from '../../chunk-36IPO47A.mjs';
1
+ import { createIframeRpcChannel } from '../../chunk-6WREP7C2.mjs';
2
2
  import '../../chunk-ZTT7SNYN.mjs';
3
3
  import '../../chunk-THOBCJJA.mjs';
4
4
  import '../../chunk-4ITJNSXZ.mjs';
@@ -71,7 +71,8 @@ function subscribeToPreviewHealth({
71
71
  getFrameWindow,
72
72
  onDocumentArrived,
73
73
  onStatus,
74
- onDiagnostic
74
+ onDiagnostic,
75
+ onPeerVersionMismatch
75
76
  }) {
76
77
  const isFromFrame = (event) => {
77
78
  const frameWindow = getFrameWindow();
@@ -86,6 +87,7 @@ function subscribeToPreviewHealth({
86
87
  isExpectedMessage: isFromFrame,
87
88
  outgoingMethods: editorToPreviewHealthMethodConfig,
88
89
  incomingMethods: previewHealthToEditorMethodConfig,
90
+ onPeerVersionMismatch,
89
91
  handlers: {
90
92
  documentArrived: () => {
91
93
  onDocumentArrived?.();
package/buildMetadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sandbox-runtime",
3
- "version": "1.19.4",
3
+ "version": "1.19.5",
4
4
  "branch": "HEAD",
5
- "commit": "3a362d942a24f8c7b0af6d26e3ccad003254d6c5",
6
- "builtAt": "2026-10-25T05:11:58.524Z"
5
+ "commit": "925967a151b3e53a1891d5d57b0cf956b0f64ffe",
6
+ "builtAt": "2026-10-28T00:16:14.622Z"
7
7
  };
@@ -252,7 +252,8 @@ function createIframeRpcChannel(options) {
252
252
  isExpectedMessage,
253
253
  outgoingMethods,
254
254
  incomingMethods,
255
- handlers
255
+ handlers,
256
+ onPeerVersionMismatch
256
257
  } = options;
257
258
  const pendingRequests = /* @__PURE__ */ new Map();
258
259
  let subscriberCount = 0;
@@ -380,7 +381,7 @@ function createIframeRpcChannel(options) {
380
381
  const handleMessage = (event) => {
381
382
  const parsedMetadata = z.object({
382
383
  channelIdentifier: z.literal(channelIdentifier),
383
- version: z.literal(version)
384
+ version: z.number()
384
385
  }).safeParse(event.data);
385
386
  if (!parsedMetadata.success) {
386
387
  return;
@@ -400,6 +401,15 @@ function createIframeRpcChannel(options) {
400
401
  });
401
402
  return;
402
403
  }
404
+ const peerVersion = parsedMetadata.data.version;
405
+ if (peerVersion !== version) {
406
+ onPeerVersionMismatch?.({
407
+ channelIdentifier,
408
+ ownVersion: version,
409
+ peerVersion
410
+ });
411
+ return;
412
+ }
403
413
  const parsedCall = iframeRpcCallSchema.safeParse(event.data);
404
414
  if (!parsedCall.success) {
405
415
  return;
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sandbox-runtime",
3
- "version": "1.19.4",
3
+ "version": "1.19.5",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.19.4",
6
+ "version": "1.19.5",
7
7
  "branch": "HEAD",
8
- "commit": "3a362d942a24f8c7b0af6d26e3ccad003254d6c5",
9
- "builtAt": "2026-10-25T05:11:58.524Z"
8
+ "commit": "925967a151b3e53a1891d5d57b0cf956b0f64ffe",
9
+ "builtAt": "2026-10-28T00:16:14.622Z"
10
10
  },
11
11
  "description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -219,24 +219,21 @@ function SectionCanvas() {
219
219
  const activeIndex = drag ? drag.index : hoveredIndex ?? selectedIndex;
220
220
  const activeSection = activeIndex === null ? void 0 : canvas.sections[activeIndex];
221
221
  const activeBox = activeIndex === null ? null : getSectionViewportBox(elementsByIndex.get(activeIndex));
222
- let overlay = null;
223
- if (activeSection && activeBox && !isMovePending) {
224
- overlay = /* @__PURE__ */ jsx(
225
- SectionControls,
226
- {
227
- index: activeSection.index,
228
- label: activeSection.label,
229
- isLocked: activeSection.isLocked,
230
- isFirst: activeSection.index === 0,
231
- isLast: activeSection.index === canvas.sections.length - 1,
232
- box: activeBox,
233
- visible,
234
- unit,
235
- isDragging: drag !== null,
236
- isPicking: typeof canvas.insertingAt === "number"
237
- }
238
- );
239
- }
222
+ const overlay = activeSection && activeBox && !isMovePending ? /* @__PURE__ */ jsx(
223
+ SectionControls,
224
+ {
225
+ index: activeSection.index,
226
+ label: activeSection.label,
227
+ isLocked: activeSection.isLocked,
228
+ isFirst: activeSection.index === 0,
229
+ isLast: activeSection.index === canvas.sections.length - 1,
230
+ box: activeBox,
231
+ visible,
232
+ unit,
233
+ isDragging: drag !== null,
234
+ isPicking: typeof canvas.insertingAt === "number"
235
+ }
236
+ ) : null;
240
237
  const markedGap = drag ? drag.gap : canvas.insertingAt;
241
238
  let gapMarker = null;
242
239
  const placed = typeof markedGap === "number" ? getPlacedSections({ canvas, elementsByIndex }) : [];
@@ -284,48 +281,45 @@ function SectionCanvas() {
284
281
  )
285
282
  ] });
286
283
  }
287
- let dragLayer = null;
288
- if (drag?.isActive && activeBox) {
289
- dragLayer = /* @__PURE__ */ jsxs(Fragment, { children: [
290
- /* @__PURE__ */ jsx(
291
- "div",
292
- {
293
- style: {
294
- position: "fixed",
295
- left: activeBox.left,
296
- top: activeBox.top,
297
- width: activeBox.right - activeBox.left,
298
- height: activeBox.bottom - activeBox.top,
299
- background: "rgba(255, 255, 255, 0.65)",
300
- outline: `${2 * unit}px dashed ${ACCENT}`,
301
- outlineOffset: -unit
302
- }
303
- }
304
- ),
305
- /* @__PURE__ */ jsx(
306
- "div",
307
- {
308
- "data-shape": "pill",
309
- style: {
310
- ...pillStyle({
311
- unit,
312
- left: Math.min(
313
- Math.max(drag.x, LABEL_HALF_WIDTH * unit),
314
- window.innerWidth - LABEL_HALF_WIDTH * unit
315
- ),
316
- top: drag.y + 20 * unit + PILL_HEIGHT * unit > window.innerHeight ? drag.y - 20 * unit : drag.y + 20 * unit
317
- }),
318
- maxWidth: 2 * LABEL_HALF_WIDTH * unit,
319
- display: "block",
320
- lineHeight: `${PILL_HEIGHT * unit}px`,
321
- overflow: "hidden",
322
- textOverflow: "ellipsis"
323
- },
324
- children: activeSection?.label ?? "Section"
284
+ const dragLayer = drag?.isActive && activeBox ? /* @__PURE__ */ jsxs(Fragment, { children: [
285
+ /* @__PURE__ */ jsx(
286
+ "div",
287
+ {
288
+ style: {
289
+ position: "fixed",
290
+ left: activeBox.left,
291
+ top: activeBox.top,
292
+ width: activeBox.right - activeBox.left,
293
+ height: activeBox.bottom - activeBox.top,
294
+ background: "rgba(255, 255, 255, 0.65)",
295
+ outline: `${2 * unit}px dashed ${ACCENT}`,
296
+ outlineOffset: -unit
325
297
  }
326
- )
327
- ] });
328
- }
298
+ }
299
+ ),
300
+ /* @__PURE__ */ jsx(
301
+ "div",
302
+ {
303
+ "data-shape": "pill",
304
+ style: {
305
+ ...pillStyle({
306
+ unit,
307
+ left: Math.min(
308
+ Math.max(drag.x, LABEL_HALF_WIDTH * unit),
309
+ window.innerWidth - LABEL_HALF_WIDTH * unit
310
+ ),
311
+ top: drag.y + 20 * unit + PILL_HEIGHT * unit > window.innerHeight ? drag.y - 20 * unit : drag.y + 20 * unit
312
+ }),
313
+ maxWidth: 2 * LABEL_HALF_WIDTH * unit,
314
+ display: "block",
315
+ lineHeight: `${PILL_HEIGHT * unit}px`,
316
+ overflow: "hidden",
317
+ textOverflow: "ellipsis"
318
+ },
319
+ children: activeSection?.label ?? "Section"
320
+ }
321
+ )
322
+ ] }) : null;
329
323
  return createPortal(
330
324
  /* @__PURE__ */ jsxs(
331
325
  "div",
@@ -528,17 +522,9 @@ function SectionControls({
528
522
  )
529
523
  ] });
530
524
  }
531
- const GRIP_DOTS = [
532
- [5, 3],
533
- [5, 8],
534
- [5, 13],
535
- [11, 3],
536
- [11, 8],
537
- [11, 13]
538
- ];
539
525
  function GripIcon({ size }) {
540
- return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 16 16", "aria-hidden": "true", children: GRIP_DOTS.map(([cx, cy]) => {
541
- return /* @__PURE__ */ jsx(
526
+ return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 16 16", "aria-hidden": "true", children: [5, 11].flatMap((cx) => {
527
+ return [3, 8, 13].map((cy) => /* @__PURE__ */ jsx(
542
528
  "circle",
543
529
  {
544
530
  cx,
@@ -547,7 +533,7 @@ function GripIcon({ size }) {
547
533
  fill: "currentColor"
548
534
  },
549
535
  `${cx}-${cy}`
550
- );
536
+ ));
551
537
  }) });
552
538
  }
553
539
  export {
@@ -1,7 +1,3 @@
1
- type SectionBox = {
2
- index: number;
3
- elements: HTMLElement[];
4
- };
5
1
  /** How far each section shifts once `from` moves to `to`, keeping the original gaps between slots. */
6
2
  export declare function getMovedSectionOffsets({ sections, from, to, }: {
7
3
  sections: readonly {
@@ -28,12 +24,17 @@ export declare function getSectionViewportBox(elements: readonly HTMLElement[] |
28
24
  * page never shows the old order in between.
29
25
  */
30
26
  export declare function startOptimisticSectionMove({ sections, from, to, onLanded, }: {
31
- sections: readonly SectionBox[];
27
+ sections: readonly {
28
+ index: number;
29
+ elements: readonly HTMLElement[];
30
+ }[];
32
31
  from: number;
33
32
  to: number;
34
33
  onLanded: () => void;
35
34
  }): {
36
35
  revert: () => void;
37
- settle: () => void;
36
+ settle: ({ afterMs }: {
37
+ afterMs: number;
38
+ }) => void;
39
+ land: () => void;
38
40
  };
39
- export {};
@@ -85,11 +85,11 @@ function startOptimisticSectionMove({
85
85
  const reordered = [...order];
86
86
  reordered.splice(fromPosition, 1);
87
87
  reordered.splice(getTargetPosition({ indices: order, from, to }), 0, from);
88
+ const textOf = (elements) => {
89
+ return elements.map((element) => element.textContent ?? "").join("\n");
90
+ };
88
91
  const textBySection = new Map(
89
- placed.map((section) => [
90
- section.index,
91
- section.elements.map((element) => element.textContent ?? "").join("\n")
92
- ])
92
+ placed.map((section) => [section.index, textOf(section.elements)])
93
93
  );
94
94
  const watchedSlots = placed.flatMap((section, slot) => {
95
95
  const expected = textBySection.get(reordered[slot]);
@@ -97,11 +97,13 @@ function startOptimisticSectionMove({
97
97
  });
98
98
  const containers = new Set(
99
99
  placed.flatMap((section) => {
100
- return section.elements.flatMap((element) => {
101
- return element.parentNode ? [element.parentNode] : [];
102
- });
100
+ return section.elements.flatMap((element) => element.parentNode ?? []);
103
101
  })
104
102
  );
103
+ const rootsInMovedOrder = reordered.flatMap((index) => {
104
+ const root = placed.find((section) => section.index === index)?.elements.find((element) => element.isConnected);
105
+ return root ? [root] : [];
106
+ });
105
107
  let isDone = false;
106
108
  let fallbackTimeout;
107
109
  const finish = ({ animate }) => {
@@ -128,20 +130,26 @@ function startOptimisticSectionMove({
128
130
  restoreTransitions();
129
131
  }
130
132
  };
131
- const hasLanded = (records) => {
132
- if (records.some((record) => {
133
- return record.type === "childList" && containers.has(record.target);
134
- })) {
135
- return true;
136
- }
137
- return watchedSlots.length > 0 && watchedSlots.every(({ elements, expected }) => {
138
- return elements.some((element) => !element.isConnected) || elements.map((element) => element.textContent ?? "").join("\n") === expected;
139
- });
133
+ const hasLanded = () => {
134
+ return (
135
+ rootsInMovedOrder.length >= 2 && rootsInMovedOrder.every((root, position) => {
136
+ const previous = rootsInMovedOrder[position - 1];
137
+ return root.isConnected && (!previous || Boolean(
138
+ previous.compareDocumentPosition(root) & Node.DOCUMENT_POSITION_FOLLOWING
139
+ ));
140
+ }) ||
141
+ rootsInMovedOrder.some((root) => !root.isConnected) || watchedSlots.length > 0 && watchedSlots.every(({ elements, expected }) => {
142
+ return elements.some((element) => !element.isConnected) || textOf(elements) === expected;
143
+ })
144
+ );
145
+ };
146
+ const land = () => {
147
+ finish({ animate: false });
148
+ onLanded();
140
149
  };
141
- const observer = new MutationObserver((records) => {
142
- if (hasLanded(records)) {
143
- finish({ animate: false });
144
- onLanded();
150
+ const observer = new MutationObserver(() => {
151
+ if (hasLanded()) {
152
+ land();
145
153
  }
146
154
  });
147
155
  for (const container of containers) {
@@ -153,16 +161,14 @@ function startOptimisticSectionMove({
153
161
  }
154
162
  return {
155
163
  revert: () => finish({ animate: true }),
156
- settle: () => {
164
+ settle: ({ afterMs }) => {
157
165
  if (isDone) {
158
166
  return;
159
167
  }
160
168
  clearTimeout(fallbackTimeout);
161
- fallbackTimeout = setTimeout(() => {
162
- finish({ animate: false });
163
- onLanded();
164
- }, 8e3);
165
- }
169
+ fallbackTimeout = setTimeout(land, afterMs);
170
+ },
171
+ land
166
172
  };
167
173
  }
168
174
  export {
@@ -1,4 +1,5 @@
1
1
  import type { SectionCanvasRequest, SectionCanvasState } from "../../_vendor/schemas/generated/selectModeIframeRpc";
2
+ import { startOptimisticSectionMove } from "./optimisticSectionMove";
2
3
  export type SectionDrag = {
3
4
  index: number;
4
5
  pointerId: number;
@@ -9,10 +10,9 @@ export type SectionDrag = {
9
10
  isActive: boolean;
10
11
  gap: number | null;
11
12
  };
12
- type PendingMove = {
13
+ type PendingMove = ReturnType<typeof startOptimisticSectionMove> & {
13
14
  moveId: string;
14
- settle: () => void;
15
- revert: () => void;
15
+ phase: "saving" | "refreshedWhileSaving" | "saved";
16
16
  };
17
17
  type SectionCanvasStoreState = {
18
18
  canvas: SectionCanvasState | null;
@@ -52,11 +52,10 @@ function getDropGap({
52
52
  getPlacedSections(state),
53
53
  state.canvas?.sections.length ?? 0
54
54
  );
55
- let nearest = null;
55
+ let nearest;
56
56
  for (const gap of gaps) {
57
- const distance = Math.abs(gap.y - y);
58
- if (!nearest || distance < nearest.distance) {
59
- nearest = { at: gap.at, distance };
57
+ if (!nearest || Math.abs(gap.y - y) < Math.abs(nearest.y - y)) {
58
+ nearest = gap;
60
59
  }
61
60
  }
62
61
  if (!nearest || nearest.at === index || nearest.at === index + 1) {
@@ -145,10 +144,19 @@ const sectionCanvasStore = createShallowStore(
145
144
  reportLayout(get());
146
145
  },
147
146
  handlePreviewRefresh: () => {
148
- if (!get().canvas || get().pendingMove) {
147
+ const { canvas, pendingMove } = get();
148
+ if (!canvas) {
149
149
  return;
150
150
  }
151
- void get().actions.resolveSections();
151
+ if (!pendingMove) {
152
+ void get().actions.resolveSections();
153
+ } else if (pendingMove.phase === "saved") {
154
+ pendingMove.land();
155
+ } else {
156
+ set({
157
+ pendingMove: { ...pendingMove, phase: "refreshedWhileSaving" }
158
+ });
159
+ }
152
160
  },
153
161
  bumpGeometry: () => {
154
162
  set((state) => ({ geometryVersion: state.geometryVersion + 1 }));
@@ -206,16 +214,14 @@ const sectionCanvasStore = createShallowStore(
206
214
  },
207
215
  move: ({ index, to }) => {
208
216
  const state = get();
209
- const sectionCount = state.canvas?.sections.length ?? 0;
210
- if (!state.canvas || state.canvas.isDisabled || state.pendingMove || state.canvas.sections[index]?.isLocked !== false || to === index || to < 0 || to >= sectionCount) {
217
+ if (!state.canvas || state.canvas.isDisabled || state.pendingMove || state.canvas.sections[index]?.isLocked !== false || to === index || to < 0 || to >= state.canvas.sections.length) {
211
218
  return;
212
219
  }
213
- moveCounter += 1;
214
- const moveId = `section-move-${Date.now()}-${moveCounter}`;
220
+ const moveId = `section-move-${Date.now()}-${++moveCounter}`;
215
221
  const optimistic = startOptimisticSectionMove({
216
222
  sections: state.canvas.sections.map((section) => ({
217
223
  index: section.index,
218
- elements: [...state.elementsByIndex.get(section.index) ?? []]
224
+ elements: state.elementsByIndex.get(section.index) ?? []
219
225
  })),
220
226
  from: index,
221
227
  to,
@@ -228,7 +234,7 @@ const sectionCanvasStore = createShallowStore(
228
234
  }
229
235
  });
230
236
  set({
231
- pendingMove: { moveId, ...optimistic },
237
+ pendingMove: { moveId, phase: "saving", ...optimistic },
232
238
  hoveredIndex: null
233
239
  });
234
240
  get().actions.request({ type: "move", moveId, index, to });
@@ -239,7 +245,9 @@ const sectionCanvasStore = createShallowStore(
239
245
  return;
240
246
  }
241
247
  if (outcome === "persisted") {
242
- pendingMove.settle();
248
+ const afterMs = pendingMove.phase === "refreshedWhileSaving" ? 8e3 : 2e4;
249
+ set({ pendingMove: { ...pendingMove, phase: "saved" } });
250
+ pendingMove.settle({ afterMs });
243
251
  return;
244
252
  }
245
253
  pendingMove.revert();
@@ -1,16 +1,14 @@
1
- import type { SelectModeSourceLocation } from "../../_vendor/schemas/generated/selectMode";
2
1
  import type { SectionCanvasSection } from "../../_vendor/schemas/generated/selectModeIframeRpc";
3
- type Resolve = (locations: readonly SelectModeSourceLocation[]) => Promise<SelectModeSourceLocation[]>;
2
+ import { symbolicateSourceLocations } from "../symbolicate";
4
3
  /**
5
4
  * The DOM elements each page section renders, found from source, not layout: an element belongs to
6
- * a section when a JSX call site in its owner chain lies in the page file inside that section's lines. Walks down from <body> a level at a time, symbolicating each level
7
- * in one request and stopping at the first element that matches, so a section's descendants are
8
- * never read.
5
+ * a section when a JSX call site in its owner chain lies in the page file inside that section's
6
+ * lines. Walks down from <body> a level at a time, symbolicating each level in one request and
7
+ * stopping at the first element that matches, so a section's descendants are never read.
9
8
  */
10
9
  export declare function findSectionElements({ root, pageFilePath, sections, resolve, }: {
11
10
  root: Element;
12
11
  pageFilePath: string;
13
12
  sections: readonly Pick<SectionCanvasSection, "index" | "startLine" | "endLine">[];
14
- resolve?: Resolve;
13
+ resolve?: typeof symbolicateSourceLocations;
15
14
  }): Promise<Map<number, HTMLElement[]>>;
16
- export {};
@@ -47,10 +47,9 @@ async function findSectionElements({
47
47
  }
48
48
  }
49
49
  if (pending.size > 0) {
50
- const keys = [...pending.keys()];
51
50
  const results = await resolve([...pending.values()]);
52
- keys.forEach((key, index) => {
53
- resolved.set(key, results[index] ?? pending.get(key));
51
+ [...pending].forEach(([key, location], index) => {
52
+ resolved.set(key, results[index] ?? location);
54
53
  });
55
54
  }
56
55
  const next = [];
@@ -58,15 +57,9 @@ async function findSectionElements({
58
57
  let section;
59
58
  for (const location of chain) {
60
59
  const original = resolved.get(locationKey(location));
61
- if (!original || !isSectionCanvasPageFile({
62
- filePath: original.filePath,
63
- pageFilePath
64
- })) {
65
- continue;
66
- }
67
- section = sections.find((candidate) => {
60
+ section = original && isSectionCanvasPageFile({ filePath: original.filePath, pageFilePath }) ? sections.find((candidate) => {
68
61
  return candidate.startLine <= original.lineNumber && original.lineNumber <= candidate.endLine;
69
- });
62
+ }) : void 0;
70
63
  if (section) {
71
64
  break;
72
65
  }