tmux-ide 2.9.0-beta.5 → 2.9.0-beta.6
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/bin/cli.js +13 -6
- package/package.json +1 -1
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +24 -9
- package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +16 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +26 -10
- package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +21 -0
- package/packages/daemon-client/src/workspace-client.test.ts +56 -0
- package/packages/daemon-client/src/workspace-client.ts +16 -5
package/bin/cli.js
CHANGED
|
@@ -47379,8 +47379,13 @@ var init_terminal_replica_interpreter = __esm({
|
|
|
47379
47379
|
function boundedPositive(value) {
|
|
47380
47380
|
return Number.isSafeInteger(value) && value > 0 && value <= 65535;
|
|
47381
47381
|
}
|
|
47382
|
+
function nativeRowsMatchLease(lease, nativeRows) {
|
|
47383
|
+
if (lease.paneBorderStatus === "off") return lease.pane.height === nativeRows;
|
|
47384
|
+
const touchesStatusEdge = lease.paneBorderStatus === "top" ? lease.pane.top === 0 : lease.pane.top + lease.pane.height === lease.windowRows;
|
|
47385
|
+
return lease.pane.height === nativeRows + (touchesStatusEdge ? 1 : 0);
|
|
47386
|
+
}
|
|
47382
47387
|
function layoutLeaseEqual(left, right) {
|
|
47383
|
-
return left.semanticWindowId === right.semanticWindowId && left.
|
|
47388
|
+
return left.semanticWindowId === right.semanticWindowId && left.zoomed === right.zoomed && left.windowCols === right.windowCols && left.windowRows === right.windowRows && left.paneBorderStatus === right.paneBorderStatus && left.pane.semanticPaneId === right.pane.semanticPaneId && left.pane.left === right.pane.left && left.pane.top === right.pane.top && left.pane.width === right.pane.width && left.pane.height === right.pane.height;
|
|
47384
47389
|
}
|
|
47385
47390
|
var SessionRuntimeTerminalReplicaOwner;
|
|
47386
47391
|
var init_terminal_replica_owner = __esm({
|
|
@@ -47586,7 +47591,10 @@ var init_terminal_replica_owner = __esm({
|
|
|
47586
47591
|
rows: lease.pane.height,
|
|
47587
47592
|
chunks: reseed.chunks,
|
|
47588
47593
|
trace: reseed.trace,
|
|
47589
|
-
|
|
47594
|
+
// tmux can report x === cols while its terminal is in the
|
|
47595
|
+
// right-margin wrap-pending state. The canonical replica stores
|
|
47596
|
+
// a cell position, so project that state onto the final column.
|
|
47597
|
+
cursor: { x: Math.min(event.x, reseed.nativeCols - 1), y: event.y },
|
|
47590
47598
|
bootstrap: "painted-capture",
|
|
47591
47599
|
validateBeforeCommit: () => this.#leaseIsCurrent(lease, reseed.subscriptionEpoch),
|
|
47592
47600
|
onInvalidated: () => this.#retryReseedOrFault("terminal reseed layout lease crossed")
|
|
@@ -47666,9 +47674,8 @@ var init_terminal_replica_owner = __esm({
|
|
|
47666
47674
|
const lease = reseed.layoutLease;
|
|
47667
47675
|
if (!lease || !this.#leaseIsCurrent(lease, reseed.subscriptionEpoch)) return null;
|
|
47668
47676
|
if (lease.pane.width !== reseed.nativeCols) return null;
|
|
47669
|
-
|
|
47670
|
-
if (!
|
|
47671
|
-
if (!Number.isSafeInteger(cursorX) || !Number.isSafeInteger(cursorY) || cursorX < 0 || cursorY < 0 || cursorX >= reseed.nativeCols || cursorY >= reseed.nativeRows)
|
|
47677
|
+
if (!nativeRowsMatchLease(lease, reseed.nativeRows)) return null;
|
|
47678
|
+
if (!Number.isSafeInteger(cursorX) || !Number.isSafeInteger(cursorY) || cursorX < 0 || cursorY < 0 || cursorX > reseed.nativeCols || cursorY >= reseed.nativeRows)
|
|
47672
47679
|
return null;
|
|
47673
47680
|
return lease;
|
|
47674
47681
|
}
|
|
@@ -61987,7 +61994,7 @@ var require_package = __commonJS({
|
|
|
61987
61994
|
"package.json"(exports, module) {
|
|
61988
61995
|
module.exports = {
|
|
61989
61996
|
name: "tmux-ide",
|
|
61990
|
-
version: "2.9.0-beta.
|
|
61997
|
+
version: "2.9.0-beta.6",
|
|
61991
61998
|
description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
61992
61999
|
type: "module",
|
|
61993
62000
|
bin: {
|
package/package.json
CHANGED
|
@@ -220,7 +220,10 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
220
220
|
rows: lease.pane.height,
|
|
221
221
|
chunks: reseed.chunks,
|
|
222
222
|
trace: reseed.trace,
|
|
223
|
-
|
|
223
|
+
// tmux can report x === cols while its terminal is in the
|
|
224
|
+
// right-margin wrap-pending state. The canonical replica stores
|
|
225
|
+
// a cell position, so project that state onto the final column.
|
|
226
|
+
cursor: { x: Math.min(event.x, reseed.nativeCols - 1), y: event.y },
|
|
224
227
|
bootstrap: "painted-capture",
|
|
225
228
|
validateBeforeCommit: () => this.#leaseIsCurrent(lease, reseed.subscriptionEpoch),
|
|
226
229
|
onInvalidated: () => this.#retryReseedOrFault("terminal reseed layout lease crossed"),
|
|
@@ -317,16 +320,13 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
317
320
|
return null;
|
|
318
321
|
if (lease.pane.width !== reseed.nativeCols)
|
|
319
322
|
return null;
|
|
320
|
-
|
|
321
|
-
? lease.pane.height === reseed.nativeRows
|
|
322
|
-
: lease.pane.height === reseed.nativeRows + 1;
|
|
323
|
-
if (!rowsExact)
|
|
323
|
+
if (!nativeRowsMatchLease(lease, reseed.nativeRows))
|
|
324
324
|
return null;
|
|
325
325
|
if (!Number.isSafeInteger(cursorX) ||
|
|
326
326
|
!Number.isSafeInteger(cursorY) ||
|
|
327
327
|
cursorX < 0 ||
|
|
328
328
|
cursorY < 0 ||
|
|
329
|
-
cursorX
|
|
329
|
+
cursorX > reseed.nativeCols ||
|
|
330
330
|
cursorY >= reseed.nativeRows)
|
|
331
331
|
return null;
|
|
332
332
|
return lease;
|
|
@@ -371,9 +371,25 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
371
371
|
function boundedPositive(value) {
|
|
372
372
|
return Number.isSafeInteger(value) && value > 0 && value <= 65_535;
|
|
373
373
|
}
|
|
374
|
+
function nativeRowsMatchLease(lease, nativeRows) {
|
|
375
|
+
if (lease.paneBorderStatus === "off")
|
|
376
|
+
return lease.pane.height === nativeRows;
|
|
377
|
+
// pane-border-status consumes a terminal row only on the pane touching the
|
|
378
|
+
// configured outer window edge. Interior separators are drawn inside the
|
|
379
|
+
// layout and tmux reports their capture at the full visible pane height.
|
|
380
|
+
const touchesStatusEdge = lease.paneBorderStatus === "top"
|
|
381
|
+
? lease.pane.top === 0
|
|
382
|
+
: lease.pane.top + lease.pane.height === lease.windowRows;
|
|
383
|
+
return lease.pane.height === nativeRows + (touchesStatusEdge ? 1 : 0);
|
|
384
|
+
}
|
|
374
385
|
function layoutLeaseEqual(left, right) {
|
|
386
|
+
// Focus is ordinary mutable tmux state, not terminal geometry. A second
|
|
387
|
+
// attached client can switch the current window or active pane while an
|
|
388
|
+
// atomic capture is in flight without changing the bytes' dimensions or
|
|
389
|
+
// identity. Treating those flags as part of the capture lease made that
|
|
390
|
+
// harmless navigation invalidate bootstrap, eventually faulting the whole
|
|
391
|
+
// pane stream after the single reseed retry.
|
|
375
392
|
return (left.semanticWindowId === right.semanticWindowId &&
|
|
376
|
-
left.currentWindow === right.currentWindow &&
|
|
377
393
|
left.zoomed === right.zoomed &&
|
|
378
394
|
left.windowCols === right.windowCols &&
|
|
379
395
|
left.windowRows === right.windowRows &&
|
|
@@ -382,6 +398,5 @@ function layoutLeaseEqual(left, right) {
|
|
|
382
398
|
left.pane.left === right.pane.left &&
|
|
383
399
|
left.pane.top === right.pane.top &&
|
|
384
400
|
left.pane.width === right.pane.width &&
|
|
385
|
-
left.pane.height === right.pane.height
|
|
386
|
-
left.pane.active === right.pane.active);
|
|
401
|
+
left.pane.height === right.pane.height);
|
|
387
402
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { TerminalDeliveryAssembler, decodeVerifiedCompactSemanticTerminalUpdateCooperatively, decodeVerifiedLegacySemanticTerminalUpdate, terminalDeliveryEncodingAccepted, } from "@tmux-ide/core";
|
|
2
|
+
import { TerminalDeliveryAssembler, decodeVerifiedCompactSemanticTerminalUpdate, decodeVerifiedCompactSemanticTerminalUpdateCooperatively, decodeVerifiedLegacySemanticTerminalUpdate, terminalDeliveryEncodingAccepted, } from "@tmux-ide/core";
|
|
3
3
|
import { PaneStreamOperationError, } from "@tmux-ide/daemon-client/pane-stream-client";
|
|
4
4
|
import { createOpenTuiPaneStreamSocket } from "./open-tui-pane-stream-socket.js";
|
|
5
5
|
import { currentTuiPerformanceEventSink } from "./performance-events.js";
|
|
@@ -294,6 +294,21 @@ class WireTerminalEndpoint {
|
|
|
294
294
|
return consumed;
|
|
295
295
|
const bytes = assembler.complete();
|
|
296
296
|
if (envelope.encoding === "semantic-compact-v1") {
|
|
297
|
+
// Bootstrap every endpoint synchronously. Opening a workspace starts
|
|
298
|
+
// several cooperative decoders at once while live panes are already
|
|
299
|
+
// producing deltas; under sustained output one initial seed could be
|
|
300
|
+
// perpetually retired before it established a canonical baseline.
|
|
301
|
+
// Compact seeds are already size-bounded and decoding one atomically
|
|
302
|
+
// gives the endpoint the baseline needed for subsequent cooperative
|
|
303
|
+
// patches and their normal backpressure.
|
|
304
|
+
if (envelope.frame === "seed" && !this.#hasCanonicalSeed) {
|
|
305
|
+
const verified = decodeVerifiedCompactSemanticTerminalUpdate(bytes, this.#canonicalSnapshot, envelope.canonicalStateHash, {
|
|
306
|
+
grantReducerAdoption: true,
|
|
307
|
+
...(this.#compactDecodeProfile ? { onComplete: this.#compactDecodeProfile } : {}),
|
|
308
|
+
});
|
|
309
|
+
this.#acceptVerified(envelope, verified, performanceSink, parseStartedAt);
|
|
310
|
+
return consumed;
|
|
311
|
+
}
|
|
297
312
|
const token = Object.freeze({});
|
|
298
313
|
this.#decodeToken = token;
|
|
299
314
|
void this.#completeCooperativeCompactDecode(token, envelope, bytes, performanceSink, parseStartedAt);
|
|
@@ -298,7 +298,10 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
298
298
|
rows: lease.pane.height,
|
|
299
299
|
chunks: reseed.chunks,
|
|
300
300
|
trace: reseed.trace,
|
|
301
|
-
|
|
301
|
+
// tmux can report x === cols while its terminal is in the
|
|
302
|
+
// right-margin wrap-pending state. The canonical replica stores
|
|
303
|
+
// a cell position, so project that state onto the final column.
|
|
304
|
+
cursor: { x: Math.min(event.x, reseed.nativeCols - 1), y: event.y },
|
|
302
305
|
bootstrap: "painted-capture",
|
|
303
306
|
validateBeforeCommit: () => this.#leaseIsCurrent(lease, reseed.subscriptionEpoch),
|
|
304
307
|
onInvalidated: () => this.#retryReseedOrFault("terminal reseed layout lease crossed"),
|
|
@@ -399,17 +402,13 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
399
402
|
const lease = reseed.layoutLease;
|
|
400
403
|
if (!lease || !this.#leaseIsCurrent(lease, reseed.subscriptionEpoch)) return null;
|
|
401
404
|
if (lease.pane.width !== reseed.nativeCols) return null;
|
|
402
|
-
|
|
403
|
-
lease.paneBorderStatus === "off"
|
|
404
|
-
? lease.pane.height === reseed.nativeRows
|
|
405
|
-
: lease.pane.height === reseed.nativeRows + 1;
|
|
406
|
-
if (!rowsExact) return null;
|
|
405
|
+
if (!nativeRowsMatchLease(lease, reseed.nativeRows)) return null;
|
|
407
406
|
if (
|
|
408
407
|
!Number.isSafeInteger(cursorX) ||
|
|
409
408
|
!Number.isSafeInteger(cursorY) ||
|
|
410
409
|
cursorX < 0 ||
|
|
411
410
|
cursorY < 0 ||
|
|
412
|
-
cursorX
|
|
411
|
+
cursorX > reseed.nativeCols ||
|
|
413
412
|
cursorY >= reseed.nativeRows
|
|
414
413
|
)
|
|
415
414
|
return null;
|
|
@@ -477,13 +476,31 @@ function boundedPositive(value: number): boolean {
|
|
|
477
476
|
return Number.isSafeInteger(value) && value > 0 && value <= 65_535;
|
|
478
477
|
}
|
|
479
478
|
|
|
479
|
+
function nativeRowsMatchLease(lease: LayoutLease, nativeRows: number): boolean {
|
|
480
|
+
if (lease.paneBorderStatus === "off") return lease.pane.height === nativeRows;
|
|
481
|
+
|
|
482
|
+
// pane-border-status consumes a terminal row only on the pane touching the
|
|
483
|
+
// configured outer window edge. Interior separators are drawn inside the
|
|
484
|
+
// layout and tmux reports their capture at the full visible pane height.
|
|
485
|
+
const touchesStatusEdge =
|
|
486
|
+
lease.paneBorderStatus === "top"
|
|
487
|
+
? lease.pane.top === 0
|
|
488
|
+
: lease.pane.top + lease.pane.height === lease.windowRows;
|
|
489
|
+
return lease.pane.height === nativeRows + (touchesStatusEdge ? 1 : 0);
|
|
490
|
+
}
|
|
491
|
+
|
|
480
492
|
function layoutLeaseEqual(
|
|
481
493
|
left: LayoutLease | Omit<LayoutLease, "epoch">,
|
|
482
494
|
right: LayoutLease | Omit<LayoutLease, "epoch">,
|
|
483
495
|
): boolean {
|
|
496
|
+
// Focus is ordinary mutable tmux state, not terminal geometry. A second
|
|
497
|
+
// attached client can switch the current window or active pane while an
|
|
498
|
+
// atomic capture is in flight without changing the bytes' dimensions or
|
|
499
|
+
// identity. Treating those flags as part of the capture lease made that
|
|
500
|
+
// harmless navigation invalidate bootstrap, eventually faulting the whole
|
|
501
|
+
// pane stream after the single reseed retry.
|
|
484
502
|
return (
|
|
485
503
|
left.semanticWindowId === right.semanticWindowId &&
|
|
486
|
-
left.currentWindow === right.currentWindow &&
|
|
487
504
|
left.zoomed === right.zoomed &&
|
|
488
505
|
left.windowCols === right.windowCols &&
|
|
489
506
|
left.windowRows === right.windowRows &&
|
|
@@ -492,7 +509,6 @@ function layoutLeaseEqual(
|
|
|
492
509
|
left.pane.left === right.pane.left &&
|
|
493
510
|
left.pane.top === right.pane.top &&
|
|
494
511
|
left.pane.width === right.pane.width &&
|
|
495
|
-
left.pane.height === right.pane.height
|
|
496
|
-
left.pane.active === right.pane.active
|
|
512
|
+
left.pane.height === right.pane.height
|
|
497
513
|
);
|
|
498
514
|
}
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from "@tmux-ide/contracts";
|
|
20
20
|
import {
|
|
21
21
|
TerminalDeliveryAssembler,
|
|
22
|
+
decodeVerifiedCompactSemanticTerminalUpdate,
|
|
22
23
|
decodeVerifiedCompactSemanticTerminalUpdateCooperatively,
|
|
23
24
|
decodeVerifiedLegacySemanticTerminalUpdate,
|
|
24
25
|
terminalDeliveryEncodingAccepted,
|
|
@@ -474,6 +475,26 @@ class WireTerminalEndpoint {
|
|
|
474
475
|
if (chunk.index + 1 < envelope.chunkCount) return consumed;
|
|
475
476
|
const bytes = assembler.complete();
|
|
476
477
|
if (envelope.encoding === "semantic-compact-v1") {
|
|
478
|
+
// Bootstrap every endpoint synchronously. Opening a workspace starts
|
|
479
|
+
// several cooperative decoders at once while live panes are already
|
|
480
|
+
// producing deltas; under sustained output one initial seed could be
|
|
481
|
+
// perpetually retired before it established a canonical baseline.
|
|
482
|
+
// Compact seeds are already size-bounded and decoding one atomically
|
|
483
|
+
// gives the endpoint the baseline needed for subsequent cooperative
|
|
484
|
+
// patches and their normal backpressure.
|
|
485
|
+
if (envelope.frame === "seed" && !this.#hasCanonicalSeed) {
|
|
486
|
+
const verified = decodeVerifiedCompactSemanticTerminalUpdate(
|
|
487
|
+
bytes,
|
|
488
|
+
this.#canonicalSnapshot,
|
|
489
|
+
envelope.canonicalStateHash,
|
|
490
|
+
{
|
|
491
|
+
grantReducerAdoption: true,
|
|
492
|
+
...(this.#compactDecodeProfile ? { onComplete: this.#compactDecodeProfile } : {}),
|
|
493
|
+
},
|
|
494
|
+
);
|
|
495
|
+
this.#acceptVerified(envelope, verified, performanceSink, parseStartedAt);
|
|
496
|
+
return consumed;
|
|
497
|
+
}
|
|
477
498
|
const token = Object.freeze({});
|
|
478
499
|
this.#decodeToken = token;
|
|
479
500
|
void this.#completeCooperativeCompactDecode(
|
|
@@ -298,6 +298,32 @@ class FakeRuntime implements WorkspaceClientRuntimePort<string, string> {
|
|
|
298
298
|
}
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
class EagerSeedRuntime extends FakeRuntime {
|
|
302
|
+
constructor(
|
|
303
|
+
generation: string,
|
|
304
|
+
private readonly snapshot: string,
|
|
305
|
+
) {
|
|
306
|
+
super(generation);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
override async subscribeTerminal(
|
|
310
|
+
address: TerminalReplicaAddress,
|
|
311
|
+
): Promise<FakeTerminalSubscription> {
|
|
312
|
+
const subscription = new FakeTerminalSubscription(this.generation);
|
|
313
|
+
const onUpdate = subscription.onUpdate.bind(subscription);
|
|
314
|
+
subscription.onUpdate = (listener) => {
|
|
315
|
+
const unsubscribe = onUpdate(listener);
|
|
316
|
+
listener({
|
|
317
|
+
...terminalSeedFor(address.semanticPaneId, this.generation),
|
|
318
|
+
snapshot: this.snapshot,
|
|
319
|
+
});
|
|
320
|
+
return unsubscribe;
|
|
321
|
+
};
|
|
322
|
+
this.subscriptions.set(address.semanticPaneId, subscription);
|
|
323
|
+
return subscription;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
301
327
|
class ClosableFakeRuntime extends FakeRuntime {
|
|
302
328
|
private readonly closedState = deferred<void>();
|
|
303
329
|
override readonly closed = this.closedState.promise;
|
|
@@ -880,6 +906,36 @@ describe("WorkspaceClient", () => {
|
|
|
880
906
|
).toBe(true);
|
|
881
907
|
});
|
|
882
908
|
|
|
909
|
+
it("does not charge an eager canonical seed against the candidate update buffer", async () => {
|
|
910
|
+
const shell = shellBroker({ alpha: shellResource("alpha", ["pane.alpha"]) });
|
|
911
|
+
const runtime = new EagerSeedRuntime(ALPHA_DAEMON.instanceId, "x".repeat(8 * 1024 * 1024 + 1));
|
|
912
|
+
const publications: TerminalReplicaUpdate<string, string>[] = [];
|
|
913
|
+
const activations: FakeRuntime[] = [];
|
|
914
|
+
const client = createWorkspaceClient<string, string>({
|
|
915
|
+
target: target("alpha"),
|
|
916
|
+
ports: {
|
|
917
|
+
shell: shell.transport,
|
|
918
|
+
connectRuntime: async (_current, _inventory, _signal, prepare) => {
|
|
919
|
+
await prepare(runtime);
|
|
920
|
+
return runtime;
|
|
921
|
+
},
|
|
922
|
+
didActivateRuntime: (nextRuntime) => activations.push(nextRuntime as FakeRuntime),
|
|
923
|
+
actions,
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
client.subscribeTerminal({ workspaceName: "alpha", semanticPaneId: "pane.alpha" }, (update) =>
|
|
927
|
+
publications.push(update),
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
shell.connections[0]!.handlers.onVerifiedOpen();
|
|
931
|
+
await settle();
|
|
932
|
+
|
|
933
|
+
expect(activations).toEqual([runtime]);
|
|
934
|
+
expect(publications).toHaveLength(1);
|
|
935
|
+
expect(publications[0]?.type).toBe("terminal.seed");
|
|
936
|
+
await client.dispose();
|
|
937
|
+
});
|
|
938
|
+
|
|
883
939
|
it("does not reconnect when a shell refresh leaves runtime inventory unchanged", async () => {
|
|
884
940
|
const shell = shellBroker({ alpha: shellResource("alpha") });
|
|
885
941
|
const runtime = new FakeRuntime(ALPHA_DAEMON.instanceId);
|
|
@@ -777,11 +777,22 @@ export function createWorkspaceClient<
|
|
|
777
777
|
publishTerminalUpdate(interest, nextRuntime, update, metadata);
|
|
778
778
|
return;
|
|
779
779
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
780
|
+
// A seed is already retained by the candidate runtime as its
|
|
781
|
+
// canonical baseline. Buffering it here adds only one reference,
|
|
782
|
+
// so charging its fully expanded JSON representation can reject
|
|
783
|
+
// an otherwise bounded multi-pane bootstrap before activation.
|
|
784
|
+
// Patches and tombstones are additional retained payloads and
|
|
785
|
+
// continue to count against the byte budget.
|
|
786
|
+
let encodedBytes = 0;
|
|
787
|
+
if (update.type !== "terminal.seed") {
|
|
788
|
+
encodedBytes = MAX_PREPARED_TERMINAL_BYTES + 1;
|
|
789
|
+
try {
|
|
790
|
+
encodedBytes = UTF8_ENCODER.encode(
|
|
791
|
+
JSON.stringify({ update, metadata }),
|
|
792
|
+
).byteLength;
|
|
793
|
+
} catch {
|
|
794
|
+
// An unencodable canonical update cannot be admitted safely.
|
|
795
|
+
}
|
|
785
796
|
}
|
|
786
797
|
if (
|
|
787
798
|
prepared.bufferedUpdateCount + 1 > MAX_PREPARED_TERMINAL_UPDATES ||
|