@1agh/maude 1.0.11 → 1.3.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/README.md +3 -1
- package/apps/studio/acp/bridge.ts +18 -13
- package/apps/studio/acp/index.ts +43 -7
- package/apps/studio/acp/transcript-io.ts +394 -0
- package/apps/studio/acp/transcript.ts +103 -38
- package/apps/studio/annotations-layer.tsx +191 -116
- package/apps/studio/annotations-model.ts +39 -0
- package/apps/studio/annotations-sync.ts +50 -0
- package/apps/studio/api.ts +821 -55
- package/apps/studio/bin/_import-figma.mjs +57 -11
- package/apps/studio/bin/_seed-fixture.mjs +163 -0
- package/apps/studio/bin/annotate.mjs +3 -1
- package/apps/studio/bin/server-up.sh +17 -2
- package/apps/studio/canvas-edit.ts +391 -7
- package/apps/studio/canvas-imports.ts +89 -0
- package/apps/studio/canvas-lib.tsx +82 -18
- package/apps/studio/canvas-notice-message.ts +16 -0
- package/apps/studio/canvas-notifications.tsx +19 -0
- package/apps/studio/canvas-shell.tsx +57 -6
- package/apps/studio/client/app.jsx +943 -248
- package/apps/studio/client/apply-edit-request.ts +55 -0
- package/apps/studio/client/export-center.jsx +125 -67
- package/apps/studio/client/github.js +38 -1
- package/apps/studio/client/index-loader.ts +76 -0
- package/apps/studio/client/panels/CloudBar.jsx +52 -6
- package/apps/studio/client/panels/GitPanel.jsx +103 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
- package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
- package/apps/studio/client/panels/SyncPanel.jsx +238 -4
- package/apps/studio/client/panels/TeamProjects.jsx +376 -0
- package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
- package/apps/studio/client/photo-knobs.jsx +3 -0
- package/apps/studio/client/report-bug.jsx +19 -1
- package/apps/studio/client/share-dialog.jsx +91 -0
- package/apps/studio/client/share-link.js +94 -0
- package/apps/studio/client/styles/3-shell-maude.css +69 -18
- package/apps/studio/client/styles/4-components.css +44 -1
- package/apps/studio/client/tree-row-menu.jsx +48 -3
- package/apps/studio/client/whats-new.jsx +26 -32
- package/apps/studio/cloud/endpoints.ts +132 -0
- package/apps/studio/collab/index.ts +4 -0
- package/apps/studio/collab/persistence.ts +137 -17
- package/apps/studio/collab/registry.ts +19 -21
- package/apps/studio/collab/room.ts +13 -2
- package/apps/studio/context.ts +59 -0
- package/apps/studio/debug-bundle.ts +13 -0
- package/apps/studio/dist/client.bundle.js +1911 -1879
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/sonner.js +1 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/export-dialog.tsx +54 -0
- package/apps/studio/exporters/degraded.ts +19 -3
- package/apps/studio/exporters/ghostscript.ts +194 -0
- package/apps/studio/exporters/pdf-fonts.ts +261 -0
- package/apps/studio/exporters/pdf.ts +152 -3
- package/apps/studio/exporters/remote.ts +36 -1
- package/apps/studio/fs-watch.ts +45 -1
- package/apps/studio/http.ts +296 -19
- package/apps/studio/inspect.ts +19 -0
- package/apps/studio/managed-projects.ts +150 -0
- package/apps/studio/mem.ts +45 -9
- package/apps/studio/notifications.tsx +292 -0
- package/apps/studio/runtime-bundle.ts +2 -0
- package/apps/studio/server.ts +69 -6
- package/apps/studio/sync/accepted-cold-start.ts +225 -0
- package/apps/studio/sync/accepted-link.ts +320 -0
- package/apps/studio/sync/action-stage.ts +343 -0
- package/apps/studio/sync/agent.ts +104 -45
- package/apps/studio/sync/asset-push.ts +1 -47
- package/apps/studio/sync/cell-file-events.ts +3 -0
- package/apps/studio/sync/codec.ts +192 -34
- package/apps/studio/sync/cold-start-apply.ts +7 -0
- package/apps/studio/sync/cold-start.ts +118 -22
- package/apps/studio/sync/comment-identity.ts +68 -0
- package/apps/studio/sync/connection-state.ts +15 -1
- package/apps/studio/sync/ctl-provider.ts +15 -2
- package/apps/studio/sync/document-discovery.ts +81 -0
- package/apps/studio/sync/file-ledger.ts +117 -0
- package/apps/studio/sync/file-membership.ts +10 -2
- package/apps/studio/sync/file-plane.ts +926 -47
- package/apps/studio/sync/index.ts +1786 -36
- package/apps/studio/sync/journal-client.ts +13 -1
- package/apps/studio/sync/migrate-seed.ts +150 -22
- package/apps/studio/sync/poke.ts +4 -2
- package/apps/studio/sync/presentation.ts +311 -5
- package/apps/studio/sync/projection.ts +820 -23
- package/apps/studio/sync/repeated-module.ts +73 -0
- package/apps/studio/sync/retry-after.ts +169 -0
- package/apps/studio/sync/revision-barrier.ts +129 -0
- package/apps/studio/sync/seed-progress.ts +218 -0
- package/apps/studio/sync/seed-repair.ts +46 -0
- package/apps/studio/sync/source-merge.ts +100 -0
- package/apps/studio/sync/source-ops.ts +289 -0
- package/apps/studio/sync/source-recovery.ts +70 -0
- package/apps/studio/sync/source-validation.ts +56 -0
- package/apps/studio/sync/status.ts +201 -6
- package/apps/studio/sync/transaction-client.ts +460 -0
- package/apps/studio/sync/transport-error.ts +111 -0
- package/apps/studio/sync/writer-registry.ts +236 -0
- package/apps/studio/text-caret.ts +35 -0
- package/apps/studio/undo-hud.tsx +9 -87
- package/apps/studio/use-canvas-media-drop.tsx +4 -39
- package/apps/studio/whats-new.json +173 -0
- package/cli/bin/maude.mjs +2 -0
- package/cli/commands/codex.mjs +191 -0
- package/cli/commands/harness.mjs +1051 -0
- package/cli/commands/help.mjs +13 -0
- package/cli/lib/design-link.mjs +190 -2
- package/cli/lib/harness/capabilities.mjs +302 -0
- package/cli/lib/harness/codex-runtime.mjs +892 -0
- package/cli/lib/harness/compatibility.mjs +27 -0
- package/cli/lib/harness/discover-claude.mjs +1393 -0
- package/cli/lib/harness/managed-state.mjs +1296 -0
- package/cli/lib/harness/model.mjs +86 -0
- package/cli/lib/harness/secrets.mjs +390 -0
- package/cli/lib/harness/targets/codex.mjs +1141 -0
- package/cli/lib/harness/targets/opencode.mjs +807 -0
- package/cli/lib/harness/transaction.mjs +1099 -0
- package/cli/lib/hubs-config.mjs +35 -8
- package/cli/templates/harness/opencode/maude-projector.ts +58 -0
- package/docs/harness-capability-matrix.md +125 -0
- package/docs/harness-environment-projection.md +95 -0
- package/package.json +33 -9
- package/plugins/design/dependencies.json +3 -3
- package/plugins/design/templates/_shell.html +47 -0
- package/plugins/flow/.claude-plugin/config.schema.json +8 -0
- package/plugins/flow/dependencies.json +3 -3
- package/apps/studio/.ai/cache/_stats.json +0 -7
- package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
- package/apps/studio/bin/_curl-local.test.mjs +0 -280
- package/apps/studio/bin/_pw-launch.test.mjs +0 -90
- package/apps/studio/bin/_smart-frames.test.mjs +0 -195
- package/apps/studio/bin/_transcribe.test.mjs +0 -80
- package/apps/studio/bun.lock +0 -928
- package/apps/studio/draw/test/animate.test.ts +0 -172
- package/apps/studio/draw/test/brush.test.ts +0 -213
- package/apps/studio/draw/test/composition.test.ts +0 -141
- package/apps/studio/draw/test/geometry.test.ts +0 -199
- package/apps/studio/draw/test/gradient.test.ts +0 -167
- package/apps/studio/draw/test/layout.test.ts +0 -120
- package/apps/studio/draw/test/morph.test.ts +0 -107
- package/apps/studio/draw/test/optimize.test.ts +0 -40
- package/apps/studio/draw/test/palette.test.ts +0 -123
- package/apps/studio/draw/test/primitives.test.ts +0 -129
- package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
- package/apps/studio/draw/test/serialize.test.ts +0 -105
- package/apps/studio/figma/assets.test.ts +0 -464
- package/apps/studio/figma/client.test.ts +0 -395
- package/apps/studio/figma/codegen-client.test.ts +0 -276
- package/apps/studio/figma/codegen-fonts.test.ts +0 -103
- package/apps/studio/figma/codegen-values.test.ts +0 -179
- package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
- package/apps/studio/figma/fig-decode.test.ts +0 -788
- package/apps/studio/figma/fig-differential.test.ts +0 -182
- package/apps/studio/figma/fig-translator.test.ts +0 -192
- package/apps/studio/figma/fig-vector.test.ts +0 -113
- package/apps/studio/figma/from-codegen.test.ts +0 -408
- package/apps/studio/figma/sanitize.test.ts +0 -325
- package/apps/studio/figma/tailwind-map.test.ts +0 -142
- package/apps/studio/figma/to-artboard.test.ts +0 -808
- package/apps/studio/figma/to-render.test.ts +0 -180
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
- package/apps/studio/figma/to-strokes.test.ts +0 -705
- package/apps/studio/figma/to-tokens.test.ts +0 -321
- package/apps/studio/figma/url.test.ts +0 -167
- package/apps/studio/footage/schema.test.ts +0 -287
- package/apps/studio/generation/adapters/groq.test.ts +0 -80
- package/apps/studio/generation/audio-library.test.ts +0 -95
- package/apps/studio/generation/captions.test.ts +0 -78
- package/apps/studio/generation/elevenlabs.test.ts +0 -223
- package/apps/studio/generation/gemini.test.ts +0 -350
- package/apps/studio/generation/jobs.test.ts +0 -128
- package/apps/studio/generation/keys.test.ts +0 -75
- package/apps/studio/generation/prefs.test.ts +0 -95
- package/apps/studio/generation/types.test.ts +0 -123
- package/apps/studio/generation/whisper-models.test.ts +0 -179
- package/apps/studio/test/_helpers.ts +0 -108
- package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
- package/apps/studio/test/acp-activity.test.ts +0 -154
- package/apps/studio/test/acp-ai-activity.test.ts +0 -182
- package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
- package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
- package/apps/studio/test/acp-branch-guard.test.ts +0 -123
- package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
- package/apps/studio/test/acp-bridge.test.ts +0 -545
- package/apps/studio/test/acp-capabilities.test.ts +0 -123
- package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
- package/apps/studio/test/acp-commands.test.ts +0 -119
- package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
- package/apps/studio/test/acp-elicitation.test.ts +0 -251
- package/apps/studio/test/acp-env.test.ts +0 -95
- package/apps/studio/test/acp-mode-banner.test.ts +0 -45
- package/apps/studio/test/acp-origin-gate.test.ts +0 -160
- package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
- package/apps/studio/test/acp-permission.test.ts +0 -278
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
- package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
- package/apps/studio/test/acp-session-plugins.test.ts +0 -209
- package/apps/studio/test/acp-toolgroup.test.ts +0 -76
- package/apps/studio/test/acp-transcript-view.test.ts +0 -71
- package/apps/studio/test/acp-transcript.test.ts +0 -238
- package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
- package/apps/studio/test/acp-usage.test.ts +0 -143
- package/apps/studio/test/acp-write-gate.test.ts +0 -323
- package/apps/studio/test/acp-write-scope.test.ts +0 -533
- package/apps/studio/test/active-state.test.ts +0 -186
- package/apps/studio/test/activity.test.ts +0 -189
- package/apps/studio/test/ai-activity.test.ts +0 -113
- package/apps/studio/test/annotate-write.test.ts +0 -1074
- package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
- package/apps/studio/test/annotations-align.test.ts +0 -88
- package/apps/studio/test/annotations-api.test.ts +0 -145
- package/apps/studio/test/annotations-bindings.test.ts +0 -262
- package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
- package/apps/studio/test/annotations-groups.test.ts +0 -231
- package/apps/studio/test/annotations-layer.test.ts +0 -1011
- package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
- package/apps/studio/test/annotations-snap.test.ts +0 -135
- package/apps/studio/test/annotations-text-format.test.ts +0 -396
- package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
- package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
- package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
- package/apps/studio/test/artboard-kinds.test.tsx +0 -83
- package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
- package/apps/studio/test/asset-api.test.ts +0 -149
- package/apps/studio/test/assets-s3.test.ts +0 -249
- package/apps/studio/test/binary-smoke.test.ts +0 -47
- package/apps/studio/test/boot-self-heal.test.ts +0 -87
- package/apps/studio/test/browse-posture.test.tsx +0 -107
- package/apps/studio/test/bundle-smoke.test.ts +0 -46
- package/apps/studio/test/camera-reveal.test.tsx +0 -173
- package/apps/studio/test/canvas-artifacts.test.ts +0 -125
- package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
- package/apps/studio/test/canvas-build.test.ts +0 -141
- package/apps/studio/test/canvas-create-api.test.ts +0 -676
- package/apps/studio/test/canvas-cursors.test.ts +0 -164
- package/apps/studio/test/canvas-edit.test.ts +0 -470
- package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
- package/apps/studio/test/canvas-header.test.ts +0 -127
- package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
- package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
- package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
- package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
- package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
- package/apps/studio/test/canvas-list-watch.test.ts +0 -388
- package/apps/studio/test/canvas-media-drop.test.ts +0 -187
- package/apps/studio/test/canvas-meta-api.test.ts +0 -707
- package/apps/studio/test/canvas-move-api.test.ts +0 -587
- package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
- package/apps/studio/test/canvas-pipeline.test.ts +0 -180
- package/apps/studio/test/canvas-rects.test.ts +0 -198
- package/apps/studio/test/canvas-reorder.test.ts +0 -211
- package/apps/studio/test/canvas-route.test.ts +0 -179
- package/apps/studio/test/canvas-shell-base.test.ts +0 -107
- package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
- package/apps/studio/test/canvas-url.test.ts +0 -138
- package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
- package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
- package/apps/studio/test/capture-core.test.ts +0 -32
- package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
- package/apps/studio/test/chat-attachments.test.ts +0 -117
- package/apps/studio/test/chat-context.test.ts +0 -129
- package/apps/studio/test/chat-markdown.test.tsx +0 -58
- package/apps/studio/test/clip-addressing.test.ts +0 -738
- package/apps/studio/test/clip-ops.test.ts +0 -922
- package/apps/studio/test/cloud-build.test.ts +0 -95
- package/apps/studio/test/cloud-connect-note.test.ts +0 -135
- package/apps/studio/test/cloud-endpoints.test.ts +0 -590
- package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
- package/apps/studio/test/cloud-history-posture.test.ts +0 -230
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
- package/apps/studio/test/cloud-renew.test.ts +0 -205
- package/apps/studio/test/cloud-session-role.test.ts +0 -161
- package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
- package/apps/studio/test/cold-start-apply.test.ts +0 -303
- package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
- package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
- package/apps/studio/test/collab-bridge.test.ts +0 -79
- package/apps/studio/test/collab-loopback.test.ts +0 -63
- package/apps/studio/test/collab-origin-gate.test.ts +0 -323
- package/apps/studio/test/collab-protocol.test.ts +0 -146
- package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
- package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
- package/apps/studio/test/collab-room.test.ts +0 -188
- package/apps/studio/test/collab-session-survival.test.tsx +0 -176
- package/apps/studio/test/collab-stress.test.ts +0 -129
- package/apps/studio/test/comment-mount.test.ts +0 -97
- package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
- package/apps/studio/test/comments-api.test.ts +0 -277
- package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
- package/apps/studio/test/comments-overlay.test.ts +0 -117
- package/apps/studio/test/compile-entry.test.ts +0 -133
- package/apps/studio/test/component-map.test.ts +0 -48
- package/apps/studio/test/config-projection.test.ts +0 -104
- package/apps/studio/test/config-reload.test.ts +0 -230
- package/apps/studio/test/config-version.test.ts +0 -88
- package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
- package/apps/studio/test/convert-to-absolute.test.ts +0 -333
- package/apps/studio/test/credential-grammar.test.ts +0 -107
- package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
- package/apps/studio/test/csrf-write-guard.test.ts +0 -141
- package/apps/studio/test/debug-bundle.test.ts +0 -105
- package/apps/studio/test/design-setup-readiness.test.ts +0 -155
- package/apps/studio/test/detach-component.test.ts +0 -94
- package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
- package/apps/studio/test/dom-selection.test.ts +0 -198
- package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
- package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
- package/apps/studio/test/edit-persistence.test.ts +0 -91
- package/apps/studio/test/edit-scope-api.test.ts +0 -119
- package/apps/studio/test/edit-source-command.test.ts +0 -142
- package/apps/studio/test/edit-suppress.test.ts +0 -170
- package/apps/studio/test/editing-presence.test.ts +0 -103
- package/apps/studio/test/element-resize.test.ts +0 -136
- package/apps/studio/test/element-structural-api.test.ts +0 -434
- package/apps/studio/test/element-structural-edit.test.ts +0 -750
- package/apps/studio/test/ensure-browser.test.ts +0 -92
- package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
- package/apps/studio/test/export-assemble.test.ts +0 -53
- package/apps/studio/test/export-browser-lane.test.ts +0 -126
- package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
- package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
- package/apps/studio/test/export-center.test.tsx +0 -287
- package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
- package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
- package/apps/studio/test/export-lane.test.ts +0 -245
- package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
- package/apps/studio/test/exporters/canva.test.ts +0 -63
- package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
- package/apps/studio/test/exporters/endpoint.test.ts +0 -121
- package/apps/studio/test/exporters/history.test.ts +0 -107
- package/apps/studio/test/exporters/html.test.ts +0 -26
- package/apps/studio/test/exporters/jobs.test.ts +0 -342
- package/apps/studio/test/exporters/pdf.test.ts +0 -85
- package/apps/studio/test/exporters/png.test.ts +0 -103
- package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
- package/apps/studio/test/exporters/pptx.test.ts +0 -31
- package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
- package/apps/studio/test/exporters/runtime.test.ts +0 -59
- package/apps/studio/test/exporters/scope.test.ts +0 -292
- package/apps/studio/test/exporters/svg.test.ts +0 -29
- package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
- package/apps/studio/test/exporters/zip.test.ts +0 -104
- package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
- package/apps/studio/test/figjam-v3-model.test.ts +0 -342
- package/apps/studio/test/figma-explode.test.ts +0 -438
- package/apps/studio/test/figma-provenance.test.ts +0 -108
- package/apps/studio/test/figma-routes.test.ts +0 -294
- package/apps/studio/test/file-lock.test.ts +0 -84
- package/apps/studio/test/fixtures/backgrounding-claude-auth.sh +0 -12
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
- package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
- package/apps/studio/test/fixtures/noisy-claude-auth.mjs +0 -27
- package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
- package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
- package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
- package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
- package/apps/studio/test/fixtures/wedged-claude-auth.sh +0 -11
- package/apps/studio/test/footage-store.test.ts +0 -100
- package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
- package/apps/studio/test/fs-watch.test.ts +0 -41
- package/apps/studio/test/gemma-models.test.ts +0 -245
- package/apps/studio/test/generate-route.test.ts +0 -106
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +0 -341
- package/apps/studio/test/git-cloud-posture.test.ts +0 -59
- package/apps/studio/test/git-grouping.test.ts +0 -106
- package/apps/studio/test/git-lifecycle.test.ts +0 -100
- package/apps/studio/test/git-log-format.test.ts +0 -95
- package/apps/studio/test/git-safe-rel.test.ts +0 -132
- package/apps/studio/test/git-system-engine.test.ts +0 -135
- package/apps/studio/test/git-watch.test.ts +0 -97
- package/apps/studio/test/github-api.test.ts +0 -577
- package/apps/studio/test/grid-track-handles.test.ts +0 -160
- package/apps/studio/test/handoff-static-frames.test.ts +0 -215
- package/apps/studio/test/handoff.test.ts +0 -329
- package/apps/studio/test/history-rollback.test.ts +0 -88
- package/apps/studio/test/hmr-broadcast.test.ts +0 -236
- package/apps/studio/test/hmr-classify.test.ts +0 -87
- package/apps/studio/test/hub-link.test.ts +0 -80
- package/apps/studio/test/identity-cache.test.ts +0 -83
- package/apps/studio/test/import-asset-browser.test.ts +0 -64
- package/apps/studio/test/import-asset-route.test.ts +0 -71
- package/apps/studio/test/import-asset.test.ts +0 -427
- package/apps/studio/test/import-brand-browser.test.ts +0 -88
- package/apps/studio/test/import-brand-route.test.ts +0 -120
- package/apps/studio/test/import-brand.test.ts +0 -206
- package/apps/studio/test/import-figma.test.ts +0 -667
- package/apps/studio/test/import-tokens.test.ts +0 -722
- package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
- package/apps/studio/test/input-router.test.ts +0 -757
- package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
- package/apps/studio/test/inspect-selections.test.ts +0 -219
- package/apps/studio/test/knob-props-authored.test.ts +0 -87
- package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
- package/apps/studio/test/locator.test.ts +0 -214
- package/apps/studio/test/marquee-overlay.test.ts +0 -94
- package/apps/studio/test/media-commit-chain.test.ts +0 -210
- package/apps/studio/test/mode-toggle.test.tsx +0 -226
- package/apps/studio/test/motion-sample.test.ts +0 -35
- package/apps/studio/test/move-artboards-command.test.ts +0 -108
- package/apps/studio/test/participants-chrome.test.ts +0 -65
- package/apps/studio/test/paths.test.ts +0 -88
- package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
- package/apps/studio/test/perf-harness.ts +0 -193
- package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
- package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
- package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
- package/apps/studio/test/photo-edit-api.test.ts +0 -201
- package/apps/studio/test/photo-filters.test.ts +0 -149
- package/apps/studio/test/photo-pipeline.test.ts +0 -106
- package/apps/studio/test/photo-taxonomy.test.ts +0 -96
- package/apps/studio/test/print-marks.test.ts +0 -113
- package/apps/studio/test/print-units.test.ts +0 -173
- package/apps/studio/test/read-annotations.test.ts +0 -727
- package/apps/studio/test/read-only-gate.test.ts +0 -188
- package/apps/studio/test/read-only-role.test.ts +0 -95
- package/apps/studio/test/readiness.test.ts +0 -337
- package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
- package/apps/studio/test/reorder-api.test.ts +0 -210
- package/apps/studio/test/repo-concurrency.test.ts +0 -146
- package/apps/studio/test/repo-lock.test.ts +0 -350
- package/apps/studio/test/report-proxy.test.ts +0 -125
- package/apps/studio/test/ripple.test.ts +0 -197
- package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
- package/apps/studio/test/runtime-bundle.test.ts +0 -69
- package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
- package/apps/studio/test/scaffold-design.test.ts +0 -122
- package/apps/studio/test/server-lifecycle.test.ts +0 -38
- package/apps/studio/test/session-runtime-state.test.ts +0 -226
- package/apps/studio/test/session-scope.test.ts +0 -100
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
- package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
- package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
- package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
- package/apps/studio/test/shared-doc-projection.test.ts +0 -238
- package/apps/studio/test/shell-importmap.test.ts +0 -56
- package/apps/studio/test/sizing-mode.test.ts +0 -102
- package/apps/studio/test/slash-commands.test.ts +0 -117
- package/apps/studio/test/snap-distance-pill.test.ts +0 -68
- package/apps/studio/test/spacing-handles.test.ts +0 -138
- package/apps/studio/test/specimen-select.test.ts +0 -88
- package/apps/studio/test/sync-agent.test.ts +0 -660
- package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
- package/apps/studio/test/sync-asset-push.test.ts +0 -931
- package/apps/studio/test/sync-atomic-write.test.ts +0 -62
- package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
- package/apps/studio/test/sync-autocommit.test.ts +0 -537
- package/apps/studio/test/sync-canvas-path.test.ts +0 -200
- package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
- package/apps/studio/test/sync-codec.test.ts +0 -230
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
- package/apps/studio/test/sync-cold-start.test.ts +0 -304
- package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
- package/apps/studio/test/sync-connection-state.test.ts +0 -337
- package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
- package/apps/studio/test/sync-decide-file.test.ts +0 -420
- package/apps/studio/test/sync-doc-name.test.ts +0 -281
- package/apps/studio/test/sync-echo-guard.test.ts +0 -96
- package/apps/studio/test/sync-file-ledger.test.ts +0 -334
- package/apps/studio/test/sync-file-membership.test.ts +0 -347
- package/apps/studio/test/sync-file-plane.test.ts +0 -976
- package/apps/studio/test/sync-file-pull.test.ts +0 -333
- package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
- package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
- package/apps/studio/test/sync-hardening.test.ts +0 -586
- package/apps/studio/test/sync-hub-listing.test.ts +0 -46
- package/apps/studio/test/sync-hubs-config.test.ts +0 -133
- package/apps/studio/test/sync-incident-replay.test.ts +0 -211
- package/apps/studio/test/sync-journal.test.ts +0 -176
- package/apps/studio/test/sync-meta-codec.test.ts +0 -199
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
- package/apps/studio/test/sync-move-retirement.test.ts +0 -231
- package/apps/studio/test/sync-panel-surface.test.ts +0 -153
- package/apps/studio/test/sync-path-pull.test.ts +0 -531
- package/apps/studio/test/sync-presentation.test.ts +0 -262
- package/apps/studio/test/sync-pull-budget.test.ts +0 -169
- package/apps/studio/test/sync-remote-docs.test.ts +0 -333
- package/apps/studio/test/sync-resync-routes.test.ts +0 -168
- package/apps/studio/test/sync-runtime.test.ts +0 -1675
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
- package/apps/studio/test/sync-settings-routes.test.ts +0 -195
- package/apps/studio/test/sync-settings.test.ts +0 -151
- package/apps/studio/test/sync-status.test.ts +0 -261
- package/apps/studio/test/sync-supervisor.test.ts +0 -262
- package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
- package/apps/studio/test/sync-trash.test.ts +0 -132
- package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
- package/apps/studio/test/sync-untrusted.test.ts +0 -103
- package/apps/studio/test/system-endpoint.test.ts +0 -144
- package/apps/studio/test/text-editability-stamp.test.ts +0 -131
- package/apps/studio/test/timeline-comp-target.test.ts +0 -139
- package/apps/studio/test/timeline-media-cache.test.ts +0 -41
- package/apps/studio/test/timeline-parse.test.ts +0 -184
- package/apps/studio/test/timeline-scale.test.ts +0 -65
- package/apps/studio/test/timeline-snap.test.ts +0 -85
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
- package/apps/studio/test/tool-palette.test.tsx +0 -55
- package/apps/studio/test/tour-overlay.test.tsx +0 -90
- package/apps/studio/test/trusted-request-host.test.ts +0 -66
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
- package/apps/studio/test/undo-stack.test.ts +0 -244
- package/apps/studio/test/use-agent-presence.test.tsx +0 -117
- package/apps/studio/test/use-annotation-resize.test.ts +0 -402
- package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
- package/apps/studio/test/use-artboard-drag.test.ts +0 -324
- package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
- package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
- package/apps/studio/test/use-selection-set.test.tsx +0 -166
- package/apps/studio/test/use-snap-guides.test.ts +0 -271
- package/apps/studio/test/use-tool-mode.test.tsx +0 -186
- package/apps/studio/test/use-undo-stack.test.tsx +0 -223
- package/apps/studio/test/video-asset.test.ts +0 -163
- package/apps/studio/test/video-comp-fixture.test.ts +0 -50
- package/apps/studio/test/video-comp.test.ts +0 -270
- package/apps/studio/test/video-encode-lib.test.ts +0 -63
- package/apps/studio/test/video-render-bridge.test.ts +0 -171
- package/apps/studio/test/whats-new.test.ts +0 -113
- package/apps/studio/test/whisper-setup.test.ts +0 -97
- package/apps/studio/test/workspace-containment.test.ts +0 -382
- package/apps/studio/test/workspace-signin.test.ts +0 -292
- package/apps/studio/test/ws-handshake.test.ts +0 -33
- package/cli/commands/cache.test.mjs +0 -166
- package/cli/commands/design-link.test.mjs +0 -419
- package/cli/commands/design.test.mjs +0 -240
- package/cli/commands/doctor.test.mjs +0 -237
- package/cli/commands/hub-workspace.test.mjs +0 -275
- package/cli/commands/hub.test.mjs +0 -225
- package/cli/commands/kg.test.mjs +0 -142
- package/cli/commands/preflight.test.mjs +0 -46
- package/cli/lib/argv.test.mjs +0 -46
- package/cli/lib/cache.test.mjs +0 -303
- package/cli/lib/cell-plan.test.mjs +0 -225
- package/cli/lib/config-lint.test.mjs +0 -117
- package/cli/lib/ddr-to-kgai.test.mjs +0 -118
- package/cli/lib/design-ownership.test.mjs +0 -329
- package/cli/lib/fetch-asset.test.mjs +0 -213
- package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
- package/cli/lib/figma-import-controls.test.mjs +0 -70
- package/cli/lib/flow-design-integration.test.mjs +0 -165
- package/cli/lib/gitignore-block.test.mjs +0 -136
- package/cli/lib/gitignore-drift.test.mjs +0 -156
- package/cli/lib/hubs-config.test.mjs +0 -165
- package/cli/lib/pkg-root.test.mjs +0 -111
- package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
- package/cli/lib/plugin-name-namespace.test.mjs +0 -71
- package/cli/lib/reconstruct-toolset.test.mjs +0 -194
- package/cli/lib/stack-detect.test.mjs +0 -121
- package/cli/lib/update-check.test.mjs +0 -32
- package/cli/lib/workspace-plan.test.mjs +0 -699
|
@@ -37,9 +37,11 @@ import {
|
|
|
37
37
|
applyMetaToDoc,
|
|
38
38
|
cssFromDoc,
|
|
39
39
|
htmlFromDoc,
|
|
40
|
+
laneValueFromFile,
|
|
40
41
|
mergeSharedMetaIntoLocal,
|
|
41
42
|
metaFromDoc,
|
|
42
43
|
movedToFromDoc,
|
|
44
|
+
readLaneFromDoc,
|
|
43
45
|
stampAnnotationsEdit,
|
|
44
46
|
stampBodyEdit,
|
|
45
47
|
} from './codec.ts';
|
|
@@ -47,8 +49,29 @@ import { type EchoGuard, hashBytes } from './echo-guard.ts';
|
|
|
47
49
|
import type { SyncJournal } from './journal.ts';
|
|
48
50
|
import { MAX_CSS_BYTES, MAX_HTML_BYTES, MAX_META_BYTES, withinByteCap } from './limits.ts';
|
|
49
51
|
import { ORIGINS } from './origins.ts';
|
|
52
|
+
import { collapseRepeatedModule, collapseRepeatedText } from './repeated-module.ts';
|
|
53
|
+
import type { RevisionBarrier } from './revision-barrier.ts';
|
|
54
|
+
import { repairSeedDuplication } from './seed-repair.ts';
|
|
55
|
+
import { mergeSource } from './source-merge.ts';
|
|
56
|
+
import type { SourceOp } from './source-ops.ts';
|
|
57
|
+
import { saveRecoveryBody } from './source-recovery.ts';
|
|
58
|
+
import { sourceError } from './source-validation.ts';
|
|
59
|
+
import { laneHash } from './transaction-client.ts';
|
|
50
60
|
|
|
51
61
|
export const PROJECT_FLUSH_MS = 800;
|
|
62
|
+
/**
|
|
63
|
+
* Accepted revisions arrive as whole, validated states — not a keystroke
|
|
64
|
+
* stream from a peer's editor that the 800 ms debounce exists to coalesce — so
|
|
65
|
+
* the receiving disk (and the canvas that renders from it) follows almost at
|
|
66
|
+
* once.
|
|
67
|
+
*/
|
|
68
|
+
export const ACCEPTED_FLUSH_MS = 30;
|
|
69
|
+
/** T24 — how many times one UI operation is re-applied after losing races. */
|
|
70
|
+
export const MAX_OP_REPLAYS = 3;
|
|
71
|
+
/** How long an announced API write may hold the doc→file writer at most. */
|
|
72
|
+
export const LOCAL_WRITE_HOLD_MS = 3_000;
|
|
73
|
+
/** `MAUDE_SYNC_DEBUG=1` — one line per proposal and per disk write (diagnosis). */
|
|
74
|
+
const SYNC_DEBUG = process.env.MAUDE_SYNC_DEBUG === '1';
|
|
52
75
|
export const CIRCUIT_MAX_STRIKES = 3;
|
|
53
76
|
|
|
54
77
|
export interface ProjectionPaths {
|
|
@@ -64,6 +87,13 @@ export interface ProjectionPaths {
|
|
|
64
87
|
css?: string;
|
|
65
88
|
}
|
|
66
89
|
|
|
90
|
+
export interface BodyRejection {
|
|
91
|
+
slug: string;
|
|
92
|
+
kind: 'body-rejected';
|
|
93
|
+
reason: 'invalid-source' | 'local-edit' | 'history-failed' | 'merge-budget';
|
|
94
|
+
snapshotFailed: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
67
97
|
export interface DocProjectionOptions {
|
|
68
98
|
slug: string;
|
|
69
99
|
doc: Y.Doc;
|
|
@@ -97,6 +127,77 @@ export interface DocProjectionOptions {
|
|
|
97
127
|
/** DDR-102 — per-machine sync journal; every successful disk↔doc body/css
|
|
98
128
|
* traversal checkpoints here (same discipline as the agent). Optional. */
|
|
99
129
|
journal?: SyncJournal;
|
|
130
|
+
/** Bounded recovery slots, separate from rolling history. */
|
|
131
|
+
historyDir?: string;
|
|
132
|
+
onConflict?: (info: BodyRejection) => void;
|
|
133
|
+
onRecovered?: () => void;
|
|
134
|
+
/** Runtime waits for cold-start snapshots before allowing any projection. */
|
|
135
|
+
waitForReconcile?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* ACCEPTED-REVISIONS MODE (DDR-241). While `accepted.on()` holds, a local
|
|
138
|
+
* change is never written into the shared document: it is PROPOSED with the
|
|
139
|
+
* value it was derived from, and the document changes only when the hub
|
|
140
|
+
* publishes the accepted (possibly merged) revision. The file is the
|
|
141
|
+
* candidate until then; a rejection keeps it and reports a conflict.
|
|
142
|
+
*/
|
|
143
|
+
accepted?: AcceptedLaneLink;
|
|
144
|
+
/**
|
|
145
|
+
* Plan T14 — the revision barrier shared by every projection of this
|
|
146
|
+
* runtime: a document stamped with a multi-document revision is written to
|
|
147
|
+
* disk together with the rest of that revision (see revision-barrier.ts).
|
|
148
|
+
*/
|
|
149
|
+
revisionBarrier?: RevisionBarrier;
|
|
150
|
+
/** T24 — re-apply a UI operation onto the version that won (sync/source-ops). */
|
|
151
|
+
replayOp?: (
|
|
152
|
+
op: SourceOp,
|
|
153
|
+
head: string
|
|
154
|
+
) => { ok: true; source: string } | { ok: false; reason: string };
|
|
155
|
+
/** T29 — this checkout now holds a document at this accepted revision. */
|
|
156
|
+
onRevisionApplied?: (revision: number) => void;
|
|
157
|
+
/** T26 — a lane value of ours was accepted as this action. */
|
|
158
|
+
onAccepted?: (info: { lane: ProposalLane; value: string; actionId: string }) => void;
|
|
159
|
+
/**
|
|
160
|
+
* May a write to the shared document reach the hub right now? When false a
|
|
161
|
+
* file change is HELD (not imported) and `onWriteBlocked` fires; the runtime
|
|
162
|
+
* re-delivers it via `retryDeferred()` once the connection is writable or
|
|
163
|
+
* the project turns out to be in accepted-revisions mode.
|
|
164
|
+
*/
|
|
165
|
+
canWriteDoc?: () => boolean;
|
|
166
|
+
onWriteBlocked?: () => void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type ProposalLane = 'html' | 'css' | 'meta' | 'annotations' | 'comments';
|
|
170
|
+
export interface ProposalOutcome {
|
|
171
|
+
status: 'accepted' | 'rejected';
|
|
172
|
+
code?: string;
|
|
173
|
+
/** On a base conflict: the hash of the accepted value that won. */
|
|
174
|
+
head?: string;
|
|
175
|
+
/** On acceptance: the project action that carries it (T26 — Cmd+Z binds to it). */
|
|
176
|
+
actionId?: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface LaneProposal {
|
|
180
|
+
lane: ProposalLane;
|
|
181
|
+
content: string;
|
|
182
|
+
/** The value this edit was derived from — the hub merges three-way from it. */
|
|
183
|
+
baseContent: string;
|
|
184
|
+
writeId?: string;
|
|
185
|
+
transactionId: string;
|
|
186
|
+
/** An earlier proposal of the same lane this one was authored on top of. */
|
|
187
|
+
dependsOn?: string[];
|
|
188
|
+
/**
|
|
189
|
+
* T16 — a change a TOOL wrote (watcher import, or a cold-start difference):
|
|
190
|
+
* joins an open AI action instead of going out on its own. A change the
|
|
191
|
+
* person made through the UI is never stageable.
|
|
192
|
+
*/
|
|
193
|
+
stageable?: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface AcceptedLaneLink {
|
|
197
|
+
/** Is the project in accepted-revisions mode right now? */
|
|
198
|
+
on(): boolean;
|
|
199
|
+
newTransactionId(): string;
|
|
200
|
+
propose(p: LaneProposal): Promise<ProposalOutcome>;
|
|
100
201
|
}
|
|
101
202
|
|
|
102
203
|
export interface DocProjection {
|
|
@@ -115,6 +216,58 @@ export interface DocProjection {
|
|
|
115
216
|
reconcile(): void;
|
|
116
217
|
/** Force the pending doc→file flush immediately. */
|
|
117
218
|
flush(): Promise<void>;
|
|
219
|
+
/**
|
|
220
|
+
* Adopt `body` as the shared base before `reconcile()`. A restart that could
|
|
221
|
+
* not merge a local candidate (cold start) hands the base back here, so the
|
|
222
|
+
* write stays blocked with a visible conflict and a later save can merge.
|
|
223
|
+
*/
|
|
224
|
+
adoptBase(body: string): void;
|
|
225
|
+
/**
|
|
226
|
+
* Accepted-revisions mode: propose one lane value that did not come through
|
|
227
|
+
* the watcher (a comment/annotation API write). Resolves with the outcome;
|
|
228
|
+
* `null` when the projection is not in accepted mode.
|
|
229
|
+
*/
|
|
230
|
+
proposeLane(
|
|
231
|
+
lane: ProposalLane,
|
|
232
|
+
value: string,
|
|
233
|
+
opts?: { baseContent?: string; writeId?: string; stageable?: boolean }
|
|
234
|
+
): Promise<ProposalOutcome> | null;
|
|
235
|
+
/** Lanes with an unresolved proposal (status surfaces). */
|
|
236
|
+
pendingCount(): number;
|
|
237
|
+
/**
|
|
238
|
+
* T28 — resolve a held source conflict by taking the project's version:
|
|
239
|
+
* the accepted body returns to disk; the local candidate is already in the
|
|
240
|
+
* recovery slots. Returns false when nothing is held.
|
|
241
|
+
*/
|
|
242
|
+
takeAccepted(): boolean;
|
|
243
|
+
/** T28 — the two sides of a held source conflict (null when none). */
|
|
244
|
+
conflictSides(): { mine: string | null; theirs: string } | null;
|
|
245
|
+
/** Re-deliver file changes held while the document was not writable. */
|
|
246
|
+
retryDeferred(): void;
|
|
247
|
+
/**
|
|
248
|
+
* Accepted mode: a privileged API route is about to rewrite the canvas
|
|
249
|
+
* source (it announces this with `activity:suppress`). Captures the exact
|
|
250
|
+
* bytes that edit is based on, and holds the doc→file writer until the
|
|
251
|
+
* edit's own file event has been proposed — so a peer revision landing in
|
|
252
|
+
* the watcher's quiet window can neither overwrite the edit nor read it as
|
|
253
|
+
* a conflicting stale local change.
|
|
254
|
+
*/
|
|
255
|
+
noteLocalWrite(): void;
|
|
256
|
+
/**
|
|
257
|
+
* T24 — the API write in flight is this UI operation (see sync/source-ops):
|
|
258
|
+
* if its proposal loses a race, the operation is re-applied onto the
|
|
259
|
+
* version that won and proposed again, instead of becoming a conflict.
|
|
260
|
+
*/
|
|
261
|
+
noteSourceOp(op: SourceOp): void;
|
|
262
|
+
/** The announced write did not happen (no-op or failure). */
|
|
263
|
+
cancelLocalWrite(): void;
|
|
264
|
+
/**
|
|
265
|
+
* Accepted-revisions cold start: disk differs from the accepted value and
|
|
266
|
+
* nothing proves what it was derived from. Keep both — block the lane's
|
|
267
|
+
* writer, report the conflict, and let the next save (based on `base`, the
|
|
268
|
+
* accepted value shown in the conflict) resolve it.
|
|
269
|
+
*/
|
|
270
|
+
hold(lane: ProposalLane, base: string, local: string): void;
|
|
118
271
|
/** Stop the doc listener + timers. */
|
|
119
272
|
stop(): void;
|
|
120
273
|
/** Test/inspection — the origin used on file→doc imports. */
|
|
@@ -131,6 +284,66 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
131
284
|
let started = false;
|
|
132
285
|
let stopped = false;
|
|
133
286
|
let dirty = false;
|
|
287
|
+
let ready = !opts.waitForReconcile;
|
|
288
|
+
let observedBody = readLocal(paths.html);
|
|
289
|
+
let rejectedKey: string | null = null;
|
|
290
|
+
let validationCache: { body: string; error: string | null } | null = null;
|
|
291
|
+
|
|
292
|
+
function recovered(): void {
|
|
293
|
+
if (rejectedKey !== null) opts.onRecovered?.();
|
|
294
|
+
rejectedKey = null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function validation(body: string): string | null {
|
|
298
|
+
if (validationCache?.body === body) return validationCache.error;
|
|
299
|
+
const error = sourceError(paths.html, body);
|
|
300
|
+
validationCache = { body, error };
|
|
301
|
+
return error;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Persist the agreed body beside the journal checkpoint (best-effort). */
|
|
305
|
+
function rememberBase(body: string): void {
|
|
306
|
+
if (!opts.historyDir) return;
|
|
307
|
+
try {
|
|
308
|
+
saveRecoveryBody(opts.historyDir, paths.html, 'base', body);
|
|
309
|
+
} catch {
|
|
310
|
+
/* a restart then falls back to newest-wins — the pre-existing behaviour */
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function preserveLocal(body: string | null): void {
|
|
315
|
+
if (opts.historyDir && body?.trim()) {
|
|
316
|
+
// An invalid local draft can still contain authored work. Keep its raw
|
|
317
|
+
// bytes too before accepting a valid remote replacement.
|
|
318
|
+
saveRecoveryBody(opts.historyDir, paths.html, 'local', body);
|
|
319
|
+
if (validation(body) === null)
|
|
320
|
+
saveRecoveryBody(opts.historyDir, paths.html, 'last-valid', body);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function reject(
|
|
325
|
+
reason: BodyRejection['reason'],
|
|
326
|
+
local: string | null,
|
|
327
|
+
incoming: string,
|
|
328
|
+
file = paths.html
|
|
329
|
+
): void {
|
|
330
|
+
const key = `${file}:${reason}:${hashBytes(local ?? '')}:${hashBytes(incoming)}`;
|
|
331
|
+
if (key === rejectedKey) return;
|
|
332
|
+
rejectedKey = key;
|
|
333
|
+
let snapshotFailed = false;
|
|
334
|
+
try {
|
|
335
|
+
if (file === paths.html) preserveLocal(local);
|
|
336
|
+
if (opts.historyDir) {
|
|
337
|
+
if (local !== null) saveRecoveryBody(opts.historyDir, file, 'local', local);
|
|
338
|
+
saveRecoveryBody(opts.historyDir, file, 'incoming', incoming);
|
|
339
|
+
}
|
|
340
|
+
} catch {
|
|
341
|
+
snapshotFailed = true;
|
|
342
|
+
}
|
|
343
|
+
console.warn(`[projection/${slug}] source sync blocked (${reason}); local file kept.`);
|
|
344
|
+
opts.onConflict?.({ slug, kind: 'body-rejected', reason, snapshotFailed });
|
|
345
|
+
}
|
|
346
|
+
|
|
134
347
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
135
348
|
|
|
136
349
|
// doc→file last-written hashes (skip redundant writes).
|
|
@@ -157,6 +370,9 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
157
370
|
) {
|
|
158
371
|
return;
|
|
159
372
|
}
|
|
373
|
+
// The accepted replica is the hub's to change — a local repair would be a
|
|
374
|
+
// write the fenced connection drops, leaving this replica diverged.
|
|
375
|
+
if (!acceptedOn()) repairSeedDuplication(doc, ORIGINS.DISK_PROJECTION);
|
|
160
376
|
scheduleFlush();
|
|
161
377
|
}
|
|
162
378
|
|
|
@@ -167,10 +383,13 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
167
383
|
return;
|
|
168
384
|
}
|
|
169
385
|
if (flushTimer) clearTimeout(flushTimer);
|
|
170
|
-
flushTimer = setTimeout(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
386
|
+
flushTimer = setTimeout(
|
|
387
|
+
() => {
|
|
388
|
+
flushTimer = null;
|
|
389
|
+
void flush();
|
|
390
|
+
},
|
|
391
|
+
acceptedOn() ? Math.min(flushMs, ACCEPTED_FLUSH_MS) : flushMs
|
|
392
|
+
);
|
|
174
393
|
}
|
|
175
394
|
|
|
176
395
|
function recordEcho(path: string, value: string): void {
|
|
@@ -216,25 +435,84 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
216
435
|
|
|
217
436
|
// ----- doc → file (html / css / meta only; room owns comments/annotations)
|
|
218
437
|
|
|
219
|
-
function writeHtmlIfChanged():
|
|
220
|
-
|
|
221
|
-
|
|
438
|
+
function writeHtmlIfChanged(): boolean {
|
|
439
|
+
// A proposal for this lane is in flight: the file is our candidate and the
|
|
440
|
+
// accepted value is about to be republished. Writing the document's
|
|
441
|
+
// current value now would briefly undo the user's own edit.
|
|
442
|
+
if (pending.has('html') || localWriteActive()) return false;
|
|
443
|
+
let next = htmlFromDoc(doc);
|
|
444
|
+
// The same module written twice or more in a row is a merge of two
|
|
445
|
+
// independent seeds, never an edit (repeated-module.ts). Keep one copy —
|
|
446
|
+
// in the shared document too, so every peer and the hub are repaired,
|
|
447
|
+
// not just this disk. (Accepted revisions: the replica is the hub's; the
|
|
448
|
+
// import there collapses it instead.)
|
|
449
|
+
if (!acceptedOn()) {
|
|
450
|
+
const repeated = collapseRepeatedModule(next);
|
|
451
|
+
if (repeated) {
|
|
452
|
+
console.warn(
|
|
453
|
+
`[projection/${slug}] the shared document held this canvas ${repeated.times}× over${
|
|
454
|
+
repeated.partialTail ? ' (the last copy cut short)' : ''
|
|
455
|
+
} — keeping one copy.`
|
|
456
|
+
);
|
|
457
|
+
doc.transact(() => {
|
|
458
|
+
applyHtmlToDoc(doc, repeated.unit, ORIGINS.DISK_PROJECTION);
|
|
459
|
+
stampBodyEdit(doc, ORIGINS.DISK_PROJECTION);
|
|
460
|
+
}, ORIGINS.DISK_PROJECTION);
|
|
461
|
+
next = repeated.unit;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (next === lastHtml) return true;
|
|
222
465
|
// Don't clobber a non-empty local body with an empty doc (cold-start before
|
|
223
466
|
// the doc is seeded — the safe-reconcile invariant; full adopt is Phase E).
|
|
224
467
|
if (next === '') {
|
|
225
468
|
lastHtml = next;
|
|
226
|
-
return;
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
if (!withinCap(paths.html, next, MAX_HTML_BYTES)) return false;
|
|
472
|
+
const local = readLocal(paths.html);
|
|
473
|
+
if (validation(next) !== null) {
|
|
474
|
+
reject('invalid-source', local, next);
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
if (local !== observedBody && local !== next) {
|
|
478
|
+
reject('local-edit', local, next);
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
preserveLocal(local);
|
|
483
|
+
} catch {
|
|
484
|
+
reject('history-failed', local, next);
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
if (SYNC_DEBUG) {
|
|
488
|
+
console.log(
|
|
489
|
+
`[projection/${slug}] write html ${hashBytes(next).slice(0, 8)} (disk was ${local === null ? '-' : hashBytes(local).slice(0, 8)})`
|
|
490
|
+
);
|
|
227
491
|
}
|
|
228
|
-
if (!withinCap(paths.html, next, MAX_HTML_BYTES)) return;
|
|
229
|
-
recordEcho(paths.html, next);
|
|
230
492
|
writeAndAnnounce(paths.html, next);
|
|
493
|
+
recordEcho(paths.html, next);
|
|
494
|
+
observedBody = next;
|
|
495
|
+
recovered();
|
|
231
496
|
lastHtml = next;
|
|
232
497
|
opts.journal?.record(slug, { bodyHash: hashBytes(next) }); // DDR-102 checkpoint
|
|
498
|
+
rememberBase(next);
|
|
499
|
+
return true;
|
|
233
500
|
}
|
|
234
501
|
|
|
235
502
|
function writeCssIfChanged(): void {
|
|
236
503
|
if (!paths.css) return;
|
|
237
|
-
|
|
504
|
+
if (pending.has('css') || held.has('css')) return;
|
|
505
|
+
let next = cssFromDoc(doc);
|
|
506
|
+
if (next !== null && !acceptedOn()) {
|
|
507
|
+
const repeated = collapseRepeatedText(next);
|
|
508
|
+
if (repeated) {
|
|
509
|
+
console.warn(
|
|
510
|
+
`[projection/${slug}] the shared stylesheet held itself ${repeated.times}× over — keeping one copy.`
|
|
511
|
+
);
|
|
512
|
+
applyCssToDoc(doc, repeated.unit, ORIGINS.DISK_PROJECTION);
|
|
513
|
+
next = repeated.unit;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
238
516
|
if (next === lastCss) return;
|
|
239
517
|
lastCss = next;
|
|
240
518
|
if (next === null) return; // doc carries no css yet — nothing to write
|
|
@@ -242,10 +520,18 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
242
520
|
recordEcho(paths.css, next);
|
|
243
521
|
writeAndAnnounce(paths.css, next);
|
|
244
522
|
opts.journal?.record(slug, { cssHash: hashBytes(next) }); // DDR-102 checkpoint
|
|
523
|
+
if (opts.historyDir) {
|
|
524
|
+
try {
|
|
525
|
+
saveRecoveryBody(opts.historyDir, paths.css, 'base', next);
|
|
526
|
+
} catch {
|
|
527
|
+
/* the journal hash still covers the common case */
|
|
528
|
+
}
|
|
529
|
+
}
|
|
245
530
|
}
|
|
246
531
|
|
|
247
532
|
function writeMetaIfChanged(): void {
|
|
248
533
|
if (!paths.meta) return;
|
|
534
|
+
if (pending.has('meta') || held.has('meta')) return;
|
|
249
535
|
const shared = metaFromDoc(doc);
|
|
250
536
|
if (shared === lastMeta) return;
|
|
251
537
|
lastMeta = shared;
|
|
@@ -258,8 +544,31 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
258
544
|
writeAndAnnounce(paths.meta, merged);
|
|
259
545
|
}
|
|
260
546
|
|
|
547
|
+
/** The newest multi-document revision this projection has been released for. */
|
|
548
|
+
let releasedRevision = 0;
|
|
549
|
+
/**
|
|
550
|
+
* The first stamp a projection sees is the state it started from (boot, or
|
|
551
|
+
* a document that arrived by pull) — already whole, never held. Only a
|
|
552
|
+
* revision that arrives WHILE it runs waits for its cohort.
|
|
553
|
+
*/
|
|
554
|
+
let seenStamp = false;
|
|
555
|
+
function observeStamp(stamped: [number, number]): void {
|
|
556
|
+
seenStamp = true;
|
|
557
|
+
releasedRevision = Math.max(releasedRevision, stamped[0]);
|
|
558
|
+
opts.revisionBarrier?.present(stamped[0], stamped[1], slug);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/** `[revision, cohort]` the hub stamped on this document (cohort ≥ 1). */
|
|
562
|
+
function stampedCohort(): [number, number] | null {
|
|
563
|
+
const meta = doc.getMap('syncMeta');
|
|
564
|
+
const rev = meta.get('acceptedRevision');
|
|
565
|
+
const cohort = meta.get('acceptedCohort');
|
|
566
|
+
if (typeof rev !== 'number') return null;
|
|
567
|
+
return [rev, typeof cohort === 'number' && cohort > 1 ? cohort : 1];
|
|
568
|
+
}
|
|
569
|
+
|
|
261
570
|
async function flush(): Promise<void> {
|
|
262
|
-
if (!dirty || stopped) return;
|
|
571
|
+
if (!dirty || stopped || !ready) return;
|
|
263
572
|
// A RETIRED document is write-inert — its canvas moved to a new path in a
|
|
264
573
|
// new document, and materialising this one is how a moved canvas
|
|
265
574
|
// resurrected itself at its old path (see codec stampMovedTo).
|
|
@@ -267,15 +576,30 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
267
576
|
dirty = false;
|
|
268
577
|
return;
|
|
269
578
|
}
|
|
579
|
+
// T14 — part of a multi-document revision: wait for the rest of it, so
|
|
580
|
+
// the checkout never shows half an action.
|
|
581
|
+
const stamped = acceptedOn() && opts.revisionBarrier ? stampedCohort() : null;
|
|
582
|
+
if (stamped && !seenStamp) observeStamp(stamped);
|
|
583
|
+
else if (stamped && stamped[1] > 1 && stamped[0] > releasedRevision) {
|
|
584
|
+
const [rev, cohort] = stamped;
|
|
585
|
+
const held = opts.revisionBarrier?.arrive(rev, cohort, slug, () => {
|
|
586
|
+
releasedRevision = Math.max(releasedRevision, rev);
|
|
587
|
+
dirty = true;
|
|
588
|
+
void flush();
|
|
589
|
+
});
|
|
590
|
+
if (held) return;
|
|
591
|
+
}
|
|
270
592
|
dirty = false;
|
|
271
593
|
if (flushTimer) {
|
|
272
594
|
clearTimeout(flushTimer);
|
|
273
595
|
flushTimer = null;
|
|
274
596
|
}
|
|
275
597
|
try {
|
|
276
|
-
writeHtmlIfChanged();
|
|
277
|
-
writeCssIfChanged();
|
|
598
|
+
if (writeHtmlIfChanged()) writeCssIfChanged();
|
|
278
599
|
writeMetaIfChanged();
|
|
600
|
+
const applied = acceptedOn() ? stampedCohort() : null;
|
|
601
|
+
if (applied && !held.has('html') && !pending.has('html'))
|
|
602
|
+
opts.onRevisionApplied?.(applied[0]);
|
|
279
603
|
} catch (err) {
|
|
280
604
|
dirty = true;
|
|
281
605
|
console.error(`[projection/${slug}] flush failed:`, err);
|
|
@@ -307,6 +631,308 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
307
631
|
quarantine.delete(path);
|
|
308
632
|
}
|
|
309
633
|
|
|
634
|
+
// ----- accepted-revisions mode (DDR-241)
|
|
635
|
+
|
|
636
|
+
const acceptedOn = (): boolean => !!opts.accepted?.on();
|
|
637
|
+
/** Per lane: proposals not yet answered, and the newest one's id + value. */
|
|
638
|
+
const pending = new Map<ProposalLane, { count: number; lastTx: string; lastValue: string }>();
|
|
639
|
+
/** Lanes whose last answer was a rejection — the local file is the held candidate. */
|
|
640
|
+
const held = new Set<ProposalLane>();
|
|
641
|
+
/** Accepted mode: file events that arrived before the cold start decided. */
|
|
642
|
+
const deferredBeforeReady = new Map<string, string>();
|
|
643
|
+
/** Legacy mode: file events held while the connection was not writable. */
|
|
644
|
+
const heldWhileReadOnly = new Map<string, string>();
|
|
645
|
+
/** Accepted mode: an API source write announced but not yet seen by the watcher. */
|
|
646
|
+
let localWrite: { base: string | null; at: number; op?: SourceOp } | null = null;
|
|
647
|
+
let localWriteTimer: ReturnType<typeof setTimeout> | null = null;
|
|
648
|
+
const localWriteActive = (): boolean => {
|
|
649
|
+
if (!localWrite) return false;
|
|
650
|
+
if (Date.now() - localWrite.at < LOCAL_WRITE_HOLD_MS) return true;
|
|
651
|
+
localWrite = null;
|
|
652
|
+
return false;
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const REJECTION_REASON: Record<string, BodyRejection['reason']> = {
|
|
656
|
+
'source-invalid': 'invalid-source',
|
|
657
|
+
capacity: 'merge-budget',
|
|
658
|
+
// The change could not even be saved to this machine's outbox.
|
|
659
|
+
'local-persistence': 'history-failed',
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
function laneOfPath(p: string): ProposalLane | null {
|
|
663
|
+
if (p === paths.html) return 'html';
|
|
664
|
+
if (paths.css && p === paths.css) return 'css';
|
|
665
|
+
if (paths.meta && p === paths.meta) return 'meta';
|
|
666
|
+
if (p === paths.comments) return 'comments';
|
|
667
|
+
if (p === paths.annotations) return 'annotations';
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function pathOfLane(lane: ProposalLane): string {
|
|
672
|
+
if (lane === 'html') return paths.html;
|
|
673
|
+
if (lane === 'css') return paths.css ?? paths.html;
|
|
674
|
+
if (lane === 'meta') return paths.meta ?? paths.html;
|
|
675
|
+
return lane === 'comments' ? paths.comments : paths.annotations;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** The value disk and the accepted replica last agreed on for `lane`. */
|
|
679
|
+
function agreedValue(lane: ProposalLane): string {
|
|
680
|
+
if (lane === 'html') return lastHtml ?? htmlFromDoc(doc);
|
|
681
|
+
if (lane === 'css') return lastCss ?? cssFromDoc(doc) ?? '';
|
|
682
|
+
return readLaneFromDoc(doc, lane);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function onRejected(lane: ProposalLane, local: string, outcome: ProposalOutcome): void {
|
|
686
|
+
held.add(lane);
|
|
687
|
+
// The candidate stays on disk: the projection's local-edit guard protects
|
|
688
|
+
// it (html), `held` blocks the lane writer (css/meta), and the recovery
|
|
689
|
+
// slots keep the bytes whatever happens next.
|
|
690
|
+
//
|
|
691
|
+
// The NEXT save is the resolution, and it is made looking at the version
|
|
692
|
+
// the conflict reports as incoming — so that version becomes its base.
|
|
693
|
+
// Keeping the old base would make every resolution that touches the same
|
|
694
|
+
// region conflict again, forever (there is no way out but typing the
|
|
695
|
+
// other side's bytes back exactly).
|
|
696
|
+
const incoming = readLaneFromDoc(doc, lane);
|
|
697
|
+
if (lane === 'html') {
|
|
698
|
+
observedBody = null;
|
|
699
|
+
lastHtml = incoming;
|
|
700
|
+
} else if (lane === 'css') {
|
|
701
|
+
lastCss = incoming;
|
|
702
|
+
}
|
|
703
|
+
reject(
|
|
704
|
+
REJECTION_REASON[outcome.code ?? ''] ?? 'local-edit',
|
|
705
|
+
local,
|
|
706
|
+
readLaneFromDoc(doc, lane),
|
|
707
|
+
pathOfLane(lane)
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Send one lane proposal. A proposal made while an earlier one of the same
|
|
713
|
+
* lane is unanswered was authored ON TOP of it: it is based on that value and
|
|
714
|
+
* DEPENDS on it, so a rejected U1 can never be bypassed by an accepted U2
|
|
715
|
+
* that silently carries half of it (plan T7/T13).
|
|
716
|
+
*/
|
|
717
|
+
function submit(
|
|
718
|
+
lane: ProposalLane,
|
|
719
|
+
value: string,
|
|
720
|
+
baseContent: string,
|
|
721
|
+
local: string,
|
|
722
|
+
writeId?: string,
|
|
723
|
+
stageable = false,
|
|
724
|
+
op?: SourceOp,
|
|
725
|
+
replays = 0
|
|
726
|
+
): Promise<ProposalOutcome> {
|
|
727
|
+
const link = opts.accepted as AcceptedLaneLink;
|
|
728
|
+
const prior = pending.get(lane);
|
|
729
|
+
const transactionId = link.newTransactionId();
|
|
730
|
+
if (SYNC_DEBUG) {
|
|
731
|
+
console.log(
|
|
732
|
+
`[projection/${slug}] propose ${lane} tx=${transactionId.slice(0, 11)} value=${hashBytes(value).slice(0, 8)} base=${hashBytes(baseContent).slice(0, 8)} doc=${hashBytes(readLaneFromDoc(doc, lane)).slice(0, 8)} last=${lastHtml === null ? '-' : hashBytes(lastHtml).slice(0, 8)} via=${new Error().stack?.split('\n')[3]?.trim().slice(0, 60)}`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
pending.set(lane, {
|
|
736
|
+
count: (prior?.count ?? 0) + 1,
|
|
737
|
+
lastTx: transactionId,
|
|
738
|
+
lastValue: value,
|
|
739
|
+
});
|
|
740
|
+
const settle = async (outcome: ProposalOutcome): Promise<ProposalOutcome> => {
|
|
741
|
+
// A rejection can outrun the publication of the version that beat it
|
|
742
|
+
// (HTTP answer vs. WebSocket update). The conflict must report — and the
|
|
743
|
+
// resolution be based on — THAT version, so wait briefly for it.
|
|
744
|
+
if (outcome.status === 'rejected' && outcome.head) await untilLaneHash(lane, outcome.head);
|
|
745
|
+
const p = pending.get(lane);
|
|
746
|
+
if (p) {
|
|
747
|
+
p.count -= 1;
|
|
748
|
+
if (p.count <= 0) pending.delete(lane);
|
|
749
|
+
}
|
|
750
|
+
// T24 — a UI operation that lost a race is re-applied onto the version
|
|
751
|
+
// that won (same property: the later acceptance wins; everything else
|
|
752
|
+
// of both people survives) and proposed again, instead of a conflict.
|
|
753
|
+
if (
|
|
754
|
+
outcome.status === 'rejected' &&
|
|
755
|
+
outcome.code === 'base-conflict' &&
|
|
756
|
+
lane === 'html' &&
|
|
757
|
+
op &&
|
|
758
|
+
opts.replayOp &&
|
|
759
|
+
replays < MAX_OP_REPLAYS &&
|
|
760
|
+
!pending.has(lane) &&
|
|
761
|
+
readLocal(paths.html) === local
|
|
762
|
+
) {
|
|
763
|
+
const head = readLaneFromDoc(doc, 'html');
|
|
764
|
+
const r = opts.replayOp(op, head);
|
|
765
|
+
if (r.ok && r.source !== head && validation(r.source) === null) {
|
|
766
|
+
if (SYNC_DEBUG) console.log(`[projection/${slug}] replay ${op.kind} onto the winner`);
|
|
767
|
+
observedBody = r.source;
|
|
768
|
+
recordEcho(paths.html, r.source);
|
|
769
|
+
writeAndAnnounce(paths.html, r.source);
|
|
770
|
+
return submit('html', r.source, head, r.source, undefined, false, op, replays + 1);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (outcome.status === 'accepted') {
|
|
774
|
+
if (!pending.has(lane)) held.delete(lane);
|
|
775
|
+
if (lane === 'html') recovered();
|
|
776
|
+
if (outcome.actionId) {
|
|
777
|
+
try {
|
|
778
|
+
opts.onAccepted?.({ lane, value, actionId: outcome.actionId });
|
|
779
|
+
} catch {
|
|
780
|
+
/* bookkeeping only */
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
} else if (outcome.code === 'discarded') {
|
|
784
|
+
// T16 — the person discarded an unfinished AI edit: the accepted
|
|
785
|
+
// version returns to disk (the candidate is in the recovery slots).
|
|
786
|
+
held.delete(lane);
|
|
787
|
+
if (lane === 'html') {
|
|
788
|
+
observedBody = readLocal(paths.html);
|
|
789
|
+
lastHtml = null;
|
|
790
|
+
} else if (lane === 'css') {
|
|
791
|
+
lastCss = null;
|
|
792
|
+
} else if (lane === 'meta') {
|
|
793
|
+
lastMeta = null;
|
|
794
|
+
}
|
|
795
|
+
} else {
|
|
796
|
+
onRejected(lane, local, outcome);
|
|
797
|
+
}
|
|
798
|
+
scheduleFlush();
|
|
799
|
+
return outcome;
|
|
800
|
+
};
|
|
801
|
+
return link
|
|
802
|
+
.propose({
|
|
803
|
+
lane,
|
|
804
|
+
content: value,
|
|
805
|
+
baseContent,
|
|
806
|
+
transactionId,
|
|
807
|
+
...(prior ? { dependsOn: [prior.lastTx] } : {}),
|
|
808
|
+
...(writeId ? { writeId } : {}),
|
|
809
|
+
...(stageable ? { stageable: true } : {}),
|
|
810
|
+
})
|
|
811
|
+
.then(settle, (err: unknown) => {
|
|
812
|
+
const code = (err as { code?: unknown })?.code;
|
|
813
|
+
if (code === 'stopped') {
|
|
814
|
+
// The runtime is going away with this proposal unanswered. It is
|
|
815
|
+
// still in the durable outbox and the next runtime resends it — so
|
|
816
|
+
// it is neither a failure nor a rejection, and nothing is reported.
|
|
817
|
+
const p = pending.get(lane);
|
|
818
|
+
if (p) {
|
|
819
|
+
p.count -= 1;
|
|
820
|
+
if (p.count <= 0) pending.delete(lane);
|
|
821
|
+
}
|
|
822
|
+
return { status: 'rejected', code: 'stopped' } as ProposalOutcome;
|
|
823
|
+
}
|
|
824
|
+
console.error(`[projection/${slug}] proposing ${lane} failed:`, err);
|
|
825
|
+
return settle({
|
|
826
|
+
status: 'rejected',
|
|
827
|
+
code: typeof code === 'string' ? code : 'client-error',
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function untilLaneHash(lane: ProposalLane, head: string, ms = 3_000): Promise<void> {
|
|
833
|
+
const matches = () => laneHash(readLaneFromDoc(doc, lane)) === head;
|
|
834
|
+
if (matches()) return Promise.resolve();
|
|
835
|
+
return new Promise((resolve) => {
|
|
836
|
+
const done = () => {
|
|
837
|
+
clearTimeout(timer);
|
|
838
|
+
doc.off('update', onUpdate);
|
|
839
|
+
resolve();
|
|
840
|
+
};
|
|
841
|
+
const onUpdate = () => {
|
|
842
|
+
if (matches()) done();
|
|
843
|
+
};
|
|
844
|
+
const timer = setTimeout(done, ms);
|
|
845
|
+
doc.on('update', onUpdate);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Accepted-revisions import: propose the file's lane value; never touch the doc. */
|
|
850
|
+
function proposeFromFs(evt: { path: string; hash: string }, str: string): boolean {
|
|
851
|
+
const lane = laneOfPath(evt.path);
|
|
852
|
+
if (!lane) return false;
|
|
853
|
+
if (SYNC_DEBUG && lane === 'html') {
|
|
854
|
+
console.log(
|
|
855
|
+
`[projection/${slug}] fs event html ${hashBytes(str).slice(0, 8)} evt=${evt.hash.slice(0, 8)} doc=${hashBytes(htmlFromDoc(doc)).slice(0, 8)} last=${lastHtml === null ? '-' : hashBytes(lastHtml).slice(0, 8)} held=${held.has('html')} pending=${pending.has('html')}`
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
// COMMENTS AND ANNOTATIONS ARE NEVER PROPOSED FROM A FILE EVENT. Their
|
|
859
|
+
// files have a second writer — the collab room projects the accepted
|
|
860
|
+
// document onto them — so a file change here is as likely the room
|
|
861
|
+
// writing a value the document has since moved past as it is an edit.
|
|
862
|
+
// Proposing that with the current head as its base REPLACES the head:
|
|
863
|
+
// a deleted shape resurrected by a stale projection (surface run
|
|
864
|
+
// 2026-09-15, L09 delete). Every edit to these lanes arrives through the
|
|
865
|
+
// API with the base it was made from (`proposeLane`).
|
|
866
|
+
if (lane === 'comments' || lane === 'annotations') return false;
|
|
867
|
+
if (lane === 'html') {
|
|
868
|
+
if (str === lastHtml && !held.has('html')) return false; // a redelivered projection
|
|
869
|
+
if (!withinCap(paths.html, str, MAX_HTML_BYTES)) return false;
|
|
870
|
+
if (validation(str) !== null) {
|
|
871
|
+
strike(evt.path, evt.hash);
|
|
872
|
+
reject('invalid-source', str, htmlFromDoc(doc));
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
} else if (lane === 'css' && str === lastCss && !held.has('css')) {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
const value = laneValueFromFile(lane, str);
|
|
879
|
+
if (value === null) {
|
|
880
|
+
strike(evt.path, evt.hash);
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
clearStrike(evt.path);
|
|
884
|
+
const inFlight = pending.get(lane);
|
|
885
|
+
// The watcher redelivering what we already proposed is not a new edit.
|
|
886
|
+
if (inFlight?.lastValue === value) return false;
|
|
887
|
+
const current = readLaneFromDoc(doc, lane);
|
|
888
|
+
if (value === current) {
|
|
889
|
+
if (inFlight) return false;
|
|
890
|
+
held.delete(lane);
|
|
891
|
+
if (lane === 'html') {
|
|
892
|
+
observedBody = str;
|
|
893
|
+
lastHtml = str;
|
|
894
|
+
recovered();
|
|
895
|
+
} else if (lane === 'css') {
|
|
896
|
+
lastCss = str;
|
|
897
|
+
}
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
// NOT "skip a value proposed before": the reader reads the file as it is
|
|
901
|
+
// NOW, so an event carrying an earlier proposal's bytes after that
|
|
902
|
+
// proposal was answered is the user going BACK to them (A→B→A, every
|
|
903
|
+
// undo/redo) — skipping it turned a real edit into a false conflict
|
|
904
|
+
// (surface run 2026-09-15, L18).
|
|
905
|
+
if (lane === 'html') {
|
|
906
|
+
try {
|
|
907
|
+
preserveLocal(str);
|
|
908
|
+
} catch {
|
|
909
|
+
reject('history-failed', str, current);
|
|
910
|
+
return false;
|
|
911
|
+
}
|
|
912
|
+
observedBody = str;
|
|
913
|
+
}
|
|
914
|
+
// An API edit announced its base (noteLocalWrite): that is exactly what
|
|
915
|
+
// it was derived from — better than the last agreed body, which a peer
|
|
916
|
+
// revision may have advanced inside the watcher's quiet window.
|
|
917
|
+
const apiBase = lane === 'html' ? (localWrite?.base ?? null) : null;
|
|
918
|
+
// T16 — a change the person made through the UI announced itself; any
|
|
919
|
+
// other file change is a tool's, and may belong to an open AI action.
|
|
920
|
+
const fromUi = localWrite !== null;
|
|
921
|
+
// T24 — the UI operation this write was, when it said.
|
|
922
|
+
const op = lane === 'html' ? localWrite?.op : undefined;
|
|
923
|
+
if (lane === 'html') localWrite = null;
|
|
924
|
+
void submit(
|
|
925
|
+
lane,
|
|
926
|
+
value,
|
|
927
|
+
inFlight ? inFlight.lastValue : (apiBase ?? agreedValue(lane)),
|
|
928
|
+
str,
|
|
929
|
+
undefined,
|
|
930
|
+
!fromUi,
|
|
931
|
+
op
|
|
932
|
+
);
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
|
|
310
936
|
function applyFromFs(evt: { path: string; bytes: Uint8Array; hash: string }): boolean {
|
|
311
937
|
if (stopped) return false;
|
|
312
938
|
// Write-inert both ways — a local edit to a stale pre-move file must not
|
|
@@ -318,25 +944,100 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
318
944
|
if (isQuarantined(evt.path, evt.hash)) return false;
|
|
319
945
|
|
|
320
946
|
const str = bytesToString(evt.bytes);
|
|
947
|
+
if (acceptedOn()) {
|
|
948
|
+
// Before the cold start has decided, a proposal would race it (and a
|
|
949
|
+
// brand-new canvas is not in the project yet). Remember the path; the
|
|
950
|
+
// file is re-read once the decision is made.
|
|
951
|
+
if (!ready) {
|
|
952
|
+
deferredBeforeReady.set(evt.path, evt.hash);
|
|
953
|
+
return false;
|
|
954
|
+
}
|
|
955
|
+
return proposeFromFs(evt, str);
|
|
956
|
+
}
|
|
957
|
+
// Legacy import: a write the hub would drop must not be made at all.
|
|
958
|
+
if (opts.canWriteDoc && !opts.canWriteDoc()) {
|
|
959
|
+
heldWhileReadOnly.set(evt.path, evt.hash);
|
|
960
|
+
opts.onWriteBlocked?.();
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
321
963
|
|
|
322
964
|
if (evt.path === paths.html) {
|
|
965
|
+
// Watchers can deliver the same projection more than once, including
|
|
966
|
+
// after the one-shot echo token was consumed or expired. These bytes
|
|
967
|
+
// contain no new local edit; diffing them against a newer remote body
|
|
968
|
+
// would turn a delayed notification into a rollback of peer work.
|
|
969
|
+
// Compare only the current baseline, so a deliberate A → B → A edit
|
|
970
|
+
// still imports after B advances lastHtml.
|
|
971
|
+
if (str === lastHtml) return false;
|
|
972
|
+
if (!withinCap(paths.html, str, MAX_HTML_BYTES)) return false;
|
|
973
|
+
if (validation(str) !== null) {
|
|
974
|
+
strike(evt.path, evt.hash);
|
|
975
|
+
reject('invalid-source', str, htmlFromDoc(doc));
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
323
978
|
clearStrike(evt.path);
|
|
979
|
+
// A PEER CHANGED THE DOC SINCE DISK AND DOC LAST AGREED (`lastHtml`), and
|
|
980
|
+
// this save was authored against that older body. Importing it as a
|
|
981
|
+
// whole-file diff against the doc turned every stale byte into an edit
|
|
982
|
+
// and reverted the peer's work (audit 2026-09-13 P0 #1). Merge from the
|
|
983
|
+
// shared base instead; what the merge cannot prove independent — or a
|
|
984
|
+
// merged body that no longer validates — is preserved and blocked, and
|
|
985
|
+
// the conflict stays until a later save actually resolves it.
|
|
986
|
+
const current = htmlFromDoc(doc);
|
|
987
|
+
let next = str;
|
|
988
|
+
if (lastHtml !== null && current !== lastHtml && current !== str) {
|
|
989
|
+
const merged = mergeSource(lastHtml, str, current);
|
|
990
|
+
if (!merged.ok) {
|
|
991
|
+
reject(merged.reason === 'budget' ? 'merge-budget' : 'local-edit', str, current);
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
if (validation(merged.merged) !== null) {
|
|
995
|
+
reject('local-edit', str, current);
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
next = merged.merged;
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
preserveLocal(str);
|
|
1002
|
+
} catch {
|
|
1003
|
+
reject('history-failed', str, htmlFromDoc(doc));
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
324
1006
|
// Body import + syncMeta stamp in ONE transaction (same FILE_IMPORT
|
|
325
1007
|
// origin) — peers get a single update carrying the newest-wins stamp.
|
|
326
1008
|
let changed = false;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
1009
|
+
try {
|
|
1010
|
+
doc.transact(() => {
|
|
1011
|
+
changed = applyHtmlToDoc(doc, next, importOrigin);
|
|
1012
|
+
if (changed) stampBodyEdit(doc, importOrigin);
|
|
1013
|
+
}, importOrigin);
|
|
1014
|
+
} catch {
|
|
1015
|
+
reject('merge-budget', str, htmlFromDoc(doc));
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
observedBody = str;
|
|
1019
|
+
lastHtml = str;
|
|
1020
|
+
recovered();
|
|
1021
|
+
if (htmlFromDoc(doc) !== str) {
|
|
1022
|
+
// A synchronous peer update can land during the import transaction.
|
|
1023
|
+
// Only bytes actually on disk count as the projection's baseline.
|
|
1024
|
+
scheduleFlush();
|
|
1025
|
+
} else if (changed) {
|
|
333
1026
|
opts.journal?.record(slug, { bodyHash: evt.hash }); // DDR-102 checkpoint
|
|
1027
|
+
rememberBase(str);
|
|
334
1028
|
}
|
|
335
1029
|
return changed;
|
|
336
1030
|
}
|
|
337
1031
|
if (paths.css && evt.path === paths.css) {
|
|
1032
|
+
let changed: boolean;
|
|
1033
|
+
try {
|
|
1034
|
+
changed = applyCssToDoc(doc, str, importOrigin);
|
|
1035
|
+
} catch {
|
|
1036
|
+
strike(evt.path, evt.hash);
|
|
1037
|
+
reject('merge-budget', str, cssFromDoc(doc) ?? '', paths.css);
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
338
1040
|
clearStrike(evt.path);
|
|
339
|
-
const changed = applyCssToDoc(doc, str, importOrigin);
|
|
340
1041
|
if (changed) {
|
|
341
1042
|
lastCss = str;
|
|
342
1043
|
opts.journal?.record(slug, { cssHash: evt.hash }); // DDR-102 checkpoint
|
|
@@ -383,13 +1084,25 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
383
1084
|
if (stopped) return;
|
|
384
1085
|
// A retired doc materialises NOTHING (see codec stampMovedTo).
|
|
385
1086
|
if (movedToFromDoc(doc) !== null) return;
|
|
1087
|
+
ready = true;
|
|
1088
|
+
if (deferredBeforeReady.size && acceptedOn()) {
|
|
1089
|
+
const owed = [...deferredBeforeReady];
|
|
1090
|
+
deferredBeforeReady.clear();
|
|
1091
|
+
for (const [p, hash] of owed) {
|
|
1092
|
+
const text = readLocal(p);
|
|
1093
|
+
if (text !== null) proposeFromFs({ path: p, hash }, text);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
386
1096
|
// Materialize the converged doc to disk (html/css/meta). The *IfChanged
|
|
387
1097
|
// writers already guard against clobbering non-empty local with empty doc
|
|
388
1098
|
// values, so this is safe to run at cold start before the authoritative
|
|
389
1099
|
// seed (Phase E) — it only writes what the doc actually holds.
|
|
390
|
-
writeHtmlIfChanged();
|
|
391
|
-
writeCssIfChanged();
|
|
1100
|
+
if (writeHtmlIfChanged()) writeCssIfChanged();
|
|
392
1101
|
writeMetaIfChanged();
|
|
1102
|
+
// A document that arrives by pull as part of a multi-document revision is
|
|
1103
|
+
// on disk now: the rest of its revision may show.
|
|
1104
|
+
const stamped = acceptedOn() ? stampedCohort() : null;
|
|
1105
|
+
if (stamped && !seenStamp) observeStamp(stamped);
|
|
393
1106
|
}
|
|
394
1107
|
|
|
395
1108
|
return {
|
|
@@ -397,14 +1110,98 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
|
|
|
397
1110
|
importOrigin,
|
|
398
1111
|
start() {
|
|
399
1112
|
if (started) return;
|
|
1113
|
+
// Snapshot before a provider can replace disk. Failure remains fail-closed
|
|
1114
|
+
// at the write boundary and is surfaced when a write is attempted.
|
|
1115
|
+
try {
|
|
1116
|
+
preserveLocal(observedBody);
|
|
1117
|
+
} catch {
|
|
1118
|
+
/* checked before writes */
|
|
1119
|
+
}
|
|
400
1120
|
doc.on('update', onDocUpdate);
|
|
401
1121
|
started = true;
|
|
402
1122
|
},
|
|
403
1123
|
applyFromFs,
|
|
404
1124
|
reconcile,
|
|
405
1125
|
flush,
|
|
1126
|
+
adoptBase(body: string) {
|
|
1127
|
+
lastHtml = body;
|
|
1128
|
+
observedBody = body;
|
|
1129
|
+
},
|
|
1130
|
+
hold(lane, base, local) {
|
|
1131
|
+
held.add(lane);
|
|
1132
|
+
if (lane === 'html') {
|
|
1133
|
+
lastHtml = base;
|
|
1134
|
+
observedBody = null;
|
|
1135
|
+
} else if (lane === 'css') {
|
|
1136
|
+
lastCss = base;
|
|
1137
|
+
}
|
|
1138
|
+
reject('local-edit', local, readLaneFromDoc(doc, lane), pathOfLane(lane));
|
|
1139
|
+
},
|
|
1140
|
+
takeAccepted() {
|
|
1141
|
+
if (!held.has('html') && rejectedKey === null) return false;
|
|
1142
|
+
held.delete('html');
|
|
1143
|
+
// The local-edit guard compares disk with `observedBody`; declaring the
|
|
1144
|
+
// current disk bytes observed lets the writer replace them.
|
|
1145
|
+
observedBody = readLocal(paths.html);
|
|
1146
|
+
lastHtml = null;
|
|
1147
|
+
dirty = true;
|
|
1148
|
+
void flush();
|
|
1149
|
+
recovered();
|
|
1150
|
+
return true;
|
|
1151
|
+
},
|
|
1152
|
+
conflictSides() {
|
|
1153
|
+
if (!held.has('html') && rejectedKey === null) return null;
|
|
1154
|
+
return { mine: readLocal(paths.html), theirs: htmlFromDoc(doc) };
|
|
1155
|
+
},
|
|
1156
|
+
proposeLane(lane, value, o) {
|
|
1157
|
+
if (!acceptedOn() || stopped) return null;
|
|
1158
|
+
if (lane === 'html') observedBody = value;
|
|
1159
|
+
const inFlight = pending.get(lane);
|
|
1160
|
+
if (inFlight?.lastValue === value) return Promise.resolve({ status: 'accepted' });
|
|
1161
|
+
if (!inFlight && value === readLaneFromDoc(doc, lane)) {
|
|
1162
|
+
held.delete(lane);
|
|
1163
|
+
return Promise.resolve({ status: 'accepted' });
|
|
1164
|
+
}
|
|
1165
|
+
const base = o?.baseContent ?? (inFlight ? inFlight.lastValue : agreedValue(lane));
|
|
1166
|
+
return submit(lane, value, base, value, o?.writeId, o?.stageable === true);
|
|
1167
|
+
},
|
|
1168
|
+
noteSourceOp(op) {
|
|
1169
|
+
if (localWrite) localWrite.op = op;
|
|
1170
|
+
},
|
|
1171
|
+
noteLocalWrite() {
|
|
1172
|
+
if (!acceptedOn() || stopped) return;
|
|
1173
|
+
localWrite = { base: readLocal(paths.html), at: Date.now() };
|
|
1174
|
+
if (localWriteTimer) clearTimeout(localWriteTimer);
|
|
1175
|
+
// The watcher event normally clears this long before; if it never comes
|
|
1176
|
+
// (a write that changed nothing), the writer resumes on its own.
|
|
1177
|
+
localWriteTimer = setTimeout(() => {
|
|
1178
|
+
localWriteTimer = null;
|
|
1179
|
+
scheduleFlush();
|
|
1180
|
+
}, LOCAL_WRITE_HOLD_MS + 10);
|
|
1181
|
+
localWriteTimer.unref?.();
|
|
1182
|
+
},
|
|
1183
|
+
cancelLocalWrite() {
|
|
1184
|
+
localWrite = null;
|
|
1185
|
+
scheduleFlush();
|
|
1186
|
+
},
|
|
1187
|
+
retryDeferred() {
|
|
1188
|
+
if (stopped || heldWhileReadOnly.size === 0) return;
|
|
1189
|
+
if (!acceptedOn() && opts.canWriteDoc && !opts.canWriteDoc()) return;
|
|
1190
|
+
const owed = [...heldWhileReadOnly];
|
|
1191
|
+
heldWhileReadOnly.clear();
|
|
1192
|
+
for (const [p, hash] of owed) {
|
|
1193
|
+
const text = readLocal(p);
|
|
1194
|
+
if (text !== null) applyFromFs({ path: p, bytes: new TextEncoder().encode(text), hash });
|
|
1195
|
+
}
|
|
1196
|
+
},
|
|
1197
|
+
pendingCount() {
|
|
1198
|
+
let n = 0;
|
|
1199
|
+
for (const p of pending.values()) n += p.count;
|
|
1200
|
+
return n;
|
|
1201
|
+
},
|
|
406
1202
|
stop() {
|
|
407
1203
|
stopped = true;
|
|
1204
|
+
if (localWriteTimer) clearTimeout(localWriteTimer);
|
|
408
1205
|
doc.off('update', onDocUpdate);
|
|
409
1206
|
if (flushTimer) {
|
|
410
1207
|
clearTimeout(flushTimer);
|