@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ANNOTATION_WRITE_ID } from '../annotations-sync.ts';
|
|
1
2
|
// Y.Doc ↔ disk codecs for the bidirectional file sync agent (Phase 9 Task 4).
|
|
2
3
|
//
|
|
3
4
|
// The agent shuttles three classes of files between disk and the Y.Doc the
|
|
@@ -22,10 +23,11 @@
|
|
|
22
23
|
// in the unchanged regions.
|
|
23
24
|
|
|
24
25
|
import { hostname } from 'node:os';
|
|
25
|
-
|
|
26
|
+
import { diffChars } from 'diff';
|
|
26
27
|
import type * as Y from 'yjs';
|
|
27
28
|
|
|
28
29
|
import { Y_TYPES } from '../collab/persistence.ts';
|
|
30
|
+
import { commentKey, dedupeCommentsById } from './comment-identity.ts';
|
|
29
31
|
import {
|
|
30
32
|
MAX_ANNOTATIONS_BYTES,
|
|
31
33
|
MAX_COMMENTS_BYTES,
|
|
@@ -96,20 +98,27 @@ export function htmlFromDoc(doc: Y.Doc): string {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
/**
|
|
99
|
-
* Apply `next`
|
|
100
|
-
*
|
|
101
|
+
* Apply `next` as bounded text hunks, preserving unchanged interior CRDT
|
|
102
|
+
* anchors as well as the common prefix/suffix (#121).
|
|
103
|
+
*
|
|
104
|
+
* THIS SHAPE IS A CONVERGENCE PROPERTY, NOT AN OPTIMIZATION (issue #114).
|
|
105
|
+
* Collapsing a concurrency-duplicated lane — `X + X` back to `X` — comes out of
|
|
106
|
+
* this diff as a PURE DELETE of the trailing run: no insert at all. Two peers
|
|
107
|
+
* that independently decide to collapse therefore delete the SAME CRDT items,
|
|
108
|
+
* and deletes are idempotent, so they converge.
|
|
109
|
+
*
|
|
110
|
+
* A wholesale `delete(0, len)` + `insert(0, next)` does not have that property.
|
|
111
|
+
* Both peers' inserts are new items at the same position, and Yjs keeps both —
|
|
112
|
+
* so the "repair" re-creates exactly the duplication it was meant to undo. The
|
|
113
|
+
* css lane was written that way, which is why it was the lane that never came
|
|
114
|
+
* out of a multi-peer cold start clean (measured: 0 of 43 lanes). Any lane whose
|
|
115
|
+
* duplication is repairable MUST go through here.
|
|
101
116
|
*
|
|
102
117
|
* Pass `origin` as the transaction origin so a downstream observer can
|
|
103
118
|
* distinguish self-originated updates from peer/remote ones.
|
|
104
119
|
*/
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
console.warn(
|
|
108
|
-
`[sync/codec] refusing HTML apply > ${MAX_HTML_BYTES} bytes (got ${byteLengthUtf8(next)}). DDR-054 §2d.`
|
|
109
|
-
);
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
const yText = doc.getText(Y_SYNC_TYPES.html);
|
|
120
|
+
function applyTextLane(doc: Y.Doc, lane: string, next: string, origin?: unknown): boolean {
|
|
121
|
+
const yText = doc.getText(lane);
|
|
113
122
|
const current = yText.toString();
|
|
114
123
|
if (current === next) return false;
|
|
115
124
|
|
|
@@ -129,17 +138,55 @@ export function applyHtmlToDoc(doc: Y.Doc, next: string, origin?: unknown): bool
|
|
|
129
138
|
suffix++;
|
|
130
139
|
}
|
|
131
140
|
|
|
141
|
+
// Y.Text offsets are UTF-16; never leave half a surrogate outside a hunk.
|
|
142
|
+
if (prefix > 0 && /[\uD800-\uDBFF]/.test(current[prefix - 1] ?? '')) prefix--;
|
|
143
|
+
if (suffix > 0 && /[\uDC00-\uDFFF]/.test(current[current.length - suffix] ?? '')) suffix--;
|
|
132
144
|
const deleteLen = current.length - prefix - suffix;
|
|
133
145
|
const insertStr = next.slice(prefix, next.length - suffix);
|
|
134
146
|
|
|
147
|
+
// Keep pure inserts/deletes (especially repeat collapse) as one operation.
|
|
148
|
+
// For replacements preserve interior anchors: one prefix/suffix splice can
|
|
149
|
+
// move a concurrent insertion inside an unrelated JSX attribute (#121).
|
|
150
|
+
const changes =
|
|
151
|
+
deleteLen > 0 && insertStr.length > 0
|
|
152
|
+
? diffChars(current.slice(prefix, current.length - suffix), insertStr, {
|
|
153
|
+
maxEditLength: 4096,
|
|
154
|
+
timeout: 50,
|
|
155
|
+
})
|
|
156
|
+
: null;
|
|
157
|
+
if (changes === undefined) throw new Error('Source diff exceeds the safe merge budget');
|
|
135
158
|
doc.transact(() => {
|
|
136
|
-
if (
|
|
137
|
-
|
|
159
|
+
if (changes) {
|
|
160
|
+
let offset = prefix;
|
|
161
|
+
for (const change of changes) {
|
|
162
|
+
if (change.removed) yText.delete(offset, change.value.length);
|
|
163
|
+
else {
|
|
164
|
+
if (change.added) yText.insert(offset, change.value);
|
|
165
|
+
offset += change.value.length;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
if (deleteLen > 0) yText.delete(prefix, deleteLen);
|
|
170
|
+
if (insertStr.length > 0) yText.insert(prefix, insertStr);
|
|
171
|
+
}
|
|
138
172
|
}, origin);
|
|
139
173
|
|
|
140
174
|
return true;
|
|
141
175
|
}
|
|
142
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Apply `next` to the canvas body Y.Text inside `doc`. See `applyTextLane`.
|
|
179
|
+
*/
|
|
180
|
+
export function applyHtmlToDoc(doc: Y.Doc, next: string, origin?: unknown): boolean {
|
|
181
|
+
if (byteLengthUtf8(next) > MAX_HTML_BYTES) {
|
|
182
|
+
console.warn(
|
|
183
|
+
`[sync/codec] refusing HTML apply > ${MAX_HTML_BYTES} bytes (got ${byteLengthUtf8(next)}). DDR-054 §2d.`
|
|
184
|
+
);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return applyTextLane(doc, Y_SYNC_TYPES.html, next, origin);
|
|
188
|
+
}
|
|
189
|
+
|
|
143
190
|
/* ---------------------------------------------------------------- comments */
|
|
144
191
|
|
|
145
192
|
/**
|
|
@@ -153,14 +200,41 @@ export function commentsFromDoc(doc: Y.Doc): CommentsSnapshot {
|
|
|
153
200
|
return arr.toArray();
|
|
154
201
|
}
|
|
155
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Apply `next` to the comments Y.Array as a MINIMAL, IDENTITY-KEYED DIFF.
|
|
205
|
+
*
|
|
206
|
+
* THIS SHAPE IS A CONVERGENCE PROPERTY, NOT AN OPTIMIZATION (issue #112) —
|
|
207
|
+
* the same law `applyTextLane` states for the text lanes, which this lane was
|
|
208
|
+
* missing. It used to be `delete(0, len)` + `push(next)`, and under concurrency
|
|
209
|
+
* that is not a replace at all: two peers delete the SAME items (idempotent)
|
|
210
|
+
* but insert two runs at the same position, and Yjs keeps both. The array
|
|
211
|
+
* becomes `list + list`, `persistJson` writes that to `_comments/<slug>.json`,
|
|
212
|
+
* the file re-imports clean, and the duplication IS the new truth. Every
|
|
213
|
+
* reconciliation round doubled it — 1 → 2 → 4 → 8, the count the #112 reporter
|
|
214
|
+
* saw — and the cold-start union repair, published through the same wholesale
|
|
215
|
+
* write, re-created the duplication it had just undone. Verbatim the css lesson
|
|
216
|
+
* from issue #114, one lane later.
|
|
217
|
+
*
|
|
218
|
+
* The diff gives the lane the property the text lanes got:
|
|
219
|
+
*
|
|
220
|
+
* - Collapsing a duplicated array is a PURE DELETE of items every peer holds,
|
|
221
|
+
* and concurrent deletes of the same items are idempotent — so two peers
|
|
222
|
+
* repairing at once converge instead of re-doubling.
|
|
223
|
+
* - Dedupe happens on EVERY apply, not only at cold start, so an array that
|
|
224
|
+
* doubled anyway (an older peer still on the wholesale write, a genuine
|
|
225
|
+
* concurrent edit of one entry) collapses at the next apply rather than
|
|
226
|
+
* compounding. Duplication became self-healing instead of exponential.
|
|
227
|
+
* - Untouched comments keep their CRDT items, so a peer replacing one entry
|
|
228
|
+
* no longer rewrites the whole list under everyone else's feet.
|
|
229
|
+
*
|
|
230
|
+
* Ordering follows the doc for surviving entries, with genuinely new ones
|
|
231
|
+
* appended. Reordering is deliberately NOT synced: it would be a delete+insert
|
|
232
|
+
* per moved entry — churn with the one shape this fix exists to avoid — and
|
|
233
|
+
* comment order carries no meaning (pins are placed by selector, the sidebar
|
|
234
|
+
* sorts by `created`).
|
|
235
|
+
*/
|
|
156
236
|
export function applyCommentsToDoc(doc: Y.Doc, next: CommentsSnapshot, origin?: unknown): boolean {
|
|
157
|
-
const arr = doc.getArray(Y_TYPES.comments);
|
|
158
|
-
// Comments are LWW on the JSON file (the snapshot is the source of truth);
|
|
159
|
-
// collapse Y.Array to the new state. For v1.1 we just replace wholesale —
|
|
160
|
-
// structural comment-level merge is deferred along with structured HTML.
|
|
161
|
-
// Check whether anything actually changed to avoid no-op transactions
|
|
162
|
-
// (transactions still fire `update` events, which would re-enter the loop).
|
|
163
|
-
const before = JSON.stringify(arr.toArray());
|
|
237
|
+
const arr = doc.getArray<unknown>(Y_TYPES.comments);
|
|
164
238
|
const after = JSON.stringify(next);
|
|
165
239
|
if (byteLengthUtf8(after) > MAX_COMMENTS_BYTES) {
|
|
166
240
|
console.warn(
|
|
@@ -168,11 +242,51 @@ export function applyCommentsToDoc(doc: Y.Doc, next: CommentsSnapshot, origin?:
|
|
|
168
242
|
);
|
|
169
243
|
return false;
|
|
170
244
|
}
|
|
171
|
-
|
|
245
|
+
|
|
246
|
+
// Desired state, keyed by comment identity. `next` can itself arrive
|
|
247
|
+
// duplicated (a file written before this fix, a list pushed by an older
|
|
248
|
+
// peer), so it is deduped on the way in — first occurrence wins, matching
|
|
249
|
+
// `unionCommentsById` and `dedupeCommentsById`.
|
|
250
|
+
const desired = new Map<string, unknown>();
|
|
251
|
+
for (const c of next) {
|
|
252
|
+
const k = commentKey(c);
|
|
253
|
+
if (!desired.has(k)) desired.set(k, c);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Classify every item currently in the array: keep it, replace it in place,
|
|
257
|
+
// or delete it. Duplicates of an identity we already kept are deletes — that
|
|
258
|
+
// is what makes EVERY apply a repair, not just the cold-start union.
|
|
259
|
+
const current = arr.toArray();
|
|
260
|
+
const deletes: number[] = [];
|
|
261
|
+
const replaces: { index: number; value: unknown }[] = [];
|
|
262
|
+
const kept = new Set<string>();
|
|
263
|
+
for (let i = 0; i < current.length; i++) {
|
|
264
|
+
const k = commentKey(current[i]);
|
|
265
|
+
if (!desired.has(k) || kept.has(k)) {
|
|
266
|
+
deletes.push(i);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
kept.add(k);
|
|
270
|
+
const want = desired.get(k);
|
|
271
|
+
if (JSON.stringify(current[i]) !== JSON.stringify(want))
|
|
272
|
+
replaces.push({ index: i, value: want });
|
|
273
|
+
}
|
|
274
|
+
const appends: unknown[] = [];
|
|
275
|
+
for (const [k, v] of desired) if (!kept.has(k)) appends.push(v);
|
|
276
|
+
|
|
277
|
+
// No-op guard. Transactions fire `update` events even when they change
|
|
278
|
+
// nothing, and an update re-enters the file→doc→file loop.
|
|
279
|
+
if (deletes.length === 0 && replaces.length === 0 && appends.length === 0) return false;
|
|
172
280
|
|
|
173
281
|
doc.transact(() => {
|
|
174
|
-
|
|
175
|
-
|
|
282
|
+
// Replaces first, while the indices computed above are still valid.
|
|
283
|
+
for (const r of replaces) {
|
|
284
|
+
arr.delete(r.index, 1);
|
|
285
|
+
arr.insert(r.index, [r.value]);
|
|
286
|
+
}
|
|
287
|
+
// Then deletes, descending, so each index stays valid as we go.
|
|
288
|
+
for (let i = deletes.length - 1; i >= 0; i--) arr.delete(deletes[i]!, 1);
|
|
289
|
+
if (appends.length > 0) arr.push(appends);
|
|
176
290
|
}, origin);
|
|
177
291
|
return true;
|
|
178
292
|
}
|
|
@@ -218,6 +332,8 @@ export function applyAnnotationsToDoc(doc: Y.Doc, next: string | null, origin?:
|
|
|
218
332
|
if (currentStr === next) return false;
|
|
219
333
|
|
|
220
334
|
doc.transact(() => {
|
|
335
|
+
// A filesystem import is a new operation, never the previous UI author's echo.
|
|
336
|
+
map.delete(ANNOTATION_WRITE_ID);
|
|
221
337
|
if (next === null || next === '') {
|
|
222
338
|
map.delete('svg');
|
|
223
339
|
} else {
|
|
@@ -301,6 +417,45 @@ function parsesAsObject(s: string): boolean {
|
|
|
301
417
|
}
|
|
302
418
|
|
|
303
419
|
/** The synced shared-meta JSON string held in the doc, or null when unset. */
|
|
420
|
+
/**
|
|
421
|
+
* The canonical LANE value for a file's text — what the accepted-revisions
|
|
422
|
+
* kernel stores and what `readLaneFromDoc` returns once it is accepted
|
|
423
|
+
* (DDR-241). `null` when the file text cannot be a value of that lane.
|
|
424
|
+
*/
|
|
425
|
+
export function laneValueFromFile(
|
|
426
|
+
lane: 'html' | 'css' | 'meta' | 'annotations' | 'comments',
|
|
427
|
+
text: string
|
|
428
|
+
): string | null {
|
|
429
|
+
if (lane === 'html' || lane === 'css' || lane === 'annotations') return text;
|
|
430
|
+
let parsed: unknown;
|
|
431
|
+
try {
|
|
432
|
+
parsed = parseJsonSafe(text);
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
if (lane === 'meta') {
|
|
437
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
438
|
+
return sharedMetaCanonical(parsed as Record<string, unknown>);
|
|
439
|
+
}
|
|
440
|
+
if (!Array.isArray(parsed)) return null;
|
|
441
|
+
const list = dedupeCommentsById(parsed);
|
|
442
|
+
return list.length ? JSON.stringify(list) : '';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** A lane's value as the accepted replica holds it (same canonical form). */
|
|
446
|
+
export function readLaneFromDoc(
|
|
447
|
+
doc: Y.Doc,
|
|
448
|
+
lane: 'html' | 'css' | 'meta' | 'annotations' | 'comments'
|
|
449
|
+
): string {
|
|
450
|
+
if (lane === 'html' || lane === 'css' || lane === 'meta') return doc.getText(lane).toString();
|
|
451
|
+
if (lane === 'annotations') {
|
|
452
|
+
const svg = doc.getMap<unknown>(Y_TYPES.annotations).get('svg');
|
|
453
|
+
return typeof svg === 'string' ? svg : '';
|
|
454
|
+
}
|
|
455
|
+
const list = doc.getArray<unknown>(Y_TYPES.comments).toArray();
|
|
456
|
+
return list.length ? JSON.stringify(list) : '';
|
|
457
|
+
}
|
|
458
|
+
|
|
304
459
|
export function metaFromDoc(doc: Y.Doc): string | null {
|
|
305
460
|
return normalizeSharedMeta(doc.getText(Y_SYNC_TYPES.meta).toString());
|
|
306
461
|
}
|
|
@@ -618,10 +773,19 @@ export function cssFromDoc(doc: Y.Doc): string | null {
|
|
|
618
773
|
}
|
|
619
774
|
|
|
620
775
|
/**
|
|
621
|
-
* Apply the canvas's `.css` to the doc as opaque Y.Text
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
776
|
+
* Apply the canvas's `.css` to the doc as opaque Y.Text.
|
|
777
|
+
*
|
|
778
|
+
* Goes through `applyTextLane` — the same prefix/suffix diff the body uses —
|
|
779
|
+
* and that is load-bearing, not tidiness (issue #114). This was a wholesale
|
|
780
|
+
* `delete(0, len)` + `insert(0, next)`, whose comment argued it "can't churn"
|
|
781
|
+
* because css round-trips byte-identically. True for a SINGLE writer, and the
|
|
782
|
+
* reason the lane looked fine in every single-peer test. Under concurrency it
|
|
783
|
+
* is the bug: two peers each replacing the lane keep both inserts, so the css
|
|
784
|
+
* doubled on a concurrent cold seed AND doubled again every time a peer tried
|
|
785
|
+
* to repair it. Measured in the field at 3×, 4× and 5×, while the body lane —
|
|
786
|
+
* same collision, diffing codec — never exceeded 3× and mostly self-healed.
|
|
787
|
+
*
|
|
788
|
+
* Returns false on over-cap / no change.
|
|
625
789
|
*/
|
|
626
790
|
export function applyCssToDoc(doc: Y.Doc, next: string, origin?: unknown): boolean {
|
|
627
791
|
if (byteLengthUtf8(next) > MAX_CSS_BYTES) {
|
|
@@ -630,11 +794,5 @@ export function applyCssToDoc(doc: Y.Doc, next: string, origin?: unknown): boole
|
|
|
630
794
|
);
|
|
631
795
|
return false;
|
|
632
796
|
}
|
|
633
|
-
|
|
634
|
-
if (t.toString() === next) return false;
|
|
635
|
-
doc.transact(() => {
|
|
636
|
-
if (t.length > 0) t.delete(0, t.length);
|
|
637
|
-
t.insert(0, next);
|
|
638
|
-
}, origin);
|
|
639
|
-
return true;
|
|
797
|
+
return applyTextLane(doc, Y_SYNC_TYPES.css, next, origin);
|
|
640
798
|
}
|
|
@@ -39,6 +39,13 @@
|
|
|
39
39
|
|
|
40
40
|
import type { ColdStartAction, ColdStartDecision } from './cold-start.ts';
|
|
41
41
|
|
|
42
|
+
/** Why a cold-start snapshot was taken.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately NOT extended for the duplication collapse (issue #114). A
|
|
45
|
+
* collapse only ever fires on an exact integer repeat, so the discarded copies
|
|
46
|
+
* hold nothing the retained one doesn't — and `pruneSnapshots` evicts
|
|
47
|
+
* oldest-first per slug, so a valueless snapshot on a hub-triggerable path is
|
|
48
|
+
* an eviction lever against the copies that carry real bytes. */
|
|
42
49
|
export type ColdStartSnapshotReason = 'pre-sync-local' | 'pre-sync-hub';
|
|
43
50
|
|
|
44
51
|
export interface ColdStartConflictInfo {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// unavailable) or equal → hub wins, exactly the v1.1 default — but now both
|
|
20
20
|
// sides are snapshotted first, so even a wrong pick costs one /design:rollback.
|
|
21
21
|
|
|
22
|
+
import { commentKey } from './comment-identity.ts';
|
|
22
23
|
import { hashBytes } from './echo-guard.ts';
|
|
23
24
|
|
|
24
25
|
export type ColdStartAction =
|
|
@@ -75,13 +76,6 @@ export function isExactRepeat(docBody: string, localBody: string): boolean {
|
|
|
75
76
|
return docBody === localBody.repeat(n);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function commentId(c: unknown): string | null {
|
|
79
|
-
if (c && typeof c === 'object' && typeof (c as { id?: unknown }).id === 'string') {
|
|
80
|
-
return (c as { id: string }).id;
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
79
|
/**
|
|
86
80
|
* Union-merge two comment snapshots by stable `id` (DDR-102): doc order first,
|
|
87
81
|
* local-only entries appended. Comments carry stable ids, so union loses
|
|
@@ -90,22 +84,30 @@ function commentId(c: unknown): string | null {
|
|
|
90
84
|
* cold-start paths (agent reconcile + migrate-seed).
|
|
91
85
|
*/
|
|
92
86
|
export function unionCommentsById(docList: unknown[], localList: unknown[]): unknown[] {
|
|
93
|
-
const out = [
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
const out: unknown[] = [];
|
|
88
|
+
const seen = new Set<string>();
|
|
89
|
+
// THE DOC IS DEDUPED AGAINST ITSELF, not just against local (issue #114/#112).
|
|
90
|
+
// `out` used to be seeded as `[...docList]`, which made this union a filter on
|
|
91
|
+
// the LOCAL side only — so a doc whose comments array had already been
|
|
92
|
+
// concurrency-doubled (`applyCommentsToDoc` is delete-all + push: two peers
|
|
93
|
+
// replacing at once keep both pushes) carried every copy forward, and the
|
|
94
|
+
// merged result was re-published as the new truth. That is the amplifier
|
|
95
|
+
// behind "the comment self duplicated like 8 times": 2 → 4 → 8, one doubling
|
|
96
|
+
// per concurrent round, with nothing in the loop able to shrink it again.
|
|
97
|
+
// Deduping the doc half turns the same pass into the repair.
|
|
98
|
+
// `commentKey` is the SHARED identity rule (comment-identity.ts) — the same
|
|
99
|
+
// one `applyCommentsToDoc`'s diff and the room seed use. It has to be shared:
|
|
100
|
+
// if the union's notion of "same comment" ever drifted from the codec's, one
|
|
101
|
+
// layer's repair would be the other's duplicate.
|
|
102
|
+
const admit = (c: unknown): void => {
|
|
103
|
+
const k = commentKey(c);
|
|
104
|
+
if (seen.has(k)) return;
|
|
105
|
+
seen.add(k);
|
|
105
106
|
out.push(c);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
};
|
|
108
|
+
// Doc order first (same-id entries keep the doc's version), local-only appended.
|
|
109
|
+
for (const c of docList) admit(c);
|
|
110
|
+
for (const c of localList) admit(c);
|
|
109
111
|
return out;
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -292,3 +294,97 @@ export function decideAnnotationsColdStart(
|
|
|
292
294
|
reason: `diverged — no per-lane stamp on one side, following the body winner (${bodyWinner})`,
|
|
293
295
|
};
|
|
294
296
|
}
|
|
297
|
+
|
|
298
|
+
/* ------------------------------------------------------- css (per-lane) */
|
|
299
|
+
|
|
300
|
+
export interface CssColdStartInput {
|
|
301
|
+
/** Local sibling `.css` content, or null when the file doesn't exist. */
|
|
302
|
+
local: string | null;
|
|
303
|
+
/** The css currently held by the doc (`null` when the lane is unset/empty). */
|
|
304
|
+
doc: string | null;
|
|
305
|
+
/** Journal entry's `cssHash` for this slug, or null when never checkpointed. */
|
|
306
|
+
journalHash: string | null;
|
|
307
|
+
/** Hash fn — passed in so this table stays as dependency-free as the rest. */
|
|
308
|
+
hash: (s: string) => string;
|
|
309
|
+
/** How the body lane resolved; the tie-break for a genuine divergence. */
|
|
310
|
+
bodyWinner: 'local' | 'hub';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface CssColdStartDecision {
|
|
314
|
+
winner: 'local' | 'hub' | 'none';
|
|
315
|
+
/** True when local won because the doc held local repeated N≥2 times. */
|
|
316
|
+
recoveredDuplication?: boolean;
|
|
317
|
+
reason: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Per-lane cold-start resolution for the canvas's sibling `.css` (issue #114).
|
|
322
|
+
*
|
|
323
|
+
* WHY THIS TABLE EXISTS. Every other Plane-A lane had one — `decideColdStart`
|
|
324
|
+
* for the body, `decideAnnotationsColdStart` for annotations, `repairSharedMeta`
|
|
325
|
+
* for meta — and css had none: it simply followed the body winner and re-applied
|
|
326
|
+
* the whole file whenever the two sides disagreed. That left it the only lane
|
|
327
|
+
* with no duplication guard at all, and `applyCssToDoc` is a delete-all+insert,
|
|
328
|
+
* so two peers seeding one empty document concurrently kept BOTH inserts and the
|
|
329
|
+
* lane became `CSS × N` (Yjs merges concurrent inserts at the same position by
|
|
330
|
+
* keeping both — not a corruption, the CRDT doing its job). Worse, the "repair"
|
|
331
|
+
* was itself another full insert, so every subsequent disagreement could double
|
|
332
|
+
* it again. Measured on the reporting machine's 84 persisted docs: the body lane
|
|
333
|
+
* was clean 21 times out of 84, the css lane ZERO times out of 43, and css
|
|
334
|
+
* carried strictly more copies than its own document's body in 43 of 43.
|
|
335
|
+
* Per-lane vigilance is what failed; a table per lane is the shape that holds.
|
|
336
|
+
*
|
|
337
|
+
* The rows mirror `decideColdStart`'s, in the same order and for the same
|
|
338
|
+
* reasons — crucially the `recover-seed-dup` row, checked BEFORE the journal
|
|
339
|
+
* fast-forward so a doubled lane is collapsed rather than read as "the hub moved
|
|
340
|
+
* ahead". Like the body's, it repairs only what it can PROVE: an exact integer
|
|
341
|
+
* repeat of the bytes on disk. A doc carrying genuinely new css is never an
|
|
342
|
+
* exact repeat, so this cannot clobber an edit.
|
|
343
|
+
*/
|
|
344
|
+
export function decideCssColdStart(input: CssColdStartInput): CssColdStartDecision {
|
|
345
|
+
const { local, doc, journalHash, hash, bodyWinner } = input;
|
|
346
|
+
const localEmpty = local === null || local === '';
|
|
347
|
+
const docEmpty = doc === null || doc === '';
|
|
348
|
+
|
|
349
|
+
if (localEmpty && docEmpty) return { winner: 'none', reason: 'both sides empty' };
|
|
350
|
+
if (localEmpty) return { winner: 'hub', reason: 'no local css — materializing the hub lane' };
|
|
351
|
+
if (docEmpty) {
|
|
352
|
+
// The body lane's DDR-064 guard, applied here: an empty css lane means the
|
|
353
|
+
// hub holds no css for this canvas YET, never "this canvas has no css".
|
|
354
|
+
// Without this row a local `.css` next to a hub-winning body never travelled
|
|
355
|
+
// at all — it sat on one disk and no peer ever saw it.
|
|
356
|
+
return { winner: 'local', reason: 'hub css lane empty — seeding local css up (DDR-064 guard)' };
|
|
357
|
+
}
|
|
358
|
+
if (doc === local) return { winner: 'none', reason: 'local and hub css identical' };
|
|
359
|
+
|
|
360
|
+
// Concurrent cold-seed collision, css edition — see the header. Collapsing is
|
|
361
|
+
// re-applying local: `applyCssToDoc` replaces the lane wholesale, so the
|
|
362
|
+
// repeated copies go and one stays.
|
|
363
|
+
if (isExactRepeat(doc, local)) {
|
|
364
|
+
return {
|
|
365
|
+
winner: 'local',
|
|
366
|
+
recoveredDuplication: true,
|
|
367
|
+
reason:
|
|
368
|
+
'hub css is local repeated — concurrent cold-seed duplication; collapsing to one copy',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// The journal checkpoint the css lane has been WRITING since DDR-102 and never
|
|
373
|
+
// reading. Same meaning as the body's: every local byte was already reconciled
|
|
374
|
+
// through this machine, so the hub is simply ahead.
|
|
375
|
+
if (journalHash !== null && journalHash === hash(local)) {
|
|
376
|
+
return {
|
|
377
|
+
winner: 'hub',
|
|
378
|
+
reason: 'local css matches the last-synced journal hash — hub is ahead, fast-forwarding',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Genuine divergence. The css is visually coupled to the body, so it follows
|
|
383
|
+
// the body's resolution rather than inventing a second, possibly disagreeing
|
|
384
|
+
// winner for the same canvas — the pre-existing behaviour, now reached only
|
|
385
|
+
// when the rows above have ruled out the cases they can decide on their own.
|
|
386
|
+
return {
|
|
387
|
+
winner: bodyWinner,
|
|
388
|
+
reason: `css diverged — following the body winner (${bodyWinner})`,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Comment identity — the one definition of "these two entries are the same
|
|
2
|
+
// comment", shared by every layer that touches the comments lane (issue #112).
|
|
3
|
+
//
|
|
4
|
+
// A LEAF module, exactly like `limits.ts`: it imports NOTHING. That is
|
|
5
|
+
// load-bearing, not tidiness. `collab/persistence.ts` owns `Y_TYPES` and
|
|
6
|
+
// `sync/codec.ts` imports it from there, so persistence can never import the
|
|
7
|
+
// codec back (its own comment says so) — yet the room seed needs the same
|
|
8
|
+
// identity rule the codec's diff uses. A dependency-free leaf is the only
|
|
9
|
+
// place both sides can reach.
|
|
10
|
+
//
|
|
11
|
+
// WHY IDENTITY IS A SHARED CONCERN. Comments travel over the hub as a Y.Array,
|
|
12
|
+
// and the lane's whole failure mode is the same logical comment appearing as
|
|
13
|
+
// several distinct CRDT items (issue #112: 1 → 2 → 4 → 8). Every layer that
|
|
14
|
+
// writes the lane has to agree on which entries are "the same one", or one
|
|
15
|
+
// layer's repair is another layer's duplicate.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stable identity for one comment entry.
|
|
19
|
+
*
|
|
20
|
+
* `id` is the real key — it is minted once by `commentsAdd` and survives every
|
|
21
|
+
* round-trip through disk, the Y.Array and the hub. Entries WITHOUT a string
|
|
22
|
+
* `id` (hand-edited files, pre-Phase-6 rows, junk pushed by an untrusted peer)
|
|
23
|
+
* fall back to JSON identity, which is deliberately conservative: it dedupes
|
|
24
|
+
* only byte-identical entries and never merges two rows that might differ.
|
|
25
|
+
*
|
|
26
|
+
* The `id:` / `json:` prefixes keep the two spaces from colliding — a comment
|
|
27
|
+
* whose id is literally the JSON of another entry can't shadow it.
|
|
28
|
+
*/
|
|
29
|
+
export function commentKey(c: unknown): string {
|
|
30
|
+
if (c && typeof c === 'object' && typeof (c as { id?: unknown }).id === 'string') {
|
|
31
|
+
return `id:${(c as { id: string }).id}`;
|
|
32
|
+
}
|
|
33
|
+
return `json:${JSON.stringify(c)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Collapse a comment list to one entry per identity, keeping the FIRST
|
|
38
|
+
* occurrence.
|
|
39
|
+
*
|
|
40
|
+
* First-wins is the same rule `unionCommentsById` uses ("doc order first"), and
|
|
41
|
+
* the two must agree: `commentsAddReply` mutates the first match, so the copy
|
|
42
|
+
* the dedupe keeps has to be the copy the mutations edit.
|
|
43
|
+
*
|
|
44
|
+
* Returns the input array itself when there is nothing to drop, so callers can
|
|
45
|
+
* cheaply test `deduped !== list` to know whether a write is worth making.
|
|
46
|
+
*/
|
|
47
|
+
export function dedupeCommentsById<T>(list: readonly T[]): T[] {
|
|
48
|
+
const seen = new Set<string>();
|
|
49
|
+
const out: T[] = [];
|
|
50
|
+
for (const c of list) {
|
|
51
|
+
const k = commentKey(c);
|
|
52
|
+
if (seen.has(k)) continue;
|
|
53
|
+
seen.add(k);
|
|
54
|
+
out.push(c);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when `list` carries the same comment identity more than once. */
|
|
60
|
+
export function hasDuplicateComments(list: readonly unknown[]): boolean {
|
|
61
|
+
const seen = new Set<string>();
|
|
62
|
+
for (const c of list) {
|
|
63
|
+
const k = commentKey(c);
|
|
64
|
+
if (seen.has(k)) return true;
|
|
65
|
+
seen.add(k);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
@@ -310,7 +310,21 @@ export function createConnectionMonitor(opts: ConnectionMonitorOptions = {}): Co
|
|
|
310
310
|
const wasOffline = state === 'offline' || state === 'offline-long';
|
|
311
311
|
clearAllTimers();
|
|
312
312
|
state = 'online';
|
|
313
|
-
lastSyncAt = now()
|
|
313
|
+
// NOT `lastSyncAt = now()`.
|
|
314
|
+
//
|
|
315
|
+
// A SOCKET TRANSITION IS NOT A SYNC. This line used to stamp the
|
|
316
|
+
// last-synced clock from the WS coming back — an event that, by itself,
|
|
317
|
+
// has moved no document and settled no handshake. The field's own contract
|
|
318
|
+
// one screen up already says otherwise ("updated on REAL sync activity
|
|
319
|
+
// (noteSyncActivity), not just on offline→online transitions"); the code
|
|
320
|
+
// simply did not honour it.
|
|
321
|
+
//
|
|
322
|
+
// It was not a cosmetic drift. Live on the reported project (issue #118):
|
|
323
|
+
// `state:"online", docs:{synced:0,pending:85}` with `lastSyncAt` refreshed
|
|
324
|
+
// to the exact millisecond of that transition — so the one number a reader
|
|
325
|
+
// could have used to notice that nothing had synced in 15 minutes was
|
|
326
|
+
// being refreshed BY the failure. `noteSyncActivity` is the only honest
|
|
327
|
+
// source, and it is now the only one.
|
|
314
328
|
offlineSince = null;
|
|
315
329
|
queuedOps = 0;
|
|
316
330
|
if (wasOffline) {
|
|
@@ -95,6 +95,8 @@ export interface CtlProviderOptions {
|
|
|
95
95
|
token: string;
|
|
96
96
|
/** Fired with the hub's head on every well-formed poke. */
|
|
97
97
|
onPoke: (head: number) => void;
|
|
98
|
+
/** Optional fast document-list invalidation; older consumers use onPoke. */
|
|
99
|
+
onDocuments?: () => void;
|
|
98
100
|
documentName?: string;
|
|
99
101
|
log?: Pick<Console, 'log' | 'warn' | 'error'>;
|
|
100
102
|
/** Injected in tests; production builds one from `@hocuspocus/provider`. */
|
|
@@ -155,9 +157,19 @@ export function createCtlProvider(opts: CtlProviderOptions): CtlProvider {
|
|
|
155
157
|
|
|
156
158
|
const wire = (p: CtlProviderLike): void => {
|
|
157
159
|
p.on('status', (data: { status?: string }) => {
|
|
158
|
-
if (typeof data?.status
|
|
160
|
+
if (typeof data?.status !== 'string') return;
|
|
161
|
+
const reconnect = data.status === 'connected' && status !== 'connected';
|
|
162
|
+
status = data.status;
|
|
163
|
+
if (reconnect && !stopped) {
|
|
164
|
+
try {
|
|
165
|
+
opts.onDocuments?.();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
log.error?.(`[sync/ctl] discovery handler threw: ${(error as Error).message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
159
170
|
});
|
|
160
171
|
p.on('stateless', (data: { payload: string }) => {
|
|
172
|
+
if (stopped) return;
|
|
161
173
|
const poke = parsePoke(data?.payload);
|
|
162
174
|
if (poke === null) {
|
|
163
175
|
malformed += 1;
|
|
@@ -172,7 +184,8 @@ export function createCtlProvider(opts: CtlProviderOptions): CtlProvider {
|
|
|
172
184
|
}
|
|
173
185
|
received += 1;
|
|
174
186
|
try {
|
|
175
|
-
|
|
187
|
+
if (poke.documents && opts.onDocuments) opts.onDocuments();
|
|
188
|
+
else opts.onPoke(poke.head);
|
|
176
189
|
} catch (err) {
|
|
177
190
|
log.error?.(`[sync/ctl] poke handler threw: ${(err as Error).message}`);
|
|
178
191
|
}
|