@readymadeit/mcp 0.0.0-stage → 0.1.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 +114 -2
- package/bundle/LICENSE +201 -0
- package/bundle/boards/aggregator-demo-board.json +54 -0
- package/bundle/boards/alpaca-board.json +316 -0
- package/bundle/boards/animate-board.json +159 -0
- package/bundle/boards/ascii-art-demo-board.json +33 -0
- package/bundle/boards/ascii-cam-board.json +184 -0
- package/bundle/boards/asset-demo-board.json +54 -0
- package/bundle/boards/audio-encode-demo-board.json +153 -0
- package/bundle/boards/audiolize-board.json +152 -0
- package/bundle/boards/batcher-demo-board.json +43 -0
- package/bundle/boards/buffer-demo-board.json +53 -0
- package/bundle/boards/cache-demo-board.json +59 -0
- package/bundle/boards/camera-demo-board.json +28 -0
- package/bundle/boards/canvas-demo-board.json +55 -0
- package/bundle/boards/changes-demo-board.json +60 -0
- package/bundle/boards/court-booking-browser-demo-board.json +221 -0
- package/bundle/boards/court-booking-demo-board.json +218 -0
- package/bundle/boards/current-time-board.json +32 -0
- package/bundle/boards/debounce-demo-board.json +43 -0
- package/bundle/boards/decrypt-demo-board.json +47 -0
- package/bundle/boards/delay-demo-board.json +28 -0
- package/bundle/boards/detect-demo-board.json +64 -0
- package/bundle/boards/document-extract-demo-board.json +55 -0
- package/bundle/boards/dropitapp-board.json +138 -0
- package/bundle/boards/encode-demo-board.json +39 -0
- package/bundle/boards/encrypt-board.json +185 -0
- package/bundle/boards/encrypt-demo-board.json +39 -0
- package/bundle/boards/face-alert-board.json +181 -0
- package/bundle/boards/fetcher-demo-board.json +40 -0
- package/bundle/boards/fft-demo-board.json +28 -0
- package/bundle/boards/filter-demo-board.json +55 -0
- package/bundle/boards/flat-map-demo-board.json +42 -0
- package/bundle/boards/frame-limit-test-board.json +130 -0
- package/bundle/boards/game-board.json +413 -0
- package/bundle/boards/game-of-life-board.json +139 -0
- package/bundle/boards/game-of-life-demo-board.json +34 -0
- package/bundle/boards/gather-demo-board.json +62 -0
- package/bundle/boards/group-by-demo-board.json +62 -0
- package/bundle/boards/hacker-demo-board.json +45 -0
- package/bundle/boards/hacker-news-board.json +741 -0
- package/bundle/boards/hash-demo-board.json +40 -0
- package/bundle/boards/health-log-browser-demo-board.json +537 -0
- package/bundle/boards/health-log-demo-board.json +498 -0
- package/bundle/boards/helloworld-board.json +57 -0
- package/bundle/boards/hold-demo-board.json +67 -0
- package/bundle/boards/hold-scopes-demo-board.json +141 -0
- package/bundle/boards/http-client-browser-demo-board.json +379 -0
- package/bundle/boards/http-client-demo-board.json +380 -0
- package/bundle/boards/image-picker-demo-board.json +22 -0
- package/bundle/boards/imap-email-demo-board.json +35 -0
- package/bundle/boards/injector-demo-board.json +55 -0
- package/bundle/boards/iterator-demo-board.json +55 -0
- package/bundle/boards/limit-demo-board.json +66 -0
- package/bundle/boards/link-debugger.json +32 -0
- package/bundle/boards/live-location-demo-board.json +93 -0
- package/bundle/boards/live-radio-cloud-demo-board.json +216 -0
- package/bundle/boards/live-radio-cloud-direct-demo-board.json +311 -0
- package/bundle/boards/live-radio-demo-board.json +211 -0
- package/bundle/boards/live-radio-members-demo-board.json +314 -0
- package/bundle/boards/local-storage-demo-board.json +30 -0
- package/bundle/boards/lz-compress-demo-board.json +52 -0
- package/bundle/boards/map-demo-board.json +46 -0
- package/bundle/boards/match-filter-demo-board.json +43 -0
- package/bundle/boards/meeting-poll-browser-demo-board.json +649 -0
- package/bundle/boards/meeting-poll-demo-board.json +650 -0
- package/bundle/boards/microphone-monitor-demo-board.json +28 -0
- package/bundle/boards/microphone-speaker-demo-board.json +86 -0
- package/bundle/boards/monitor-demo-board.json +52 -0
- package/bundle/boards/mounted-endpoint-demo-board-hkp-rt.json +84 -0
- package/bundle/boards/mounted-endpoint-demo-board.json +84 -0
- package/bundle/boards/nested-rhythm-demo-board.json +1160 -0
- package/bundle/boards/next-bus-board.json +52 -0
- package/bundle/boards/next-bus-switch-board.json +79 -0
- package/bundle/boards/night-groove-demo-board.json +742 -0
- package/bundle/boards/noise-alert-board.json +146 -0
- package/bundle/boards/offer-followup-demo-board.json +291 -0
- package/bundle/boards/offer-intake-demo-board.json +289 -0
- package/bundle/boards/offer-intake-hetzner-demo-board.json +286 -0
- package/bundle/boards/offer-intake-imap-demo-board.json +289 -0
- package/bundle/boards/offer-intake-imap-hetzner-demo-board.json +354 -0
- package/bundle/boards/p2p-receiver-demo-board.json +65 -0
- package/bundle/boards/p2p-sender-demo-board.json +94 -0
- package/bundle/boards/pad-groove-demo-board.json +644 -0
- package/bundle/boards/peer-chat-board.json +109 -0
- package/bundle/boards/peer-chat-node-board.json +97 -0
- package/bundle/boards/qr-code-demo-board.json +22 -0
- package/bundle/boards/queue-demo-board.json +239 -0
- package/bundle/boards/reduce-demo-board.json +54 -0
- package/bundle/boards/remote-named-demo-board-hkp-rt.json +61 -0
- package/bundle/boards/remote-named-demo-board.json +61 -0
- package/bundle/boards/rss-demo-board.json +530 -0
- package/bundle/boards/rss-radio-board.json +515 -0
- package/bundle/boards/rss-radio-brief-one-board.json +237 -0
- package/bundle/boards/rss-radio-editor-unit-board.json +555 -0
- package/bundle/boards/rss-radio-library-unit-board.json +293 -0
- package/bundle/boards/rss-radio-unit-board.json +809 -0
- package/bundle/boards/rss-radio-voice-unit-board.json +146 -0
- package/bundle/boards/runtime-configs/hold-example.json +41 -0
- package/bundle/boards/runtime-configs/injector-example.json +23 -0
- package/bundle/boards/runtime-configs/speech-to-text-example.json +35 -0
- package/bundle/boards/runtime-configs/text-generation-example.json +31 -0
- package/bundle/boards/runtime-configs/text-to-speech-example.json +28 -0
- package/bundle/boards/runtime-configs/text-to-speech-inflect-example.json +28 -0
- package/bundle/boards/runtime-configs/ws-audio-playback-example.json +23 -0
- package/bundle/boards/select-demo-board.json +32 -0
- package/bundle/boards/send-ntfy-board.json +52 -0
- package/bundle/boards/shared-costs-demo-board.json +361 -0
- package/bundle/boards/sign-demo-board.json +41 -0
- package/bundle/boards/skill-router-demo-board.json +210 -0
- package/bundle/boards/smooth-demo-board.json +91 -0
- package/bundle/boards/smtp-email-demo-board.json +34 -0
- package/bundle/boards/sort-demo-board.json +55 -0
- package/bundle/boards/sound-demo-board.json +65 -0
- package/bundle/boards/spectral-modifier-board.json +98 -0
- package/bundle/boards/speech-synth-demo-board.json +37 -0
- package/bundle/boards/speech-to-text-demo-board.json +146 -0
- package/bundle/boards/spotify-board.json +75 -0
- package/bundle/boards/spread-demo-board.json +35 -0
- package/bundle/boards/sql-demo-board.json +56 -0
- package/bundle/boards/sql-explorer-board.json +447 -0
- package/bundle/boards/sql-explorer-node-board.json +474 -0
- package/bundle/boards/sql-import-board.json +156 -0
- package/bundle/boards/state-demo-board.json +65 -0
- package/bundle/boards/stopper-demo-board.json +40 -0
- package/bundle/boards/storage-demo-board.json +193 -0
- package/bundle/boards/store-demo-board.json +63 -0
- package/bundle/boards/swimlanes-demo-board.json +197 -0
- package/bundle/boards/switch-demo-board.json +51 -0
- package/bundle/boards/syn-board.json +19 -0
- package/bundle/boards/syn-booking-unit-board.json +998 -0
- package/bundle/boards/syn-conversations-anthropic-demo-board.json +925 -0
- package/bundle/boards/syn-conversations-demo-board.json +925 -0
- package/bundle/boards/syn-hotels-unit-board.json +287 -0
- package/bundle/boards/syn-mail-desk-board.json +25 -0
- package/bundle/boards/syn-mail-desk-unit-board.json +254 -0
- package/bundle/boards/telegram-listener-demo-board.json +31 -0
- package/bundle/boards/text-generation-anthropic-demo-board.json +66 -0
- package/bundle/boards/text-generation-demo-board.json +51 -0
- package/bundle/boards/text-generation-local-demo-board.json +80 -0
- package/bundle/boards/text-to-speech-demo-board.json +58 -0
- package/bundle/boards/timeline-demo-board.json +477 -0
- package/bundle/boards/timer-demo-board.json +35 -0
- package/bundle/boards/tracks-demo-board.json +95 -0
- package/bundle/boards/trigger-pad-demo-board.json +47 -0
- package/bundle/boards/triggerpad-board.json +31 -0
- package/bundle/boards/triggerpad-recorder-board.json +56 -0
- package/bundle/boards/uuid-generator-demo-board.json +166 -0
- package/bundle/boards/voice-assistant-demo-board.json +230 -0
- package/bundle/boards/voice-assistant-hkp-rt-demo-board.json +239 -0
- package/bundle/boards/voice-assistant-local-demo-board.json +232 -0
- package/bundle/boards/voice-assistant-skills-demo-board.json +316 -0
- package/bundle/boards/voice-assistant-skills-local-demo-board.json +316 -0
- package/bundle/boards/xy-pad-demo-board.json +36 -0
- package/bundle/docs/content/board-json.md +277 -0
- package/bundle/docs/content/boards/alpaca-board.md +56 -0
- package/bundle/docs/content/boards/animate-board.md +40 -0
- package/bundle/docs/content/boards/ascii-cam-board.md +37 -0
- package/bundle/docs/content/boards/audiolize-board.md +47 -0
- package/bundle/docs/content/boards/court-booking-browser-demo-board.md +43 -0
- package/bundle/docs/content/boards/court-booking-demo-board.md +120 -0
- package/bundle/docs/content/boards/current-time-board.md +32 -0
- package/bundle/docs/content/boards/dropitapp-board.md +57 -0
- package/bundle/docs/content/boards/encrypt-board.md +36 -0
- package/bundle/docs/content/boards/face-alert-board.md +50 -0
- package/bundle/docs/content/boards/game-board.md +69 -0
- package/bundle/docs/content/boards/game-of-life-board.md +47 -0
- package/bundle/docs/content/boards/hacker-news-board.md +76 -0
- package/bundle/docs/content/boards/health-log-browser-demo-board.md +56 -0
- package/bundle/docs/content/boards/health-log-demo-board.md +138 -0
- package/bundle/docs/content/boards/helloworld-board.md +40 -0
- package/bundle/docs/content/boards/link-debugger.md +31 -0
- package/bundle/docs/content/boards/live-radio-cloud-demo-board.md +70 -0
- package/bundle/docs/content/boards/live-radio-cloud-direct-demo-board.md +89 -0
- package/bundle/docs/content/boards/live-radio-demo-board.md +102 -0
- package/bundle/docs/content/boards/live-radio-members-demo-board.md +91 -0
- package/bundle/docs/content/boards/meeting-poll-browser-demo-board.md +45 -0
- package/bundle/docs/content/boards/meeting-poll-demo-board.md +121 -0
- package/bundle/docs/content/boards/mounted-endpoint-demo-board-hkp-rt.md +50 -0
- package/bundle/docs/content/boards/nested-rhythm-demo-board.md +165 -0
- package/bundle/docs/content/boards/next-bus-board.md +38 -0
- package/bundle/docs/content/boards/next-bus-switch-board.md +44 -0
- package/bundle/docs/content/boards/night-groove-demo-board.md +110 -0
- package/bundle/docs/content/boards/noise-alert-board.md +44 -0
- package/bundle/docs/content/boards/pad-groove-demo-board.md +93 -0
- package/bundle/docs/content/boards/peer-chat-board.md +49 -0
- package/bundle/docs/content/boards/peer-chat-node-board.md +47 -0
- package/bundle/docs/content/boards/rss-radio-board.md +221 -0
- package/bundle/docs/content/boards/send-ntfy-board.md +33 -0
- package/bundle/docs/content/boards/shared-costs-demo-board.md +65 -0
- package/bundle/docs/content/boards/spectral-modifier-board.md +55 -0
- package/bundle/docs/content/boards/spotify-board.md +46 -0
- package/bundle/docs/content/boards/sql-explorer-board.md +70 -0
- package/bundle/docs/content/boards/sql-explorer-node-board.md +76 -0
- package/bundle/docs/content/boards/sql-import-board.md +37 -0
- package/bundle/docs/content/boards/swimlanes-demo-board.md +148 -0
- package/bundle/docs/content/boards/syn-mail-desk-board.md +52 -0
- package/bundle/docs/content/boards/timeline-demo-board.md +115 -0
- package/bundle/docs/content/boards/triggerpad-board.md +32 -0
- package/bundle/docs/content/boards/triggerpad-recorder-board.md +52 -0
- package/bundle/docs/content/boards/voice-assistant-demo-board.md +51 -0
- package/bundle/docs/content/concepts/assets.md +245 -0
- package/bundle/docs/content/concepts/blocks.md +300 -0
- package/bundle/docs/content/concepts/board-assistant.md +307 -0
- package/bundle/docs/content/concepts/board.md +265 -0
- package/bundle/docs/content/concepts/cloud-boards.md +803 -0
- package/bundle/docs/content/concepts/coordinator.md +297 -0
- package/bundle/docs/content/concepts/logging.md +293 -0
- package/bundle/docs/content/concepts/mounts.md +314 -0
- package/bundle/docs/content/concepts/presets.md +360 -0
- package/bundle/docs/content/concepts/remotes.md +423 -0
- package/bundle/docs/content/concepts/runtime.md +664 -0
- package/bundle/docs/content/concepts/scopes.md +256 -0
- package/bundle/docs/content/concepts/service.md +227 -0
- package/bundle/docs/content/concepts/units.md +321 -0
- package/bundle/docs/content/introduction.md +163 -0
- package/bundle/docs/content/llm/circle-text-board.json +55 -0
- package/bundle/docs/content/llm/howto-workflow.md +369 -0
- package/bundle/docs/content/repository.md +161 -0
- package/bundle/docs/content/services/aggregator.md +76 -0
- package/bundle/docs/content/services/analyzer.md +44 -0
- package/bundle/docs/content/services/ascii-art.md +58 -0
- package/bundle/docs/content/services/asset.md +88 -0
- package/bundle/docs/content/services/audio-editor.md +57 -0
- package/bundle/docs/content/services/audio-encode.md +115 -0
- package/bundle/docs/content/services/audio-io.md +173 -0
- package/bundle/docs/content/services/batcher.md +49 -0
- package/bundle/docs/content/services/board-service.md +48 -0
- package/bundle/docs/content/services/browser-sub-service.md +75 -0
- package/bundle/docs/content/services/buffer.md +70 -0
- package/bundle/docs/content/services/cache.md +92 -0
- package/bundle/docs/content/services/camera.md +85 -0
- package/bundle/docs/content/services/canvas.md +196 -0
- package/bundle/docs/content/services/changes.md +87 -0
- package/bundle/docs/content/services/chunked-file-provider.md +63 -0
- package/bundle/docs/content/services/communication-dispatcher.md +190 -0
- package/bundle/docs/content/services/configurator.md +66 -0
- package/bundle/docs/content/services/conversations.md +244 -0
- package/bundle/docs/content/services/debounce.md +45 -0
- package/bundle/docs/content/services/decrypt.md +53 -0
- package/bundle/docs/content/services/delay.md +55 -0
- package/bundle/docs/content/services/detect.md +101 -0
- package/bundle/docs/content/services/document-extract.md +174 -0
- package/bundle/docs/content/services/download.md +67 -0
- package/bundle/docs/content/services/encode.md +63 -0
- package/bundle/docs/content/services/encrypt.md +53 -0
- package/bundle/docs/content/services/fetcher.md +122 -0
- package/bundle/docs/content/services/fft.md +96 -0
- package/bundle/docs/content/services/file-source.md +67 -0
- package/bundle/docs/content/services/filesystem.md +157 -0
- package/bundle/docs/content/services/filter.md +133 -0
- package/bundle/docs/content/services/flat-map.md +62 -0
- package/bundle/docs/content/services/game-of-life.md +93 -0
- package/bundle/docs/content/services/gather.md +135 -0
- package/bundle/docs/content/services/gif-encoder.md +56 -0
- package/bundle/docs/content/services/github.md +85 -0
- package/bundle/docs/content/services/group-by.md +54 -0
- package/bundle/docs/content/services/hacker.md +79 -0
- package/bundle/docs/content/services/hash.md +59 -0
- package/bundle/docs/content/services/hold.md +182 -0
- package/bundle/docs/content/services/http-relay-client.md +48 -0
- package/bundle/docs/content/services/http-uploader.md +62 -0
- package/bundle/docs/content/services/http.md +424 -0
- package/bundle/docs/content/services/if.md +79 -0
- package/bundle/docs/content/services/ifft.md +46 -0
- package/bundle/docs/content/services/image-picker.md +50 -0
- package/bundle/docs/content/services/imap-email.md +104 -0
- package/bundle/docs/content/services/injector.md +129 -0
- package/bundle/docs/content/services/input.md +90 -0
- package/bundle/docs/content/services/iterator.md +186 -0
- package/bundle/docs/content/services/join.md +153 -0
- package/bundle/docs/content/services/limit.md +47 -0
- package/bundle/docs/content/services/local-storage.md +47 -0
- package/bundle/docs/content/services/looper.md +62 -0
- package/bundle/docs/content/services/lz-compress.md +52 -0
- package/bundle/docs/content/services/map.md +257 -0
- package/bundle/docs/content/services/match-filter.md +38 -0
- package/bundle/docs/content/services/microphone-monitor.md +66 -0
- package/bundle/docs/content/services/monitor.md +67 -0
- package/bundle/docs/content/services/ollama.md +116 -0
- package/bundle/docs/content/services/openai-prompt.md +74 -0
- package/bundle/docs/content/services/output.md +107 -0
- package/bundle/docs/content/services/peer-socket.md +61 -0
- package/bundle/docs/content/services/qr-code.md +48 -0
- package/bundle/docs/content/services/queue.md +201 -0
- package/bundle/docs/content/services/reduce.md +51 -0
- package/bundle/docs/content/services/rss.md +232 -0
- package/bundle/docs/content/services/select.md +47 -0
- package/bundle/docs/content/services/sign.md +53 -0
- package/bundle/docs/content/services/skill-router.md +154 -0
- package/bundle/docs/content/services/smooth.md +59 -0
- package/bundle/docs/content/services/smtp-email.md +129 -0
- package/bundle/docs/content/services/sort.md +48 -0
- package/bundle/docs/content/services/sound.md +87 -0
- package/bundle/docs/content/services/speech-synth.md +46 -0
- package/bundle/docs/content/services/speech-to-text.md +172 -0
- package/bundle/docs/content/services/spotify.md +77 -0
- package/bundle/docs/content/services/spread.md +111 -0
- package/bundle/docs/content/services/sql.md +491 -0
- package/bundle/docs/content/services/state.md +57 -0
- package/bundle/docs/content/services/static.md +47 -0
- package/bundle/docs/content/services/stopper.md +55 -0
- package/bundle/docs/content/services/storage.md +155 -0
- package/bundle/docs/content/services/store.md +255 -0
- package/bundle/docs/content/services/sub-service.md +152 -0
- package/bundle/docs/content/services/switch.md +67 -0
- package/bundle/docs/content/services/telegram-listener.md +76 -0
- package/bundle/docs/content/services/text-generation.md +390 -0
- package/bundle/docs/content/services/text-to-speech.md +130 -0
- package/bundle/docs/content/services/timeline.md +333 -0
- package/bundle/docs/content/services/timer.md +113 -0
- package/bundle/docs/content/services/tracks.md +207 -0
- package/bundle/docs/content/services/trigger-pad.md +68 -0
- package/bundle/docs/content/services/uuid-generator.md +48 -0
- package/bundle/docs/content/services/wav-reader.md +66 -0
- package/bundle/docs/content/services/websocket.md +189 -0
- package/bundle/docs/content/services/workflow-board-builder.md +65 -0
- package/bundle/docs/content/services/xy-pad.md +60 -0
- package/bundle/docs/content/targets.md +434 -0
- package/bundle/docs/content/testing.md +263 -0
- package/bundle/docs/content/vocabulary.md +171 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRegistry.tsx +141 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntime.tsx +202 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeApi.ts +306 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeApp.ts +310 -0
- package/bundle/hkp-frontend/src/runtime/browser/BrowserRuntimeScope.ts +500 -0
- package/bundle/hkp-frontend/src/runtime/browser/UIRegistry.ts +150 -0
- package/bundle/hkp-frontend/src/runtime/browser/assetResolver.ts +123 -0
- package/bundle/hkp-frontend/src/runtime/browser/pipelineNames.ts +65 -0
- package/bundle/hkp-frontend/src/runtime/browser/registry/Default.ts +237 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Aggregator.ts +127 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AggregatorUI.tsx +83 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Analyzer.tsx +43 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/Bars.tsx +33 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/HorizontalSpectrogram.tsx +53 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/VerticalSpectogram.tsx +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/Waveform.tsx +109 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AnalyzerUI/index.tsx +99 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AsciiArt.ts +119 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AsciiArtUI.tsx +131 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Asset.ts +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AssetUI.tsx +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioDeviceSettings.tsx +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioEditor.ts +129 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioEditorUI.tsx +549 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioInput.ts +298 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioInputUI.tsx +257 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioOutput.ts +189 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/AudioOutputUI.tsx +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Batcher.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BoardService.tsx +86 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BoardServiceUI.tsx +410 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserGather.ts +311 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserIterator.ts +167 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserSubService.tsx +583 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserSubServiceUI.tsx +187 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/BrowserTracks.tsx +477 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Buffer.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Cache.ts +134 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CacheUI.tsx +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Camera.ts +90 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CameraUI.tsx +321 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Canvas.ts +139 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/canvasDraw.ts +543 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/canvasUtils.ts +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/index.tsx +287 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/canvasDraw.test.ts +355 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/canvasUtils.test.ts +89 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/CanvasUI/tests/fullscreen.test.tsx +26 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Changes.ts +201 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ChangesUI.tsx +85 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ChunkedFileProvider.tsx +183 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Configurator.ts +258 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ConfiguratorUI.tsx +83 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ConsideredHacker.tsx +28 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Debounce.ts +75 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DebounceUI.tsx +70 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Decrypt.ts +127 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DecryptUI.tsx +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Delay.tsx +110 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Detect.ts +214 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DetectUI.tsx +86 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Download.ts +120 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/DownloadUI.tsx +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Encode.ts +138 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/EncodeUI.tsx +52 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Encrypt.ts +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/EncryptUI.tsx +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FFT.tsx +238 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FFTUI.tsx +112 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FeedbackService.ts +247 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FeedbackServiceUI.tsx +35 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Fetcher.tsx +232 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FetcherUI.tsx +154 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FileSource.tsx +171 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Filter.tsx +18 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FilterUI.tsx +89 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FlatMap.ts +68 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/FlatMapUI.tsx +38 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLife.ts +210 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeRenderer.ts +182 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeRendererUI.tsx +48 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GameOfLifeUI.tsx +58 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GatherUI.tsx +84 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GifEncoder.tsx +651 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubAPI.ts +294 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubComponents.tsx +389 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSink.tsx +109 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSinkUI.tsx +143 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSource.tsx +86 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GithubSourceUI.tsx +162 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/GroupBy.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HTTPUploader.ts +102 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Hacker.ts +73 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HackerUI.tsx +134 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Hash.ts +134 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HashUI.tsx +45 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Hold.ts +288 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HoldUI.tsx +43 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HttpClient.ts +558 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HttpClientUI.tsx +212 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/HttpUploaderTokenInjector.ts +149 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/IfService.ts +231 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/IfServiceUI.tsx +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ImagePicker.tsx +118 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Injector.ts +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/InjectorUI.tsx +389 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Input.ts +214 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/InputUI.tsx +71 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/IteratorUI.tsx +59 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LZCompress.ts +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Limit.ts +39 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LimitUI.tsx +41 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LocalStorage.ts +65 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/LocalStorageUI.tsx +72 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Map.ts +14 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MapUI.tsx +334 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MapUIModeAssist.tsx +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MatchFilter.ts +11 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MicrophoneMonitor.ts +190 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MicrophoneMonitorUI.tsx +94 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Monitor.tsx +15 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/MonitorUI.tsx +281 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPrompt.ts +237 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/BusyIndicator.tsx +18 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/PromptInput.tsx +71 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/StreamingPreview.tsx +24 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OllamaPromptUI/index.tsx +156 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OpenAIPrompt.ts +254 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OpenAIPromptUI.tsx +189 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Output.tsx +145 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/OutputUI.tsx +118 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerChatTemplate.json +43 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerConnection.ts +260 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocket.tsx +268 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocketReceiverTemplate.json +29 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PeerSocketUI.tsx +341 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/PlaceholderUI.tsx +72 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/QrCode.ts +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/QrCodeUI.tsx +197 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Select.ts +40 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SelectUI.tsx +41 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/ServiceBase.ts +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SetRuntimeVariable.ts +65 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SetRuntimeVariableUI.tsx +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sign.ts +182 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SignUI.tsx +55 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Smooth.ts +101 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SmoothUI.tsx +50 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sort.ts +59 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SortUI.tsx +61 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sound.ts +541 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SoundUI.tsx +183 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpeechSynth.ts +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpeechSynthUI.tsx +108 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Spotify.tsx +111 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyAPI.ts +113 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyComponents.tsx +104 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpotifyUI.tsx +186 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Spread.ts +117 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SpreadUI.tsx +38 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Sql.ts +448 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SqlInspector.tsx +332 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SqlUI.tsx +226 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/State.ts +149 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Stopper.ts +29 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Switch.tsx +419 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/SwitchUI.tsx +171 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TickRuleDelta.ts +101 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TickRuleDeltaUI.tsx +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Timeline.ts +709 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/TimelinePanel.tsx +79 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/TimelineRows.tsx +574 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/Transport.tsx +141 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/index.tsx +119 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/model.ts +425 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimelineUI/parts.tsx +708 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/Timer.ts +15 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TimerUI.tsx +213 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TracksUI.tsx +81 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TriggerPad.ts +108 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/TriggerPadUI.tsx +160 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/UuidGenerator.ts +74 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/UuidGeneratorUI.tsx +59 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WaveformDisplay.tsx +104 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WebsocketClient.ts +114 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WebsocketClientUI.tsx +42 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WorkflowBoardBuilder.ts +499 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/WorkflowBoardBuilderUI.tsx +521 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/XYPad.ts +68 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/XYPadUI.tsx +274 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/audio/audioHelper.ts +36 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Batcher.ts +76 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Buffer.ts +142 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Filter.ts +125 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/GroupBy.ts +101 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Map.ts +543 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/MatchFilter.ts +49 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Monitor.ts +75 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Reduce.ts +74 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/Timer.ts +342 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/bundle.js +7375 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/eval.ts +208 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/expressionEval.ts +223 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/helpers.ts +10 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/base/index.ts +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/camera/videoCapture.ts +78 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/changes-modes.ts +9 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/colorpalette.ts +26 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/DetectionPreview.tsx +76 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/mediapipe.ts +34 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/models/README.md +14 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect/models/blaze_face_short_range.tflite +0 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/detect-modes.ts +7 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/encode-modes.ts +10 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/helpers.ts +300 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/http-client-methods.ts +10 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-database.ts +594 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-dump.ts +192 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-inspect.ts +130 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-inspector.css +155 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/sql-modes.ts +27 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/TESTING_APPROACH.md +169 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/__snapshots__/service-ui.future-enhancements.test.tsx.snap +5 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/audio-input-permissions.test.ts +42 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/audiolizeLanes.test.ts +70 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/browser-scopes.test.ts +305 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/cache-service.test.ts +53 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/camera-video-capture.test.ts +24 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/changes-service.test.ts +133 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/crypto-secrets.test.ts +196 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/detect-service.test.ts +119 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/download.test.ts +100 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/expressionEval.test.ts +117 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/fetcher-panel.test.tsx +35 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/fetcher-service.test.ts +80 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/filter-service.test.ts +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/github-secrets.test.ts +108 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/group-by-board.test.ts +42 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/hold-service.test.ts +199 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/http-client-service.test.ts +474 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/http-uploader-token-injector.test.ts +152 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/iterator-gather-service.test.ts +277 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/map-service.test.ts +1047 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/map-ui.test.tsx +70 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/nested-rhythm-board.test.ts +311 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/night-groove-board.test.ts +290 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/pad-groove-board.test.ts +230 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/peer-host-discovery.test.ts +77 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/peer-socket-discovery.test.ts +69 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/pipeline-names.test.ts +63 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/secret-canary.test.ts +121 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-modules.contract.test.ts +229 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.advanced.test.tsx +670 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.future-enhancements.test.tsx +219 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.integration.test.tsx +432 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/service-ui.smoke.test.tsx +140 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sound-service.test.ts +293 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/spread-service.test.ts +137 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-dump.test.ts +172 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-inspect.test.ts +163 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql-inspector.test.tsx +121 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sql.test.ts +702 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/sub-service-in-block.test.tsx +84 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/switch-service.test.ts +254 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-board.test.ts +218 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-service.test.ts +1002 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-ui-model.test.ts +230 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timeline-ui.test.tsx +371 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/timer-service.test.ts +902 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/tracks-service.test.ts +275 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/tracks-ui.test.tsx +126 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/triggerpad-recorder-board.test.ts +100 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/tests/uuid-generator-service.test.ts +214 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/timeline-core.ts +410 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/DefaultWorkflowBoard.ts +90 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/RefinementSession.ts +121 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/SystemPromptCatalog.ts +245 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/catalog/browser-service-specs.json +110 -0
- package/bundle/hkp-frontend/src/runtime/browser/services/workflow-prompt/catalog/runtime-capabilities.json +37 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/asset-service.test.ts +73 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/board-persistence.integration.test.tsx +391 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/browser-runtime.identity-binding.test.tsx +81 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/demo-boards.regression.test.tsx +386 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/failed-call-lifecycle.test.ts +56 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/http-client.integration.test.ts +146 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/nested-run.integration.test.ts +415 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/replay-next.test.ts +391 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/rerun.test.ts +71 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/run-no-input.test.ts +72 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-add.integration.test.tsx +256 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-operations.integration.test.tsx +310 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-processing.integration.test.ts +311 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-removal.integration.test.tsx +454 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/runtime-update.identity.integration.test.tsx +145 -0
- package/bundle/hkp-frontend/src/runtime/browser/tests/service-operations.integration.test.tsx +511 -0
- package/bundle/hkp-node/src/address.ts +72 -0
- package/bundle/hkp-node/src/assets.ts +582 -0
- package/bundle/hkp-node/src/auth.ts +369 -0
- package/bundle/hkp-node/src/callReport.ts +281 -0
- package/bundle/hkp-node/src/coordinator/auth.ts +85 -0
- package/bundle/hkp-node/src/coordinator/binaryFrame.ts +189 -0
- package/bundle/hkp-node/src/coordinator/boardStore.ts +0 -0
- package/bundle/hkp-node/src/coordinator/bridge.ts +242 -0
- package/bundle/hkp-node/src/coordinator/bridgeProtocol.ts +200 -0
- package/bundle/hkp-node/src/coordinator/coordinator.ts +594 -0
- package/bundle/hkp-node/src/coordinator/facadeAccess.ts +426 -0
- package/bundle/hkp-node/src/coordinator/fileBoardStore.ts +159 -0
- package/bundle/hkp-node/src/coordinator/frameLimits.ts +41 -0
- package/bundle/hkp-node/src/coordinator/index.ts +12 -0
- package/bundle/hkp-node/src/coordinator/join.ts +62 -0
- package/bundle/hkp-node/src/coordinator/logStore.ts +335 -0
- package/bundle/hkp-node/src/coordinator/members.ts +133 -0
- package/bundle/hkp-node/src/coordinator/mount.ts +124 -0
- package/bundle/hkp-node/src/coordinator/participantProtocol.ts +169 -0
- package/bundle/hkp-node/src/coordinator/participants.ts +666 -0
- package/bundle/hkp-node/src/coordinator/router.ts +416 -0
- package/bundle/hkp-node/src/coordinator/session.ts +1610 -0
- package/bundle/hkp-node/src/coordinator/types.ts +95 -0
- package/bundle/hkp-node/src/coordinatorLinks.ts +712 -0
- package/bundle/hkp-node/src/credentialedFetch.ts +122 -0
- package/bundle/hkp-node/src/egress.ts +344 -0
- package/bundle/hkp-node/src/expressionEval.ts +223 -0
- package/bundle/hkp-node/src/index.ts +470 -0
- package/bundle/hkp-node/src/instance.ts +172 -0
- package/bundle/hkp-node/src/mounts.ts +296 -0
- package/bundle/hkp-node/src/origins.ts +201 -0
- package/bundle/hkp-node/src/reset.ts +187 -0
- package/bundle/hkp-node/src/runtime.ts +1345 -0
- package/bundle/hkp-node/src/secrets.ts +292 -0
- package/bundle/hkp-node/src/server.ts +2120 -0
- package/bundle/hkp-node/src/services/asset.ts +150 -0
- package/bundle/hkp-node/src/services/claudeModels.ts +370 -0
- package/bundle/hkp-node/src/services/communication-dispatcher.ts +735 -0
- package/bundle/hkp-node/src/services/conversations.ts +914 -0
- package/bundle/hkp-node/src/services/database.ts +386 -0
- package/bundle/hkp-node/src/services/document-extract.ts +575 -0
- package/bundle/hkp-node/src/services/expression.ts +417 -0
- package/bundle/hkp-node/src/services/fileStore.ts +369 -0
- package/bundle/hkp-node/src/services/filesystem.ts +280 -0
- package/bundle/hkp-node/src/services/gather.ts +335 -0
- package/bundle/hkp-node/src/services/hold.ts +275 -0
- package/bundle/hkp-node/src/services/http-client.ts +525 -0
- package/bundle/hkp-node/src/services/http-server.ts +1222 -0
- package/bundle/hkp-node/src/services/imap-email.ts +627 -0
- package/bundle/hkp-node/src/services/injector.ts +135 -0
- package/bundle/hkp-node/src/services/iterator.ts +203 -0
- package/bundle/hkp-node/src/services/join.ts +226 -0
- package/bundle/hkp-node/src/services/map.ts +632 -0
- package/bundle/hkp-node/src/services/monitor.ts +128 -0
- package/bundle/hkp-node/src/services/nested-pipeline.ts +405 -0
- package/bundle/hkp-node/src/services/peer-server.ts +268 -0
- package/bundle/hkp-node/src/services/queue-store.ts +301 -0
- package/bundle/hkp-node/src/services/queue.ts +316 -0
- package/bundle/hkp-node/src/services/recordStore.ts +341 -0
- package/bundle/hkp-node/src/services/rss-parse.ts +281 -0
- package/bundle/hkp-node/src/services/rss.ts +392 -0
- package/bundle/hkp-node/src/services/smtp-email.ts +398 -0
- package/bundle/hkp-node/src/services/spread.ts +153 -0
- package/bundle/hkp-node/src/services/sql-dump.ts +192 -0
- package/bundle/hkp-node/src/services/sql.ts +429 -0
- package/bundle/hkp-node/src/services/stopper.ts +69 -0
- package/bundle/hkp-node/src/services/storage.ts +332 -0
- package/bundle/hkp-node/src/services/store.ts +609 -0
- package/bundle/hkp-node/src/services/stream.ts +367 -0
- package/bundle/hkp-node/src/services/sub-service.ts +600 -0
- package/bundle/hkp-node/src/services/telegram-listener.ts +263 -0
- package/bundle/hkp-node/src/services/telegram-sender.ts +136 -0
- package/bundle/hkp-node/src/services/text-generation.ts +1249 -0
- package/bundle/hkp-node/src/services/timer.ts +273 -0
- package/bundle/hkp-node/src/services/tracks.ts +381 -0
- package/bundle/hkp-node/src/services/websocket-reader.ts +364 -0
- package/bundle/hkp-node/src/types.ts +465 -0
- package/bundle/hkp-node/src/wsTickets.ts +77 -0
- package/bundle/hkp-node/src/yas.ts +95 -0
- package/bundle/hkp-python/src/hkp/__init__.py +1 -0
- package/bundle/hkp-python/src/hkp/__main__.py +241 -0
- package/bundle/hkp-python/src/hkp/address.py +56 -0
- package/bundle/hkp-python/src/hkp/assets.py +439 -0
- package/bundle/hkp-python/src/hkp/auth.py +252 -0
- package/bundle/hkp-python/src/hkp/binary_frame.py +100 -0
- package/bundle/hkp-python/src/hkp/call_report.py +201 -0
- package/bundle/hkp-python/src/hkp/coordinator_links.py +667 -0
- package/bundle/hkp-python/src/hkp/data.py +118 -0
- package/bundle/hkp-python/src/hkp/egress.py +310 -0
- package/bundle/hkp-python/src/hkp/mount.py +46 -0
- package/bundle/hkp-python/src/hkp/mounts.py +288 -0
- package/bundle/hkp-python/src/hkp/origins.py +169 -0
- package/bundle/hkp-python/src/hkp/runtime.py +1131 -0
- package/bundle/hkp-python/src/hkp/secrets.py +261 -0
- package/bundle/hkp-python/src/hkp/server.py +1714 -0
- package/bundle/hkp-python/src/hkp/services/__init__.py +0 -0
- package/bundle/hkp-python/src/hkp/services/asset.py +123 -0
- package/bundle/hkp-python/src/hkp/services/audio_encode.py +190 -0
- package/bundle/hkp-python/src/hkp/services/expression.py +942 -0
- package/bundle/hkp-python/src/hkp/services/gather.py +266 -0
- package/bundle/hkp-python/src/hkp/services/hold.py +246 -0
- package/bundle/hkp-python/src/hkp/services/http_client.py +444 -0
- package/bundle/hkp-python/src/hkp/services/http_server.py +790 -0
- package/bundle/hkp-python/src/hkp/services/iterator.py +141 -0
- package/bundle/hkp-python/src/hkp/services/join.py +173 -0
- package/bundle/hkp-python/src/hkp/services/map_service.py +452 -0
- package/bundle/hkp-python/src/hkp/services/monitor.py +99 -0
- package/bundle/hkp-python/src/hkp/services/nested_pipeline.py +278 -0
- package/bundle/hkp-python/src/hkp/services/skill_router.py +388 -0
- package/bundle/hkp-python/src/hkp/services/speech_to_text.py +186 -0
- package/bundle/hkp-python/src/hkp/services/spread.py +114 -0
- package/bundle/hkp-python/src/hkp/services/stopper.py +59 -0
- package/bundle/hkp-python/src/hkp/services/sub_service.py +417 -0
- package/bundle/hkp-python/src/hkp/services/text_generation.py +498 -0
- package/bundle/hkp-python/src/hkp/services/text_to_speech.py +237 -0
- package/bundle/hkp-python/src/hkp/services/timer.py +267 -0
- package/bundle/hkp-python/src/hkp/services/tracks.py +251 -0
- package/bundle/hkp-python/src/hkp/types.py +412 -0
- package/bundle/hkp-python/src/hkp/ws_tickets.py +54 -0
- package/bundle/hkp-python/src/hkp/yas.py +192 -0
- package/bundle/hkp-rt/lib/CMakeLists.txt +193 -0
- package/bundle/hkp-rt/lib/hkp-bundles/CMakeLists.txt +55 -0
- package/bundle/hkp-rt/lib/hkp-bundles/plugin_register.cpp +14 -0
- package/bundle/hkp-rt/lib/include/app.h +201 -0
- package/bundle/hkp-rt/lib/include/assets.h +239 -0
- package/bundle/hkp-rt/lib/include/auth.h +167 -0
- package/bundle/hkp-rt/lib/include/call_report.h +197 -0
- package/bundle/hkp-rt/lib/include/coordinator_links.h +127 -0
- package/bundle/hkp-rt/lib/include/discovery_hooks.h +29 -0
- package/bundle/hkp-rt/lib/include/egress.h +379 -0
- package/bundle/hkp-rt/lib/include/files.h +275 -0
- package/bundle/hkp-rt/lib/include/log_entry.h +131 -0
- package/bundle/hkp-rt/lib/include/mounts.h +139 -0
- package/bundle/hkp-rt/lib/include/origins.h +363 -0
- package/bundle/hkp-rt/lib/include/registry.h +119 -0
- package/bundle/hkp-rt/lib/include/secrets.h +414 -0
- package/bundle/hkp-rt/lib/include/server.h +122 -0
- package/bundle/hkp-rt/lib/include/service.h +190 -0
- package/bundle/hkp-rt/lib/include/slot_store.h +41 -0
- package/bundle/hkp-rt/lib/include/standalone_config.h +253 -0
- package/bundle/hkp-rt/lib/include/types/audio.h +3 -0
- package/bundle/hkp-rt/lib/include/types/data.h +152 -0
- package/bundle/hkp-rt/lib/include/types/message.h +56 -0
- package/bundle/hkp-rt/lib/include/types/models.h +154 -0
- package/bundle/hkp-rt/lib/include/types/owns_me.h +19 -0
- package/bundle/hkp-rt/lib/include/types/ringbuffer.h +86 -0
- package/bundle/hkp-rt/lib/include/types/types.h +33 -0
- package/bundle/hkp-rt/lib/include/types/validation.h +205 -0
- package/bundle/hkp-rt/lib/src/address.h +88 -0
- package/bundle/hkp-rt/lib/src/app.cpp +591 -0
- package/bundle/hkp-rt/lib/src/assets.cpp +686 -0
- package/bundle/hkp-rt/lib/src/binary_frame.h +154 -0
- package/bundle/hkp-rt/lib/src/common/expression.h +1481 -0
- package/bundle/hkp-rt/lib/src/common/inja.h +36 -0
- package/bundle/hkp-rt/lib/src/common/link_socket.cpp +437 -0
- package/bundle/hkp-rt/lib/src/common/link_socket.h +97 -0
- package/bundle/hkp-rt/lib/src/common/realtime_wakeup.h +120 -0
- package/bundle/hkp-rt/lib/src/common/stream_broadcast.h +250 -0
- package/bundle/hkp-rt/lib/src/common/string_util.h +31 -0
- package/bundle/hkp-rt/lib/src/common/websocket_client_session.cpp +217 -0
- package/bundle/hkp-rt/lib/src/common/websocket_client_session.h +37 -0
- package/bundle/hkp-rt/lib/src/common/websocket_config.h +119 -0
- package/bundle/hkp-rt/lib/src/common/websocket_protocol.h +32 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server.cpp +82 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server.h +57 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_listener.cpp +115 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_listener.h +35 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_session.cpp +172 -0
- package/bundle/hkp-rt/lib/src/common/websocket_server_session.h +53 -0
- package/bundle/hkp-rt/lib/src/common.h +13 -0
- package/bundle/hkp-rt/lib/src/coordinator_links.cpp +1142 -0
- package/bundle/hkp-rt/lib/src/discovery/discovery.cpp +707 -0
- package/bundle/hkp-rt/lib/src/discovery/discovery.h +89 -0
- package/bundle/hkp-rt/lib/src/http/auth.cpp +569 -0
- package/bundle/hkp-rt/lib/src/http/front_door.cpp +518 -0
- package/bundle/hkp-rt/lib/src/http/front_door.h +131 -0
- package/bundle/hkp-rt/lib/src/http/server.cpp +1539 -0
- package/bundle/hkp-rt/lib/src/http/ws_tickets.h +125 -0
- package/bundle/hkp-rt/lib/src/mount.h +63 -0
- package/bundle/hkp-rt/lib/src/process_context.h +335 -0
- package/bundle/hkp-rt/lib/src/process_depth.h +20 -0
- package/bundle/hkp-rt/lib/src/registry.cpp +233 -0
- package/bundle/hkp-rt/lib/src/runtime.cpp +986 -0
- package/bundle/hkp-rt/lib/src/runtime.h +225 -0
- package/bundle/hkp-rt/lib/src/runtime_host.h +180 -0
- package/bundle/hkp-rt/lib/src/service.cpp +315 -0
- package/bundle/hkp-rt/lib/src/services/asset.h +185 -0
- package/bundle/hkp-rt/lib/src/services/audio_encode.cpp +372 -0
- package/bundle/hkp-rt/lib/src/services/audio_encode.h +97 -0
- package/bundle/hkp-rt/lib/src/services/cache.h +362 -0
- package/bundle/hkp-rt/lib/src/services/cache_subservices.h +463 -0
- package/bundle/hkp-rt/lib/src/services/core_audio.cpp +384 -0
- package/bundle/hkp-rt/lib/src/services/core_audio.h +22 -0
- package/bundle/hkp-rt/lib/src/services/core_input.h +337 -0
- package/bundle/hkp-rt/lib/src/services/core_output.h +370 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/detail.cpp +749 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/detail.h +29 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/ffmpeg.cpp +193 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/ffmpeg.h +40 -0
- package/bundle/hkp-rt/lib/src/services/ffmpeg/types.h +120 -0
- package/bundle/hkp-rt/lib/src/services/fft_service.h +190 -0
- package/bundle/hkp-rt/lib/src/services/filesystem.h +309 -0
- package/bundle/hkp-rt/lib/src/services/filter.h +208 -0
- package/bundle/hkp-rt/lib/src/services/gather.h +272 -0
- package/bundle/hkp-rt/lib/src/services/hold.h +304 -0
- package/bundle/hkp-rt/lib/src/services/http_client.cpp +347 -0
- package/bundle/hkp-rt/lib/src/services/http_client.h +67 -0
- package/bundle/hkp-rt/lib/src/services/http_client_impl.h +45 -0
- package/bundle/hkp-rt/lib/src/services/http_client_template_examples.md +202 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_listener.cpp +130 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_listener.h +33 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server.cpp +351 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server.h +79 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_impl.cpp +230 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_impl.h +64 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_subservices.cpp +950 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_server_subservices.h +253 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_session.cpp +495 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_session.h +169 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_stream_listener.cpp +270 -0
- package/bundle/hkp-rt/lib/src/services/http_server/http_stream_listener.h +104 -0
- package/bundle/hkp-rt/lib/src/services/http_server/request_decode.h +164 -0
- package/bundle/hkp-rt/lib/src/services/if_service.h +210 -0
- package/bundle/hkp-rt/lib/src/services/ifft_service.h +198 -0
- package/bundle/hkp-rt/lib/src/services/injector.h +162 -0
- package/bundle/hkp-rt/lib/src/services/iterator.h +201 -0
- package/bundle/hkp-rt/lib/src/services/map.h +641 -0
- package/bundle/hkp-rt/lib/src/services/monitor.h +161 -0
- package/bundle/hkp-rt/lib/src/services/mp3_frames.h +119 -0
- package/bundle/hkp-rt/lib/src/services/mp4_to_wav.cpp +281 -0
- package/bundle/hkp-rt/lib/src/services/mp4_to_wav.h +263 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_registry.cpp +88 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_registry.h +43 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server.h +163 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_listener.cpp +133 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_listener.h +46 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_session.cpp +165 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_server_session.h +44 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_ws_session.cpp +184 -0
- package/bundle/hkp-rt/lib/src/services/peer_server/peer_ws_session.h +55 -0
- package/bundle/hkp-rt/lib/src/services/receiver_session.h +172 -0
- package/bundle/hkp-rt/lib/src/services/root_certificates.h +3959 -0
- package/bundle/hkp-rt/lib/src/services/sha256_openssl.h +49 -0
- package/bundle/hkp-rt/lib/src/services/speech_http.h +306 -0
- package/bundle/hkp-rt/lib/src/services/speech_to_text.cpp +595 -0
- package/bundle/hkp-rt/lib/src/services/speech_to_text.h +69 -0
- package/bundle/hkp-rt/lib/src/services/static.cpp +45 -0
- package/bundle/hkp-rt/lib/src/services/static.h +41 -0
- package/bundle/hkp-rt/lib/src/services/stopper.h +72 -0
- package/bundle/hkp-rt/lib/src/services/sub_service.h +286 -0
- package/bundle/hkp-rt/lib/src/services/text_generation.cpp +1132 -0
- package/bundle/hkp-rt/lib/src/services/text_generation.h +71 -0
- package/bundle/hkp-rt/lib/src/services/text_to_speech.cpp +853 -0
- package/bundle/hkp-rt/lib/src/services/text_to_speech.h +97 -0
- package/bundle/hkp-rt/lib/src/services/timer.h +300 -0
- package/bundle/hkp-rt/lib/src/services/tracks.h +397 -0
- package/bundle/hkp-rt/lib/src/services/tts_inflect.cpp +149 -0
- package/bundle/hkp-rt/lib/src/services/tts_inflect.h +82 -0
- package/bundle/hkp-rt/lib/src/services/wav_reader.h +213 -0
- package/bundle/hkp-rt/lib/src/services/websocket_client_service.h +182 -0
- package/bundle/hkp-rt/lib/src/services/websocket_reader.h +148 -0
- package/bundle/hkp-rt/lib/src/services/websocket_server_service.h +196 -0
- package/bundle/hkp-rt/lib/src/services/websocket_writer.cpp +641 -0
- package/bundle/hkp-rt/lib/src/services/websocket_writer.h +117 -0
- package/bundle/hkp-rt/lib/src/sub_runtime.cpp +234 -0
- package/bundle/hkp-rt/lib/src/sub_runtime.h +160 -0
- package/bundle/hkp-rt/lib/src/types/data.cpp +237 -0
- package/bundle/hkp-rt/lib/src/types/message.cpp +148 -0
- package/bundle/hkp-rt/lib/src/types/models.cpp +83 -0
- package/bundle/hkp-rt/lib/src/types/ringbuffer.cpp +336 -0
- package/bundle/hkp-rt/lib/src/uuid.h +12 -0
- package/bundle/readymade-mcp-commits.json +5 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +79 -0
- package/dist/git.d.ts +8 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +53 -0
- package/dist/http.d.ts +17 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +130 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/repository.d.ts +49 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +452 -0
- package/dist/schemas.d.ts +156 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +43 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +83 -0
- package/dist/types.d.ts +63 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/validation.d.ts +9 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +101 -0
- package/package.json +52 -4
|
@@ -0,0 +1,1610 @@
|
|
|
1
|
+
import { WebSocket } from "ws";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import {
|
|
4
|
+
CloudBoardConfig,
|
|
5
|
+
CloudRuntimeDescriptor,
|
|
6
|
+
BoardSessionStatus,
|
|
7
|
+
isRemoteRuntime,
|
|
8
|
+
isBrowserRuntime,
|
|
9
|
+
} from "./types";
|
|
10
|
+
import { LogStore } from "./logStore";
|
|
11
|
+
import {
|
|
12
|
+
BinaryPayload,
|
|
13
|
+
decodeBinaryFrame,
|
|
14
|
+
encodeBinaryFrame,
|
|
15
|
+
frameBytes,
|
|
16
|
+
} from "./binaryFrame";
|
|
17
|
+
import { Caller, LogEntry, LogLevel, ProcessContext } from "../types";
|
|
18
|
+
import { MOUNT_FIELD, collectMountRefs, formatMountRef } from "./mount";
|
|
19
|
+
import {
|
|
20
|
+
BridgeMessage,
|
|
21
|
+
RuntimeSnapshot,
|
|
22
|
+
ServiceStates,
|
|
23
|
+
isBridgeMessage,
|
|
24
|
+
} from "./bridgeProtocol";
|
|
25
|
+
import {
|
|
26
|
+
BoardParticipants,
|
|
27
|
+
NO_PARTICIPANTS,
|
|
28
|
+
Participant,
|
|
29
|
+
ParticipantEvent,
|
|
30
|
+
ProvisionResult,
|
|
31
|
+
ReportedService,
|
|
32
|
+
} from "./participantProtocol";
|
|
33
|
+
import { AssetDescriptor } from "../assets";
|
|
34
|
+
import {
|
|
35
|
+
FacadeAccess,
|
|
36
|
+
projectConfig,
|
|
37
|
+
projectNotification,
|
|
38
|
+
projectState,
|
|
39
|
+
readFacadeAccess,
|
|
40
|
+
runtimeHolding,
|
|
41
|
+
} from "./facadeAccess";
|
|
42
|
+
import { DEFAULT_MEMBER_LIMITS, MemberLimits } from "./members";
|
|
43
|
+
import { frameLimitFrom } from "./frameLimits";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* What a browser attached to a board is to it.
|
|
47
|
+
*
|
|
48
|
+
* The **owner** deployed the board: their bridge is sent the board whole, may
|
|
49
|
+
* configure its services, and hosts its browser runtimes. A **member** is
|
|
50
|
+
* somebody the board is shared with: their bridge is sent the facade and what
|
|
51
|
+
* the facade reads, and may ask for exactly what the facade asks for.
|
|
52
|
+
*/
|
|
53
|
+
export type BridgeRole = "owner" | "member";
|
|
54
|
+
|
|
55
|
+
/** Who attached, as whoever admitted them established it. */
|
|
56
|
+
export type BridgeAttach = {
|
|
57
|
+
role: BridgeRole;
|
|
58
|
+
/** Absent where there is no identity to state — a coordinator without auth. */
|
|
59
|
+
caller?: Caller;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type BrowserBridge = BridgeAttach & {
|
|
63
|
+
ws: WebSocket;
|
|
64
|
+
runtimeIds: Set<string>;
|
|
65
|
+
// Each browser is told the board in its own sequence: what a member is sent
|
|
66
|
+
// is a subset of what the owner is, and a shared count would read to them as
|
|
67
|
+
// increments going missing.
|
|
68
|
+
seq: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** Closing a member's bridge because the board is no longer shared with them.
|
|
72
|
+
* Final: a client that reconnected would only be refused. */
|
|
73
|
+
export const CLOSE_NOT_A_MEMBER = 4403;
|
|
74
|
+
|
|
75
|
+
/** Machinery a runtime reports about its own flow, carrying the data passing
|
|
76
|
+
* through. A facade ignores it, so a member is not sent it. */
|
|
77
|
+
function isFlowNotification(payload: unknown): boolean {
|
|
78
|
+
return (
|
|
79
|
+
!!payload &&
|
|
80
|
+
typeof payload === "object" &&
|
|
81
|
+
"__internal" in (payload as Record<string, unknown>)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* One board, owned by this coordinator.
|
|
87
|
+
*
|
|
88
|
+
* The session dials nothing. Every runtime the board has is run by a
|
|
89
|
+
* *participant* that connected in — a runtime server holding a ticket for that
|
|
90
|
+
* runtime, or a browser attached over the bridge — and the session builds,
|
|
91
|
+
* configures and drives the board over those connections.
|
|
92
|
+
*
|
|
93
|
+
* Participants are of two kinds, told apart by the runtime's type:
|
|
94
|
+
*
|
|
95
|
+
* - **Required** — a remote runtime. The board cannot run without it, so one
|
|
96
|
+
* that is not connected puts the board in `error`, naming it. That is not
|
|
97
|
+
* terminal: when its runtime server connects, the runtime is built from the
|
|
98
|
+
* board's config and the board goes back to `running`.
|
|
99
|
+
* - **Transient** — a browser runtime, run by whichever browser has the board
|
|
100
|
+
* open. Browsers come and go as a matter of course, so data arriving for one
|
|
101
|
+
* that is away stops there, the way a `null` stops a pipeline, and the board
|
|
102
|
+
* stays `running`.
|
|
103
|
+
*/
|
|
104
|
+
export class BoardSession {
|
|
105
|
+
readonly createdAt: string;
|
|
106
|
+
// Set while the board is not being run: stopped by its owner, or restored
|
|
107
|
+
// from a store that says it was.
|
|
108
|
+
private stopped = false;
|
|
109
|
+
// Why each runtime is not as the board wants it, by runtime id.
|
|
110
|
+
private readonly runtimeErrors = new Map<string, string>();
|
|
111
|
+
// What stopping could not release. Kept apart from the above because it
|
|
112
|
+
// describes the run that ended, not the one being attempted.
|
|
113
|
+
private residue: string[] = [];
|
|
114
|
+
// The runtimes that produced `residue`, by id. A stopped board only removes
|
|
115
|
+
// one that reconnects when it knows that runtime is a leftover from its own
|
|
116
|
+
// run. An editor may legitimately recreate the same runtime id after Stop;
|
|
117
|
+
// treating every runtime seen while stopped as residue would delete the
|
|
118
|
+
// editor's live work during the next deploy introduction.
|
|
119
|
+
private readonly pendingRelease = new Set<string>();
|
|
120
|
+
// Runtimes this session has built or picked back up, whose participant is
|
|
121
|
+
// connected right now.
|
|
122
|
+
private readonly live = new Set<string>();
|
|
123
|
+
// Runtimes this session built at some point. One whose participant dropped
|
|
124
|
+
// and came back with the runtime still running is picked up, not rebuilt.
|
|
125
|
+
private readonly built = new Set<string>();
|
|
126
|
+
// Capture mode: whether this board's runtimes send their call reports with
|
|
127
|
+
// the values in them. The owner's, switched while attached, and held here
|
|
128
|
+
// only — never in the config, never persisted — so a restart or a redeploy
|
|
129
|
+
// turns it off. What passes inside a run is private unless someone building
|
|
130
|
+
// the board asked to see it (plans/TODO-CAPTURE-MODE.md).
|
|
131
|
+
private capture = false;
|
|
132
|
+
// Every browser currently viewing this board has its own bridge. Multiple
|
|
133
|
+
// clients (e.g. Readymade + a browser tab) can watch the same board at once;
|
|
134
|
+
// runtime output is fanned out to all of them.
|
|
135
|
+
private readonly bridges = new Set<BrowserBridge>();
|
|
136
|
+
private readonly pendingBrowserResults = new Map<
|
|
137
|
+
string,
|
|
138
|
+
{ resolve: (data: unknown) => void; timer: ReturnType<typeof setTimeout> }
|
|
139
|
+
>();
|
|
140
|
+
// Addresses services published for the mounts they own, keyed
|
|
141
|
+
// "runtimeId/serviceUuid". This session coordinates the board, so it is what
|
|
142
|
+
// turns a reference into the address it names — no runtime can see far enough
|
|
143
|
+
// to do it for itself.
|
|
144
|
+
private readonly mountAddresses = new Map<string, string>();
|
|
145
|
+
// The address last handed to each consumer, keyed the same way. Guards
|
|
146
|
+
// against re-configuring a service with what it already has, while still
|
|
147
|
+
// letting a *changed* address through.
|
|
148
|
+
private readonly pushedStates = new Map<string, string>();
|
|
149
|
+
// What each remote runtime's services last reported, and what that runtime
|
|
150
|
+
// says it can run. This is the board as the coordinator knows it, and what an
|
|
151
|
+
// attached browser renders from — it owns none of it itself.
|
|
152
|
+
private readonly serviceStates = new Map<string, ServiceStates>();
|
|
153
|
+
private readonly registries = new Map<string, unknown[]>();
|
|
154
|
+
// What the board's facade lets a member do and see. The config is fixed for
|
|
155
|
+
// the life of a session — deploying again makes a new one — so it is read
|
|
156
|
+
// once.
|
|
157
|
+
private readonly access: FacadeAccess;
|
|
158
|
+
// When each member last asked for something to be processed, by email, for
|
|
159
|
+
// the rate that bounds them.
|
|
160
|
+
private readonly memberProcessTimes = new Map<string, number[]>();
|
|
161
|
+
// Building, releasing and picking up runtimes happen one at a time: a
|
|
162
|
+
// participant arriving while the board is starting must not be built twice.
|
|
163
|
+
private queue: Promise<unknown> = Promise.resolve();
|
|
164
|
+
private unsubscribe: (() => void) | null = null;
|
|
165
|
+
private destroyed = false;
|
|
166
|
+
|
|
167
|
+
constructor(
|
|
168
|
+
readonly boardName: string,
|
|
169
|
+
readonly userId: string,
|
|
170
|
+
readonly config: CloudBoardConfig,
|
|
171
|
+
// The runtime servers connected for this board, and word of their coming
|
|
172
|
+
// and going. Supplied by the coordinator, which holds the tickets.
|
|
173
|
+
private readonly participants: BoardParticipants = NO_PARTICIPANTS,
|
|
174
|
+
// Set when the board comes back from a store rather than from a deploy. It
|
|
175
|
+
// keeps the date the board was first registered, and whether it was left
|
|
176
|
+
// stopped. See docs/content/concepts/cloud-boards.md.
|
|
177
|
+
restored?: { createdAt: string; stopped: boolean },
|
|
178
|
+
// Where this board's entries are kept. Absent means nothing is collected —
|
|
179
|
+
// a session in a test, or a coordinator configured without a log root.
|
|
180
|
+
private readonly logStore?: LogStore,
|
|
181
|
+
// The largest frame a browser may send that is passed on; unset means no
|
|
182
|
+
// limit. The same setting bounds what a runtime server sends, where the
|
|
183
|
+
// participants are accepted.
|
|
184
|
+
private readonly limits: { maxFrameBytes?: number } & MemberLimits = {},
|
|
185
|
+
// Checked at every coordinator boundary. The coordinator owns membership,
|
|
186
|
+
// so runtimes cannot decide whether a captured person is still entitled.
|
|
187
|
+
private readonly mayActAs: (caller: Caller) => boolean = () => true,
|
|
188
|
+
) {
|
|
189
|
+
this.createdAt = restored?.createdAt ?? new Date().toISOString();
|
|
190
|
+
this.stopped = restored?.stopped ?? false;
|
|
191
|
+
this.access = readFacadeAccess(config.facade);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The runtimes this board cannot run without, in chain order. */
|
|
195
|
+
private required(): CloudRuntimeDescriptor[] {
|
|
196
|
+
return this.config.runtimes.filter(isRemoteRuntime);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private serial<T>(work: () => Promise<T>): Promise<T> {
|
|
200
|
+
const run = this.queue.then(work, work);
|
|
201
|
+
this.queue = run.catch(() => undefined);
|
|
202
|
+
return run;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Runs the board: builds every required runtime whose participant is
|
|
207
|
+
* connected, and from then on builds the others as they arrive.
|
|
208
|
+
*
|
|
209
|
+
* A participant that is not connected is not waited for. The board says
|
|
210
|
+
* which one is missing and comes up when it does.
|
|
211
|
+
*/
|
|
212
|
+
async start(): Promise<void> {
|
|
213
|
+
await this.serial(async () => {
|
|
214
|
+
this.stopped = false;
|
|
215
|
+
this.residue = [];
|
|
216
|
+
this.pendingRelease.clear();
|
|
217
|
+
this.unsubscribe ??= this.participants.subscribe({
|
|
218
|
+
onJoin: (participant) => this.onJoin(participant),
|
|
219
|
+
onLeave: (runtimeId) => this.onLeave(runtimeId),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
for (const runtime of this.required()) {
|
|
223
|
+
const participant = this.participants.get(runtime.id);
|
|
224
|
+
if (participant) {
|
|
225
|
+
await this.bringUp(runtime, participant);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Runtimes are built one by one, so a service pointed at a mount on a
|
|
230
|
+
// runtime built later cannot have been resolved as it was created. Hand
|
|
231
|
+
// out the addresses once the whole board exists.
|
|
232
|
+
await this.publishMountAddresses();
|
|
233
|
+
});
|
|
234
|
+
this.broadcastSnapshot();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Whether the board was left stopped, for a store to remember. */
|
|
238
|
+
isStopped(): boolean {
|
|
239
|
+
return this.stopped;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
getStatus(): BoardSessionStatus {
|
|
243
|
+
if (this.stopped) {
|
|
244
|
+
return "stopped";
|
|
245
|
+
}
|
|
246
|
+
return this.getErrors().length > 0 ? "error" : "running";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getErrors(): string[] {
|
|
250
|
+
if (this.stopped) {
|
|
251
|
+
return [...this.residue];
|
|
252
|
+
}
|
|
253
|
+
const errors: string[] = [];
|
|
254
|
+
for (const runtime of this.required()) {
|
|
255
|
+
const reason = this.runtimeErrors.get(runtime.id);
|
|
256
|
+
if (reason) {
|
|
257
|
+
errors.push(`Runtime "${runtime.id}": ${reason}`);
|
|
258
|
+
} else if (!this.live.has(runtime.id)) {
|
|
259
|
+
errors.push(
|
|
260
|
+
`Runtime "${runtime.id}" is not connected — its runtime server has to connect to this coordinator`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return errors;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Builds a runtime on the participant that connected for it, or picks it
|
|
269
|
+
* back up when that participant only dropped its connection and still has
|
|
270
|
+
* the runtime this session built.
|
|
271
|
+
*/
|
|
272
|
+
private async bringUp(
|
|
273
|
+
runtime: CloudRuntimeDescriptor,
|
|
274
|
+
participant: Participant,
|
|
275
|
+
): Promise<void> {
|
|
276
|
+
const { id } = runtime;
|
|
277
|
+
try {
|
|
278
|
+
let services: ReportedService[] | undefined;
|
|
279
|
+
if (this.built.has(id) && participant.hello.runtimeExists) {
|
|
280
|
+
// Its state is live and the board's is not: rebuilding would throw
|
|
281
|
+
// away what the runtime has been doing.
|
|
282
|
+
services = (
|
|
283
|
+
await participant.request<{ services?: ReportedService[] }>({
|
|
284
|
+
op: "describe",
|
|
285
|
+
})
|
|
286
|
+
)?.services;
|
|
287
|
+
this.registries.set(id, participant.hello.registry);
|
|
288
|
+
// Whatever stopped it from being picked up last time no longer does.
|
|
289
|
+
// What it was built without is still missing, and still said.
|
|
290
|
+
if (!this.runtimeErrors.get(id)?.startsWith("needs configuration")) {
|
|
291
|
+
this.runtimeErrors.delete(id);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
const result = await participant.request<ProvisionResult>({
|
|
295
|
+
op: "provision",
|
|
296
|
+
name: runtime.name,
|
|
297
|
+
boardName: this.boardName,
|
|
298
|
+
// The board's own settings for this runtime — `logData`, and
|
|
299
|
+
// whatever a runtime reads from its state later.
|
|
300
|
+
state: runtime.state ?? {},
|
|
301
|
+
services: (this.config.services[id] ?? []).map((svc) => ({
|
|
302
|
+
uuid: svc.uuid,
|
|
303
|
+
serviceId: svc.serviceId,
|
|
304
|
+
serviceName: svc.serviceName ?? svc.name ?? svc.serviceId,
|
|
305
|
+
state: svc.state ?? {},
|
|
306
|
+
})),
|
|
307
|
+
// Every asset this runtime is given, named by a service or not.
|
|
308
|
+
// Host-local content cannot travel this way — a `file://` source is
|
|
309
|
+
// read by the runtime, inside its own volumes.
|
|
310
|
+
assets: this.assetsFor(runtime),
|
|
311
|
+
});
|
|
312
|
+
this.registries.set(
|
|
313
|
+
id,
|
|
314
|
+
Array.isArray(result?.registry)
|
|
315
|
+
? result.registry
|
|
316
|
+
: participant.hello.registry,
|
|
317
|
+
);
|
|
318
|
+
services =
|
|
319
|
+
result?.services ??
|
|
320
|
+
(
|
|
321
|
+
await participant.request<{ services?: ReportedService[] }>({
|
|
322
|
+
op: "describe",
|
|
323
|
+
})
|
|
324
|
+
)?.services;
|
|
325
|
+
// A newly built runtime has been told nothing yet, so whatever was
|
|
326
|
+
// handed to the one before it has to be handed over again.
|
|
327
|
+
for (const svc of this.config.services[id] ?? []) {
|
|
328
|
+
this.pushedStates.delete(
|
|
329
|
+
formatMountRef({ runtimeId: id, serviceUuid: svc.uuid }),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
this.built.add(id);
|
|
333
|
+
// Rebuilt while the board is captured: it starts without Capture mode,
|
|
334
|
+
// as every runtime does, so it is told again. Not part of what it is
|
|
335
|
+
// built from, which a board document could otherwise switch on.
|
|
336
|
+
if (this.capture) {
|
|
337
|
+
await participant
|
|
338
|
+
.request({ op: "setState", state: { capture: true } })
|
|
339
|
+
.catch(() => {
|
|
340
|
+
// It runs without; its frames show activity but no values.
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (result?.missingSecrets?.length) {
|
|
344
|
+
// Built, and unable to do its job: say what is missing and where,
|
|
345
|
+
// rather than leave it to surface as a failed login hours later.
|
|
346
|
+
this.runtimeErrors.set(
|
|
347
|
+
id,
|
|
348
|
+
`needs configuration — its runtime server holds no value for ${result.missingSecrets.join(", ")}`,
|
|
349
|
+
);
|
|
350
|
+
} else {
|
|
351
|
+
this.runtimeErrors.delete(id);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.recordServices(id, services ?? []);
|
|
356
|
+
participant.listen((event) => this.onParticipantEvent(id, event));
|
|
357
|
+
this.live.add(id);
|
|
358
|
+
} catch (err) {
|
|
359
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
360
|
+
this.live.delete(id);
|
|
361
|
+
this.runtimeErrors.set(id, reason);
|
|
362
|
+
console.error(
|
|
363
|
+
`[coordinator] Failed to provision runtime "${id}" for board "${this.boardName}":`,
|
|
364
|
+
reason,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** A required participant connected, now or again. */
|
|
370
|
+
private onJoin(participant: Participant): void {
|
|
371
|
+
const runtime = this.required().find((rt) => rt.id === participant.runtimeId);
|
|
372
|
+
void this.serial(async () => {
|
|
373
|
+
if (this.destroyed) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (!runtime) {
|
|
377
|
+
// The board no longer contains this runtime. Its ticket is revoked by
|
|
378
|
+
// the coordinator immediately after the old session is destroyed, but
|
|
379
|
+
// a connection already arriving is still made harmless here.
|
|
380
|
+
if (participant.hello.runtimeExists) {
|
|
381
|
+
await participant.request({ op: "remove" }).catch(() => undefined);
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (this.stopped) {
|
|
386
|
+
// Only a runtime this session failed to release is ours to remove. A
|
|
387
|
+
// stopped board is also the normal state while a browser edits it, and
|
|
388
|
+
// that browser may have recreated the same runtime id on this server.
|
|
389
|
+
if (
|
|
390
|
+
participant.hello.runtimeExists &&
|
|
391
|
+
this.pendingRelease.has(participant.runtimeId)
|
|
392
|
+
) {
|
|
393
|
+
try {
|
|
394
|
+
await participant.request({ op: "remove" });
|
|
395
|
+
this.pendingRelease.delete(participant.runtimeId);
|
|
396
|
+
this.residue = this.residue.filter(
|
|
397
|
+
(line) => !line.startsWith(`Runtime "${participant.runtimeId}"`),
|
|
398
|
+
);
|
|
399
|
+
} catch {
|
|
400
|
+
// Still residue; the status continues to say so.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
await this.bringUp(runtime, participant);
|
|
406
|
+
await this.publishMountAddresses();
|
|
407
|
+
})
|
|
408
|
+
.catch((err) => {
|
|
409
|
+
console.error(
|
|
410
|
+
`[coordinator] Failed to take runtime "${participant.runtimeId}" back into board "${this.boardName}":`,
|
|
411
|
+
err instanceof Error ? err.message : err,
|
|
412
|
+
);
|
|
413
|
+
})
|
|
414
|
+
.finally(() => this.broadcastSnapshot());
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** A required participant's connection went away. */
|
|
418
|
+
private onLeave(runtimeId: string): void {
|
|
419
|
+
if (!this.live.delete(runtimeId)) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
console.log(
|
|
423
|
+
`[coordinator] Runtime "${runtimeId}" of board "${this.boardName}" disconnected`,
|
|
424
|
+
);
|
|
425
|
+
this.broadcastSnapshot();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private onParticipantEvent(runtimeId: string, event: ParticipantEvent): void {
|
|
429
|
+
// An entry from one of this board's runtimes. It is written before it is
|
|
430
|
+
// forwarded: a browser may or may not be attached, and the log is for the
|
|
431
|
+
// case where none is.
|
|
432
|
+
if (event.type === "log") {
|
|
433
|
+
if (event.entry) {
|
|
434
|
+
this.logStore?.append(this.userId, this.boardName, event.entry);
|
|
435
|
+
// The board's log is its owner's: an entry names services, runs and
|
|
436
|
+
// callers a member has no business seeing.
|
|
437
|
+
const caller = event.entry.caller
|
|
438
|
+
? this.knownCallers.get(event.entry.caller)
|
|
439
|
+
: undefined;
|
|
440
|
+
this.toOwners({
|
|
441
|
+
type: "log",
|
|
442
|
+
entry: event.entry,
|
|
443
|
+
...(caller ? { caller } : {}),
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// A service publishes the address of a mount it owns through a
|
|
450
|
+
// notification — when it is unbypassed at runtime, say, long after the
|
|
451
|
+
// board loaded. Whoever is waiting on that address learns of it here.
|
|
452
|
+
if (event.type === "notification") {
|
|
453
|
+
if (event.serviceUuid) {
|
|
454
|
+
this.onRuntimeNotification(
|
|
455
|
+
runtimeId,
|
|
456
|
+
event.serviceUuid,
|
|
457
|
+
event.payload,
|
|
458
|
+
event.context,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (event.data === null || event.data === undefined) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
this.routeResult(runtimeId, event.data, event.context).catch((err) => {
|
|
468
|
+
console.error(
|
|
469
|
+
`[coordinator] Failed to route result from runtime "${runtimeId}":`,
|
|
470
|
+
err instanceof Error ? err.message : err,
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Takes in what a runtime's services report: their state, and any mounts. */
|
|
476
|
+
private recordServices(runtimeId: string, services: ReportedService[]): void {
|
|
477
|
+
const states: ServiceStates = {};
|
|
478
|
+
for (const svc of services) {
|
|
479
|
+
if (!svc?.uuid) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
states[svc.uuid] = svc.state;
|
|
483
|
+
const published = svc.state?.[MOUNT_FIELD];
|
|
484
|
+
if (typeof published === "string" && published) {
|
|
485
|
+
this.recordMountAddress(runtimeId, svc.uuid, published);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
this.serviceStates.set(runtimeId, states);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Hands the board's runtimes back without giving up the board.
|
|
493
|
+
*
|
|
494
|
+
* This is what editing does: the browser takes the runtimes over and
|
|
495
|
+
* provisions them itself, so the coordinator must not still hold them — but
|
|
496
|
+
* the board keeps its place in the coordinator's list and keeps its config,
|
|
497
|
+
* because a board being edited must not be a board that can be lost.
|
|
498
|
+
* Attached browsers stay attached and are told the board is now empty.
|
|
499
|
+
*/
|
|
500
|
+
async stop(): Promise<void> {
|
|
501
|
+
await this.serial(async () => {
|
|
502
|
+
const unreleased = await this.teardownRuntimes();
|
|
503
|
+
this.live.clear();
|
|
504
|
+
this.built.clear();
|
|
505
|
+
this.runtimeErrors.clear();
|
|
506
|
+
this.mountAddresses.clear();
|
|
507
|
+
this.pushedStates.clear();
|
|
508
|
+
this.serviceStates.clear();
|
|
509
|
+
this.registries.clear();
|
|
510
|
+
// Whatever went wrong starting the board is history now; what is worth
|
|
511
|
+
// carrying is what would not let go.
|
|
512
|
+
this.residue = unreleased;
|
|
513
|
+
this.stopped = true;
|
|
514
|
+
});
|
|
515
|
+
this.broadcastSnapshot();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Turn this board's logging on or off, on the runtimes already running.
|
|
520
|
+
*
|
|
521
|
+
* Applied live rather than by re-provisioning: the setting is one a board
|
|
522
|
+
* revisits — on to look into something, off again afterwards — and rebuilding
|
|
523
|
+
* every runtime to carry a boolean would restart the board to change its mind.
|
|
524
|
+
*
|
|
525
|
+
* The config is updated too, so the setting survives a restart and so a
|
|
526
|
+
* runtime provisioned later comes up with it already on. Returns the runtimes
|
|
527
|
+
* it could not reach; they keep whatever they had, which is why the caller is
|
|
528
|
+
* told rather than left to assume it took.
|
|
529
|
+
*/
|
|
530
|
+
async setLogging(enabled: boolean, level: LogLevel = "info"): Promise<string[]> {
|
|
531
|
+
for (const runtime of this.config.runtimes) {
|
|
532
|
+
// Only what this switch is for. `logData` is not touched: what a board
|
|
533
|
+
// records about its own flow and what it is willing to write of the data
|
|
534
|
+
// passing through are different decisions, and a control that quietly
|
|
535
|
+
// did both would turn a verbosity setting into an exposure one.
|
|
536
|
+
runtime.state = {
|
|
537
|
+
...(runtime.state ?? {}),
|
|
538
|
+
logging: enabled,
|
|
539
|
+
logLevel: level,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const unreachable: string[] = [];
|
|
544
|
+
await Promise.all(
|
|
545
|
+
this.required().map(async ({ id }) => {
|
|
546
|
+
if (!this.built.has(id)) {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const participant = this.live.has(id)
|
|
550
|
+
? this.participants.get(id)
|
|
551
|
+
: undefined;
|
|
552
|
+
if (!participant) {
|
|
553
|
+
unreachable.push(id);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
try {
|
|
557
|
+
await participant.request({
|
|
558
|
+
op: "setState",
|
|
559
|
+
state: { logging: enabled, logLevel: level },
|
|
560
|
+
});
|
|
561
|
+
} catch {
|
|
562
|
+
unreachable.push(id);
|
|
563
|
+
}
|
|
564
|
+
}),
|
|
565
|
+
);
|
|
566
|
+
return unreachable;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Turn Capture mode on or off for this board's runtimes, while they run.
|
|
571
|
+
*
|
|
572
|
+
* Applied the way logging is (`setLogging`), and unlike logging never written
|
|
573
|
+
* into the config: Capture mode is a builder's switch, not a property of the
|
|
574
|
+
* board, so nothing that saves or restores the board can carry it. Returns
|
|
575
|
+
* the runtimes it could not reach.
|
|
576
|
+
*/
|
|
577
|
+
async setCapture(enabled: boolean): Promise<string[]> {
|
|
578
|
+
this.capture = enabled;
|
|
579
|
+
const unreachable: string[] = [];
|
|
580
|
+
await Promise.all(
|
|
581
|
+
this.required().map(async ({ id }) => {
|
|
582
|
+
if (!this.built.has(id)) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const participant = this.live.has(id)
|
|
586
|
+
? this.participants.get(id)
|
|
587
|
+
: undefined;
|
|
588
|
+
if (!participant) {
|
|
589
|
+
unreachable.push(id);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
await participant.request({ op: "setState", state: { capture: enabled } });
|
|
594
|
+
} catch {
|
|
595
|
+
unreachable.push(id);
|
|
596
|
+
}
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
return unreachable;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
capturesCalls(): boolean {
|
|
603
|
+
return this.capture;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Releases the runtimes this session built, and reports the ones it could
|
|
608
|
+
* not.
|
|
609
|
+
*
|
|
610
|
+
* Releasing is best-effort by design: a runtime server that is away must not
|
|
611
|
+
* make a board impossible to stop. But a runtime that was not released is
|
|
612
|
+
* very likely still running — built to persist, holding its mount. Saying so
|
|
613
|
+
* is the difference between an orphan someone can go and deal with and one
|
|
614
|
+
* nobody ever hears about. It is released when its server next connects.
|
|
615
|
+
*/
|
|
616
|
+
private async teardownRuntimes(): Promise<string[]> {
|
|
617
|
+
const unreleased: string[] = [];
|
|
618
|
+
await Promise.all(
|
|
619
|
+
[...this.built].map(async (id) => {
|
|
620
|
+
const participant = this.participants.get(id);
|
|
621
|
+
if (!participant) {
|
|
622
|
+
this.pendingRelease.add(id);
|
|
623
|
+
unreleased.push(
|
|
624
|
+
`Runtime "${id}" could not be released because its runtime server is not connected; it may still be running, and is released when that server reconnects.`,
|
|
625
|
+
);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
await participant.request({ op: "remove" });
|
|
630
|
+
participant.listen(null);
|
|
631
|
+
this.pendingRelease.delete(id);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
this.pendingRelease.add(id);
|
|
634
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
635
|
+
console.error(
|
|
636
|
+
`[coordinator] Failed to release runtime "${id}":`,
|
|
637
|
+
reason,
|
|
638
|
+
);
|
|
639
|
+
unreleased.push(
|
|
640
|
+
`Runtime "${id}" could not be released (${reason}); it may still be running.`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
}),
|
|
644
|
+
);
|
|
645
|
+
return unreleased;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Ends this session: releases its runtimes and lets go of its browsers.
|
|
650
|
+
*
|
|
651
|
+
* The participants' connections are not this session's to close. They belong
|
|
652
|
+
* to the tickets, so a session replacing this one finds them still there.
|
|
653
|
+
*/
|
|
654
|
+
async destroy(): Promise<void> {
|
|
655
|
+
await this.serial(async () => {
|
|
656
|
+
this.destroyed = true;
|
|
657
|
+
this.unsubscribe?.();
|
|
658
|
+
this.unsubscribe = null;
|
|
659
|
+
// What it reported while it ran is left as it was: a session that has
|
|
660
|
+
// been replaced still answers for the run it was.
|
|
661
|
+
await this.teardownRuntimes();
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Bridges are either transferred to the new session or closed here.
|
|
665
|
+
for (const bridge of this.bridges) {
|
|
666
|
+
console.log(
|
|
667
|
+
`[bridge-close] Server initiating close: destroy() closing bridge for board "${this.boardName}"`,
|
|
668
|
+
);
|
|
669
|
+
bridge.ws.close();
|
|
670
|
+
}
|
|
671
|
+
this.bridges.clear();
|
|
672
|
+
for (const [requestId, pending] of this.pendingBrowserResults) {
|
|
673
|
+
this.pendingBrowserResults.delete(requestId);
|
|
674
|
+
clearTimeout(pending.timer);
|
|
675
|
+
pending.resolve(null);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Detach all browser bridges from this session and return them so the caller
|
|
681
|
+
* can hand them to the replacement session without the browsers seeing a
|
|
682
|
+
* disconnect. Each comes with who it is, which the replacement checks again.
|
|
683
|
+
*/
|
|
684
|
+
takeBridges(): Array<{ ws: WebSocket; runtimeIds: string[] } & BridgeAttach> {
|
|
685
|
+
const taken = [...this.bridges].map((bridge) => {
|
|
686
|
+
// Detach handlers so the old session no longer processes bridge messages.
|
|
687
|
+
bridge.ws.removeAllListeners("message");
|
|
688
|
+
bridge.ws.removeAllListeners("close");
|
|
689
|
+
return {
|
|
690
|
+
ws: bridge.ws,
|
|
691
|
+
runtimeIds: [...bridge.runtimeIds],
|
|
692
|
+
role: bridge.role,
|
|
693
|
+
...(bridge.caller ? { caller: bridge.caller } : {}),
|
|
694
|
+
};
|
|
695
|
+
});
|
|
696
|
+
this.bridges.clear();
|
|
697
|
+
return taken;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** How many bridges somebody holds on this board, by the email they attached as. */
|
|
701
|
+
countMemberBridges(email: string): number {
|
|
702
|
+
let count = 0;
|
|
703
|
+
for (const bridge of this.bridges) {
|
|
704
|
+
if (bridge.role === "member" && bridge.caller?.email === email) {
|
|
705
|
+
count += 1;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return count;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Closes the bridges of somebody the board is no longer shared with. At
|
|
713
|
+
* once: being taken off the list is not something that waits for a
|
|
714
|
+
* reconnect.
|
|
715
|
+
*/
|
|
716
|
+
evictMember(email: string): void {
|
|
717
|
+
for (const bridge of [...this.bridges]) {
|
|
718
|
+
if (bridge.role === "member" && bridge.caller?.email === email) {
|
|
719
|
+
bridge.ws.close(CLOSE_NOT_A_MEMBER, "no longer shared");
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
this.memberProcessTimes.delete(email);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* What the board's list now calls an email, for the bridges attached as it.
|
|
727
|
+
* The name rides with every run they begin, so a rename has to reach
|
|
728
|
+
* somebody who is already attached.
|
|
729
|
+
*/
|
|
730
|
+
renameMember(email: string, name: string | undefined): void {
|
|
731
|
+
for (const bridge of this.bridges) {
|
|
732
|
+
if (bridge.caller?.email !== email) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const { name: _previous, ...rest } = bridge.caller;
|
|
736
|
+
bridge.caller = name ? { ...rest, name } : rest;
|
|
737
|
+
this.rememberCaller(bridge.caller.sub, name, bridge.role);
|
|
738
|
+
this.send(bridge.ws, this.snapshotFor(bridge));
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* Takes a browser that whoever is calling has already admitted, as what it
|
|
744
|
+
* was admitted as. Deciding that is not this session's job: it holds no
|
|
745
|
+
* list, and is told.
|
|
746
|
+
*/
|
|
747
|
+
registerBrowserSocket(
|
|
748
|
+
ws: WebSocket,
|
|
749
|
+
runtimeIds: string[],
|
|
750
|
+
attach: BridgeAttach = { role: "owner" },
|
|
751
|
+
): void {
|
|
752
|
+
// A member's bridge never hosts a runtime, whatever it says it has: the
|
|
753
|
+
// chain would wait on an answer that is not theirs to give.
|
|
754
|
+
const hosted = attach.role === "owner" ? runtimeIds : [];
|
|
755
|
+
|
|
756
|
+
// The same socket re-registering (e.g. its browser runtimes changed) — just
|
|
757
|
+
// refresh its runtimeIds rather than adding a duplicate or re-attaching
|
|
758
|
+
// listeners.
|
|
759
|
+
for (const existing of this.bridges) {
|
|
760
|
+
if (existing.ws === ws) {
|
|
761
|
+
existing.runtimeIds = new Set(hosted);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const bridge: BrowserBridge = {
|
|
767
|
+
ws,
|
|
768
|
+
runtimeIds: new Set(hosted),
|
|
769
|
+
role: attach.role,
|
|
770
|
+
...(attach.caller ? { caller: attach.caller } : {}),
|
|
771
|
+
seq: 0,
|
|
772
|
+
};
|
|
773
|
+
this.bridges.add(bridge);
|
|
774
|
+
if (attach.caller) {
|
|
775
|
+
this.rememberCaller(attach.caller.sub, attach.caller.name, attach.role);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
ws.on("message", (raw, isBinary) => {
|
|
779
|
+
const bytes = frameBytes(raw);
|
|
780
|
+
const limit = frameLimitFrom(this.limits.maxFrameBytes);
|
|
781
|
+
if (bytes.length > limit) {
|
|
782
|
+
this.logStore?.append(this.userId, this.boardName, {
|
|
783
|
+
runId: "",
|
|
784
|
+
ts: new Date().toISOString(),
|
|
785
|
+
runtimeId: [...bridge.runtimeIds][0] ?? "",
|
|
786
|
+
serviceUuid: "",
|
|
787
|
+
level: "warn",
|
|
788
|
+
event: "frame-dropped",
|
|
789
|
+
data: { bytes: bytes.length, limit },
|
|
790
|
+
});
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
let message: BridgeMessage;
|
|
795
|
+
try {
|
|
796
|
+
// A browser runtime's output that holds bytes. The header is the
|
|
797
|
+
// message it would have sent as text; the payload goes on as it came.
|
|
798
|
+
const frame = isBinary ? decodeBinaryFrame(bytes) : null;
|
|
799
|
+
if (isBinary && !frame) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const parsed: unknown = frame
|
|
803
|
+
? { ...frame.header, data: frame.payload }
|
|
804
|
+
: JSON.parse(bytes.toString("utf8"));
|
|
805
|
+
if (!isBridgeMessage(parsed)) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
message = parsed;
|
|
809
|
+
} catch (err) {
|
|
810
|
+
console.error(
|
|
811
|
+
`[coordinator] Failed to parse bridge message for board "${this.boardName}":`,
|
|
812
|
+
err instanceof Error ? err.message : err,
|
|
813
|
+
);
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// A browser that reconnected, or noticed a gap in the sequence, asking to
|
|
818
|
+
// be told the board again rather than carrying on from a stale view.
|
|
819
|
+
if (message.type === "resync") {
|
|
820
|
+
this.send(ws, this.snapshotFor(bridge));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (message.type === "processService") {
|
|
825
|
+
void this.serveProcessRequest(bridge, message);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Everything below is the owner's. A member's bridge may ask to be told
|
|
830
|
+
// the board again and ask the facade's services to do their job, and
|
|
831
|
+
// that is the whole of what it may send: it configures nothing, records
|
|
832
|
+
// nothing, and answers for no runtime.
|
|
833
|
+
if (bridge.role !== "owner") {
|
|
834
|
+
if (message.type === "configureService") {
|
|
835
|
+
this.send(ws, {
|
|
836
|
+
type: "response",
|
|
837
|
+
requestId: message.requestId,
|
|
838
|
+
error: "Only the board's owner may configure it",
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (message.type === "configureService") {
|
|
845
|
+
void this.serveBrowserRequest(bridge, message);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// An entry from a runtime this browser hosts. The board's log spans every
|
|
850
|
+
// runtime it uses, and a browser runtime has no other route into it —
|
|
851
|
+
// it does not hold a socket of its own to this coordinator.
|
|
852
|
+
//
|
|
853
|
+
// Not forwarded on to the other bridges, unlike an entry arriving from a
|
|
854
|
+
// remote runtime: the browser that sent it already has it, and a second
|
|
855
|
+
// browser watching the same board is not hosting the runtime that
|
|
856
|
+
// produced it.
|
|
857
|
+
if (message.type === "log" && message.entry) {
|
|
858
|
+
this.logStore?.append(
|
|
859
|
+
this.userId,
|
|
860
|
+
this.boardName,
|
|
861
|
+
message.entry as LogEntry,
|
|
862
|
+
);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (message.type === "result" && message.requestId) {
|
|
867
|
+
const pending = this.pendingBrowserResults.get(message.requestId);
|
|
868
|
+
if (pending) {
|
|
869
|
+
this.pendingBrowserResults.delete(message.requestId);
|
|
870
|
+
clearTimeout(pending.timer);
|
|
871
|
+
pending.resolve(message.data);
|
|
872
|
+
}
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (message.type === "result-from-browser" && message.runtimeId) {
|
|
877
|
+
// A person using the browser begins a run as the verified bridge
|
|
878
|
+
// caller. A Timer or standing subscription with no active call is the
|
|
879
|
+
// board's own and begins one with no caller. Only an owner bridge can
|
|
880
|
+
// reach this branch, so letting it say "nobody" grants it nothing.
|
|
881
|
+
this.routeResult(
|
|
882
|
+
message.runtimeId,
|
|
883
|
+
message.data,
|
|
884
|
+
message.boardOrigin === true || message.callerless === true
|
|
885
|
+
? { runId: randomUUID(), actor: { kind: "board" } }
|
|
886
|
+
: this.beginRun(bridge),
|
|
887
|
+
).catch((err) => {
|
|
888
|
+
console.error(
|
|
889
|
+
`[coordinator] Failed to route result from browser runtime "${message.runtimeId}":`,
|
|
890
|
+
err instanceof Error ? err.message : err,
|
|
891
|
+
);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
ws.on("close", () => {
|
|
897
|
+
this.bridges.delete(bridge);
|
|
898
|
+
// Capture mode lasts while its owner is looking: the last of their
|
|
899
|
+
// bridges closing ends it, so a tab closed with it on does not leave the
|
|
900
|
+
// board sending what passes inside its runs to a coordinator nobody
|
|
901
|
+
// reads from.
|
|
902
|
+
if (
|
|
903
|
+
this.capture &&
|
|
904
|
+
![...this.bridges].some((open) => open.role === "owner")
|
|
905
|
+
) {
|
|
906
|
+
void this.setCapture(false);
|
|
907
|
+
}
|
|
908
|
+
// Only abandon in-flight browser results once nobody remains who could
|
|
909
|
+
// answer them; another of the owner's bridges may still reply.
|
|
910
|
+
if (this.hostingBridges().length === 0) {
|
|
911
|
+
for (const [requestId, pending] of this.pendingBrowserResults) {
|
|
912
|
+
this.pendingBrowserResults.delete(requestId);
|
|
913
|
+
clearTimeout(pending.timer);
|
|
914
|
+
pending.resolve(null);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// Attaching is a read: the browser renders what the board currently is,
|
|
920
|
+
// rather than provisioning anything itself.
|
|
921
|
+
this.send(ws, this.snapshotFor(bridge));
|
|
922
|
+
|
|
923
|
+
console.log(
|
|
924
|
+
`[coordinator] Browser bridge registered for board "${this.boardName}" (${bridge.role}, runtimeIds: ${hosted.join(", ")}, bridges: ${this.bridges.size})`,
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* The assets a runtime is given: every one the board declares, unless an
|
|
932
|
+
* asset names the runtimes it is for. Not only the ones its services name —
|
|
933
|
+
* which asset a service uses can be decided as it runs, by its input or by a
|
|
934
|
+
* request, and a reference that reaches a runtime has to resolve there.
|
|
935
|
+
*
|
|
936
|
+
* None for a runtime a unit contributed: it resolves against that unit's
|
|
937
|
+
* assets, which are not the board's.
|
|
938
|
+
*/
|
|
939
|
+
private assetsFor(runtime: CloudRuntimeDescriptor): Record<string, AssetDescriptor> {
|
|
940
|
+
const assets: Record<string, AssetDescriptor> = {};
|
|
941
|
+
if (runtime.unit) {
|
|
942
|
+
return assets;
|
|
943
|
+
}
|
|
944
|
+
for (const descriptor of this.config.assets ?? []) {
|
|
945
|
+
if (!descriptor.runtimes || descriptor.runtimes.includes(runtime.id)) {
|
|
946
|
+
assets[descriptor.id] = descriptor;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return assets;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
private onRuntimeNotification(
|
|
953
|
+
runtimeId: string,
|
|
954
|
+
serviceUuid: string,
|
|
955
|
+
payload: unknown,
|
|
956
|
+
context?: ProcessContext,
|
|
957
|
+
): void {
|
|
958
|
+
// Pass it on as what it is. A service's notifications are its output, not
|
|
959
|
+
// its state — a Monitor's message never appears in its getState — so a
|
|
960
|
+
// browser reaching this runtime through us must receive the same
|
|
961
|
+
// notifications it would have received from the runtime directly. A payload
|
|
962
|
+
// is whatever the service said, string and number included; only the mount
|
|
963
|
+
// address below is looked for, and only an object can carry one.
|
|
964
|
+
this.deliverNotification(runtimeId, serviceUuid, payload, context);
|
|
965
|
+
|
|
966
|
+
if (!payload || typeof payload !== "object") {
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const published = (payload as Record<string, unknown>)[MOUNT_FIELD];
|
|
971
|
+
if (typeof published !== "string" || !published) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// A published address *is* state, and the one piece of it a browser cannot
|
|
976
|
+
// work out for itself, so keep the board's view of it current.
|
|
977
|
+
const states = this.serviceStates.get(runtimeId) ?? {};
|
|
978
|
+
const previous = states[serviceUuid];
|
|
979
|
+
states[serviceUuid] =
|
|
980
|
+
previous && typeof previous === "object"
|
|
981
|
+
? { ...(previous as Record<string, unknown>), [MOUNT_FIELD]: published }
|
|
982
|
+
: { [MOUNT_FIELD]: published };
|
|
983
|
+
this.serviceStates.set(runtimeId, states);
|
|
984
|
+
this.broadcastServiceState(runtimeId, serviceUuid, states[serviceUuid]);
|
|
985
|
+
|
|
986
|
+
if (this.recordMountAddress(runtimeId, serviceUuid, published)) {
|
|
987
|
+
void this.publishMountAddresses().catch((err) => {
|
|
988
|
+
console.error(
|
|
989
|
+
`[coordinator] Failed to publish mount addresses for board "${this.boardName}":`,
|
|
990
|
+
err instanceof Error ? err.message : err,
|
|
991
|
+
);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* Tells the browsers that are to hear it what a service said.
|
|
998
|
+
*
|
|
999
|
+
* A notification raised with a person actor goes to that person's bridges
|
|
1000
|
+
* and nobody else's — what one member's action produced is not another
|
|
1001
|
+
* member's to read, and not the owner's to watch. Board, mount and local
|
|
1002
|
+
* actors produce shared board news, so those notifications go to everyone.
|
|
1003
|
+
*
|
|
1004
|
+
* A member is sent it only from a service the facade reads, cut down to
|
|
1005
|
+
* what the facade reads of it, and never the runtime's own account of its
|
|
1006
|
+
* flow, which carries the data passing through every service. The owner is
|
|
1007
|
+
* sent what was said.
|
|
1008
|
+
*/
|
|
1009
|
+
private deliverNotification(
|
|
1010
|
+
runtimeId: string,
|
|
1011
|
+
serviceUuid: string,
|
|
1012
|
+
payload: unknown,
|
|
1013
|
+
context?: ProcessContext,
|
|
1014
|
+
): void {
|
|
1015
|
+
if (!this.runMayContinue(context)) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const caller =
|
|
1019
|
+
context?.actor.kind === "person" ? context.actor : undefined;
|
|
1020
|
+
const frameOf = (said: unknown) =>
|
|
1021
|
+
JSON.stringify({ type: "notification", runtimeId, serviceUuid, payload: said });
|
|
1022
|
+
let whole: string | null = null;
|
|
1023
|
+
let projected: string | null = null;
|
|
1024
|
+
let projectionDone = false;
|
|
1025
|
+
for (const bridge of this.bridges) {
|
|
1026
|
+
if (caller && bridge.caller?.sub !== caller.sub) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (
|
|
1030
|
+
bridge.role === "member" &&
|
|
1031
|
+
(!this.access.sources.has(serviceUuid) || isFlowNotification(payload))
|
|
1032
|
+
) {
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (bridge.ws.readyState !== WebSocket.OPEN) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (bridge.role === "member") {
|
|
1039
|
+
if (!projectionDone) {
|
|
1040
|
+
const said = projectNotification(this.access, serviceUuid, payload);
|
|
1041
|
+
projected = said === undefined ? null : frameOf(said);
|
|
1042
|
+
projectionDone = true;
|
|
1043
|
+
}
|
|
1044
|
+
if (projected !== null) {
|
|
1045
|
+
bridge.ws.send(projected);
|
|
1046
|
+
}
|
|
1047
|
+
} else {
|
|
1048
|
+
whole ??= frameOf(payload);
|
|
1049
|
+
bridge.ws.send(whole);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Who a `sub` in this board's log is, as the board's list calls them, for
|
|
1056
|
+
* the owner's live view of it. Learnt from the bridges people attach with,
|
|
1057
|
+
* and kept after they leave, so a run is still named once its caller has
|
|
1058
|
+
* closed the board. Held only while the session lives, and never written
|
|
1059
|
+
* to the log.
|
|
1060
|
+
*/
|
|
1061
|
+
private readonly knownCallers = new Map<
|
|
1062
|
+
string,
|
|
1063
|
+
{ name?: string; role: "owner" | "member" }
|
|
1064
|
+
>();
|
|
1065
|
+
|
|
1066
|
+
private rememberCaller(
|
|
1067
|
+
sub: string,
|
|
1068
|
+
name: string | undefined,
|
|
1069
|
+
role: "owner" | "member",
|
|
1070
|
+
): void {
|
|
1071
|
+
this.knownCallers.set(sub, { ...(name ? { name } : {}), role });
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** Sends a message to every browser the board's owner has attached. */
|
|
1075
|
+
private toOwners(message: BridgeMessage): void {
|
|
1076
|
+
const payload = JSON.stringify(message);
|
|
1077
|
+
for (const bridge of this.bridges) {
|
|
1078
|
+
if (bridge.role === "owner" && bridge.ws.readyState === WebSocket.OPEN) {
|
|
1079
|
+
bridge.ws.send(payload);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
/** The owner's open bridges: the only ones a browser runtime can be on. */
|
|
1085
|
+
private hostingBridges(): BrowserBridge[] {
|
|
1086
|
+
return [...this.bridges].filter(
|
|
1087
|
+
(bridge) =>
|
|
1088
|
+
bridge.role === "owner" && bridge.ws.readyState === WebSocket.OPEN,
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/** Tells every browser the board again, each as it is entitled to see it. */
|
|
1093
|
+
private broadcastSnapshot(): void {
|
|
1094
|
+
for (const bridge of this.bridges) {
|
|
1095
|
+
this.send(bridge.ws, this.snapshotFor(bridge));
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Tells every browser one service's new state. A member is told only of a
|
|
1101
|
+
* service the facade names, and only what the facade reads of it.
|
|
1102
|
+
*/
|
|
1103
|
+
private broadcastServiceState(
|
|
1104
|
+
runtimeId: string,
|
|
1105
|
+
serviceUuid: string,
|
|
1106
|
+
state: unknown,
|
|
1107
|
+
): void {
|
|
1108
|
+
for (const bridge of this.bridges) {
|
|
1109
|
+
if (bridge.role === "member") {
|
|
1110
|
+
if (!this.access.named.has(serviceUuid)) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
this.send(bridge.ws, {
|
|
1114
|
+
type: "serviceState",
|
|
1115
|
+
seq: ++bridge.seq,
|
|
1116
|
+
runtimeId,
|
|
1117
|
+
serviceUuid,
|
|
1118
|
+
state: projectState(this.access, serviceUuid, state),
|
|
1119
|
+
});
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
this.send(bridge.ws, {
|
|
1123
|
+
type: "serviceState",
|
|
1124
|
+
seq: ++bridge.seq,
|
|
1125
|
+
runtimeId,
|
|
1126
|
+
serviceUuid,
|
|
1127
|
+
state,
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
private send(ws: WebSocket, message: BridgeMessage): void {
|
|
1133
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1134
|
+
ws.send(JSON.stringify(message));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** A run a browser begins, as whoever attached with it. */
|
|
1139
|
+
private beginRun(bridge: BrowserBridge): ProcessContext {
|
|
1140
|
+
const now = Date.now();
|
|
1141
|
+
return {
|
|
1142
|
+
runId: randomUUID(),
|
|
1143
|
+
actor: bridge.caller
|
|
1144
|
+
? {
|
|
1145
|
+
kind: "person",
|
|
1146
|
+
...bridge.caller,
|
|
1147
|
+
expiresAt:
|
|
1148
|
+
now +
|
|
1149
|
+
(this.limits.maxPersonRunAgeMs ??
|
|
1150
|
+
DEFAULT_MEMBER_LIMITS.maxPersonRunAgeMs),
|
|
1151
|
+
}
|
|
1152
|
+
: { kind: "local" },
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/** Whether the authority captured by a run is still valid now. */
|
|
1157
|
+
private runMayContinue(context?: ProcessContext): boolean {
|
|
1158
|
+
if (!context || context.actor.kind !== "person") {
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
return (
|
|
1162
|
+
Number.isFinite(context.actor.expiresAt) &&
|
|
1163
|
+
context.actor.expiresAt > Date.now() &&
|
|
1164
|
+
this.mayActAs(context.actor)
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* The board as this coordinator knows it, for one browser.
|
|
1170
|
+
*
|
|
1171
|
+
* Sent when a browser attaches, and again on request. For the owner it is
|
|
1172
|
+
* every remote runtime's registry and the state its services last reported:
|
|
1173
|
+
* the live state rather than the saved board, because that is the part a
|
|
1174
|
+
* browser cannot work out for itself — a mount's address is assigned when
|
|
1175
|
+
* the runtime is provisioned and appears in no saved board.
|
|
1176
|
+
*
|
|
1177
|
+
* For a member it is a projection: the facade, and of the board only the
|
|
1178
|
+
* services the facade names with what it reads of each. A board's config
|
|
1179
|
+
* carries service state, and state can carry credentials, so not showing a
|
|
1180
|
+
* member the board would mean little if this sent it to them.
|
|
1181
|
+
*/
|
|
1182
|
+
private snapshotFor(bridge: BrowserBridge): BridgeMessage {
|
|
1183
|
+
const you = bridge.caller
|
|
1184
|
+
? {
|
|
1185
|
+
...(bridge.caller.email ? { email: bridge.caller.email } : {}),
|
|
1186
|
+
...(bridge.caller.name ? { name: bridge.caller.name } : {}),
|
|
1187
|
+
}
|
|
1188
|
+
: undefined;
|
|
1189
|
+
const common = {
|
|
1190
|
+
type: "snapshot" as const,
|
|
1191
|
+
seq: ++bridge.seq,
|
|
1192
|
+
boardName: this.boardName,
|
|
1193
|
+
status: this.getStatus(),
|
|
1194
|
+
role: bridge.role,
|
|
1195
|
+
...(you ? { you } : {}),
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
if (bridge.role === "member") {
|
|
1199
|
+
const config = projectConfig(this.config, this.access, (runtimeId, uuid) =>
|
|
1200
|
+
this.liveState(runtimeId, uuid),
|
|
1201
|
+
);
|
|
1202
|
+
return {
|
|
1203
|
+
...common,
|
|
1204
|
+
// Whether the board is running is a member's to know; which runtime
|
|
1205
|
+
// server is away, and why, is its owner's.
|
|
1206
|
+
errors: [],
|
|
1207
|
+
config,
|
|
1208
|
+
runtimes: config.runtimes.map(({ id }) => ({
|
|
1209
|
+
runtimeId: id,
|
|
1210
|
+
registry: [],
|
|
1211
|
+
services: Object.fromEntries(
|
|
1212
|
+
(config.services[id] ?? []).map((svc) => [svc.uuid, svc.state ?? {}]),
|
|
1213
|
+
),
|
|
1214
|
+
})),
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// In the board's own order, and including a runtime whose participant has
|
|
1219
|
+
// dropped: what it last reported is still the best account of it there is,
|
|
1220
|
+
// and the status beside it says that it is away.
|
|
1221
|
+
const runtimes: RuntimeSnapshot[] = this.required()
|
|
1222
|
+
.filter(({ id }) => this.built.has(id))
|
|
1223
|
+
.map(({ id }) => ({
|
|
1224
|
+
runtimeId: id,
|
|
1225
|
+
registry: this.registries.get(id) ?? [],
|
|
1226
|
+
services: this.serviceStates.get(id) ?? {},
|
|
1227
|
+
}));
|
|
1228
|
+
return {
|
|
1229
|
+
...common,
|
|
1230
|
+
errors: this.getErrors(),
|
|
1231
|
+
config: this.config,
|
|
1232
|
+
runtimes,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/** What a service last reported, or what the board configured it with. */
|
|
1237
|
+
private liveState(runtimeId: string, serviceUuid: string): unknown {
|
|
1238
|
+
const reported = this.serviceStates.get(runtimeId)?.[serviceUuid];
|
|
1239
|
+
if (reported !== undefined) {
|
|
1240
|
+
return reported;
|
|
1241
|
+
}
|
|
1242
|
+
return (this.config.services[runtimeId] ?? []).find(
|
|
1243
|
+
(svc) => svc.uuid === serviceUuid,
|
|
1244
|
+
)?.state;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Whether a member has asked for more than they are allowed in the last
|
|
1249
|
+
* minute. Counted per person rather than per bridge, so opening another tab
|
|
1250
|
+
* is not a way round it.
|
|
1251
|
+
*/
|
|
1252
|
+
private overMemberRate(email: string): boolean {
|
|
1253
|
+
const limit =
|
|
1254
|
+
this.limits.maxMemberProcessPerMinute ??
|
|
1255
|
+
DEFAULT_MEMBER_LIMITS.maxMemberProcessPerMinute;
|
|
1256
|
+
const now = Date.now();
|
|
1257
|
+
const recent = (this.memberProcessTimes.get(email) ?? []).filter(
|
|
1258
|
+
(at) => now - at < 60_000,
|
|
1259
|
+
);
|
|
1260
|
+
if (recent.length >= limit) {
|
|
1261
|
+
this.memberProcessTimes.set(email, recent);
|
|
1262
|
+
return true;
|
|
1263
|
+
}
|
|
1264
|
+
recent.push(now);
|
|
1265
|
+
this.memberProcessTimes.set(email, recent);
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Asks a service on a remote runtime to do its job, for a browser.
|
|
1271
|
+
*
|
|
1272
|
+
* The run begins here. Its person actor, when there is one, was established
|
|
1273
|
+
* when the bridge was admitted, so runtimes can rely on it and nothing in
|
|
1274
|
+
* the message can change it.
|
|
1275
|
+
*
|
|
1276
|
+
* The answer says only whether the work was taken. What it produces arrives
|
|
1277
|
+
* the way a pipeline's output always does — as notifications, and as a
|
|
1278
|
+
* result this session carries to the board's next runtime.
|
|
1279
|
+
*/
|
|
1280
|
+
private async serveProcessRequest(
|
|
1281
|
+
bridge: BrowserBridge,
|
|
1282
|
+
message: Extract<BridgeMessage, { type: "processService" }>,
|
|
1283
|
+
): Promise<void> {
|
|
1284
|
+
const refuse = (error: string) =>
|
|
1285
|
+
this.send(bridge.ws, {
|
|
1286
|
+
type: "response",
|
|
1287
|
+
requestId: message.requestId,
|
|
1288
|
+
error,
|
|
1289
|
+
});
|
|
1290
|
+
if (
|
|
1291
|
+
typeof message.requestId !== "string" ||
|
|
1292
|
+
typeof message.serviceUuid !== "string"
|
|
1293
|
+
) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
let runtimeId = message.runtimeId;
|
|
1298
|
+
if (bridge.role === "member") {
|
|
1299
|
+
// The entry point is the capability: a member may begin at a service
|
|
1300
|
+
// the facade asks to process, and at no other.
|
|
1301
|
+
if (!this.access.processTargets.has(message.serviceUuid)) {
|
|
1302
|
+
refuse("This board does not offer that");
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
// Where that service is, is the board's to say and not the message's.
|
|
1306
|
+
runtimeId = runtimeHolding(this.config, message.serviceUuid) ?? "";
|
|
1307
|
+
const email = bridge.caller?.email ?? "";
|
|
1308
|
+
if (this.overMemberRate(email)) {
|
|
1309
|
+
refuse("Too many requests — try again in a moment");
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (this.stopped) {
|
|
1315
|
+
refuse("This board is stopped");
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const participant = this.live.has(runtimeId)
|
|
1319
|
+
? this.participants.get(runtimeId)
|
|
1320
|
+
: undefined;
|
|
1321
|
+
if (!participant) {
|
|
1322
|
+
refuse(
|
|
1323
|
+
bridge.role === "member"
|
|
1324
|
+
? "This board is not running right now"
|
|
1325
|
+
: this.required().some((rt) => rt.id === runtimeId)
|
|
1326
|
+
? `Runtime "${runtimeId}" is not connected`
|
|
1327
|
+
: `Unknown runtime "${runtimeId}"`,
|
|
1328
|
+
);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
try {
|
|
1333
|
+
await participant.request({
|
|
1334
|
+
op: "processService",
|
|
1335
|
+
serviceUuid: message.serviceUuid,
|
|
1336
|
+
params: message.payload ?? null,
|
|
1337
|
+
context: this.beginRun(bridge),
|
|
1338
|
+
});
|
|
1339
|
+
this.send(bridge.ws, {
|
|
1340
|
+
type: "response",
|
|
1341
|
+
requestId: message.requestId,
|
|
1342
|
+
data: { accepted: true },
|
|
1343
|
+
});
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1346
|
+
refuse(
|
|
1347
|
+
bridge.role === "member"
|
|
1348
|
+
? "This board could not take that right now"
|
|
1349
|
+
: `Runtime "${runtimeId}": ${reason}`,
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* Acts on a remote service for a browser that cannot reach it.
|
|
1356
|
+
*
|
|
1357
|
+
* The browser is a viewer: it renders this board and asks for changes, but the
|
|
1358
|
+
* runtimes are the coordinator's to talk to. The reply carries whatever the
|
|
1359
|
+
* runtime returned, so a panel can reconcile its optimistic state with what
|
|
1360
|
+
* actually took effect.
|
|
1361
|
+
*/
|
|
1362
|
+
private async serveBrowserRequest(
|
|
1363
|
+
bridge: BrowserBridge,
|
|
1364
|
+
message: Extract<BridgeMessage, { type: "configureService" }>,
|
|
1365
|
+
): Promise<void> {
|
|
1366
|
+
const ws = bridge.ws;
|
|
1367
|
+
const participant = this.live.has(message.runtimeId)
|
|
1368
|
+
? this.participants.get(message.runtimeId)
|
|
1369
|
+
: undefined;
|
|
1370
|
+
if (!participant) {
|
|
1371
|
+
this.send(ws, {
|
|
1372
|
+
type: "response",
|
|
1373
|
+
requestId: message.requestId,
|
|
1374
|
+
error: this.required().some((rt) => rt.id === message.runtimeId)
|
|
1375
|
+
? `Runtime "${message.runtimeId}" is not connected`
|
|
1376
|
+
: `Unknown runtime "${message.runtimeId}"`,
|
|
1377
|
+
});
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
try {
|
|
1382
|
+
// Configuring returns the service's whole state; record it so a browser
|
|
1383
|
+
// attaching later sees the same thing this one just did.
|
|
1384
|
+
const data = await participant.request({
|
|
1385
|
+
op: "configureService",
|
|
1386
|
+
serviceUuid: message.serviceUuid,
|
|
1387
|
+
config: message.config ?? {},
|
|
1388
|
+
context: this.beginRun(bridge),
|
|
1389
|
+
});
|
|
1390
|
+
const states = this.serviceStates.get(message.runtimeId) ?? {};
|
|
1391
|
+
states[message.serviceUuid] = data;
|
|
1392
|
+
this.serviceStates.set(message.runtimeId, states);
|
|
1393
|
+
this.broadcastServiceState(message.runtimeId, message.serviceUuid, data);
|
|
1394
|
+
this.send(ws, { type: "response", requestId: message.requestId, data });
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
this.send(ws, {
|
|
1397
|
+
type: "response",
|
|
1398
|
+
requestId: message.requestId,
|
|
1399
|
+
error: `Runtime "${message.runtimeId}": ${err instanceof Error ? err.message : String(err)}`,
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/** Records a published address. Returns whether it changed anything. */
|
|
1405
|
+
private recordMountAddress(
|
|
1406
|
+
runtimeId: string,
|
|
1407
|
+
serviceUuid: string,
|
|
1408
|
+
url: string,
|
|
1409
|
+
): boolean {
|
|
1410
|
+
const key = formatMountRef({ runtimeId, serviceUuid });
|
|
1411
|
+
if (this.mountAddresses.get(key) === url) {
|
|
1412
|
+
return false;
|
|
1413
|
+
}
|
|
1414
|
+
this.mountAddresses.set(key, url);
|
|
1415
|
+
return true;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Hands every service holding a mount reference the address it names.
|
|
1420
|
+
*
|
|
1421
|
+
* A runtime cannot do this for itself: it sees its own services and nothing
|
|
1422
|
+
* else, while a reference names a service on another runtime, possibly on
|
|
1423
|
+
* another machine. So the coordinator resolves against the board it owns and
|
|
1424
|
+
* configures the consumer with a plain address, which is the same value the
|
|
1425
|
+
* consumer would have been given had the board been exported.
|
|
1426
|
+
*
|
|
1427
|
+
* Services on browser runtimes are not reachable from here — the bridge only
|
|
1428
|
+
* carries processing — so those still resolve in the browser, which
|
|
1429
|
+
* coordinates its own board state.
|
|
1430
|
+
*/
|
|
1431
|
+
private async publishMountAddresses(): Promise<void> {
|
|
1432
|
+
if (this.mountAddresses.size === 0) {
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
await Promise.all(
|
|
1437
|
+
this.required().map(async (descriptor) => {
|
|
1438
|
+
if (!this.live.has(descriptor.id)) {
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const services = this.config.services[descriptor.id] ?? [];
|
|
1442
|
+
for (const svc of services) {
|
|
1443
|
+
const state = svc.state;
|
|
1444
|
+
if (!state) {
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
// A reference is written wherever the service names its target, so it
|
|
1448
|
+
// is looked for by scheme rather than read out of one field.
|
|
1449
|
+
const refs = [...collectMountRefs(state)];
|
|
1450
|
+
if (refs.length === 0) {
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
if (refs.length > 1) {
|
|
1454
|
+
// One address field, so one mount per consumer. A service needing
|
|
1455
|
+
// two should host a pipeline instead.
|
|
1456
|
+
console.warn(
|
|
1457
|
+
`[coordinator] Service "${descriptor.id}/${svc.uuid}" names ${refs.length} mounts; only the first is resolved`,
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
const address = this.mountAddresses.get(refs[0]);
|
|
1461
|
+
if (!address) {
|
|
1462
|
+
// The owner has not published yet. Later passes run whenever an
|
|
1463
|
+
// address appears.
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
const key = formatMountRef({
|
|
1467
|
+
runtimeId: descriptor.id,
|
|
1468
|
+
serviceUuid: svc.uuid,
|
|
1469
|
+
});
|
|
1470
|
+
if (this.pushedStates.get(key) === address) {
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
this.pushedStates.set(key, address);
|
|
1474
|
+
// Only the address, and only in the field addresses live in. The
|
|
1475
|
+
// board keeps its reference: that is what survives being saved and
|
|
1476
|
+
// reopened somewhere else, while an address is only true of this run.
|
|
1477
|
+
await this.configureService(descriptor.id, svc.uuid, {
|
|
1478
|
+
[MOUNT_FIELD]: address,
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}),
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
private async configureService(
|
|
1486
|
+
runtimeId: string,
|
|
1487
|
+
serviceUuid: string,
|
|
1488
|
+
state: Record<string, unknown>,
|
|
1489
|
+
): Promise<void> {
|
|
1490
|
+
try {
|
|
1491
|
+
await this.participants
|
|
1492
|
+
.get(runtimeId)
|
|
1493
|
+
?.request({
|
|
1494
|
+
op: "configureService",
|
|
1495
|
+
serviceUuid,
|
|
1496
|
+
config: state,
|
|
1497
|
+
context: { runId: randomUUID(), actor: { kind: "board" } },
|
|
1498
|
+
});
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
console.error(
|
|
1501
|
+
`[coordinator] Failed to configure "${serviceUuid}" on runtime "${runtimeId}":`,
|
|
1502
|
+
err instanceof Error ? err.message : err,
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
/**
|
|
1508
|
+
* Hands what one runtime produced to the next one in the board.
|
|
1509
|
+
*
|
|
1510
|
+
* `context` is the run it was produced in. It goes on with the data, which
|
|
1511
|
+
* is what makes a board's runtimes one run rather than one each — and what
|
|
1512
|
+
* lets the second runtime know who began it.
|
|
1513
|
+
*/
|
|
1514
|
+
private async routeResult(
|
|
1515
|
+
fromRuntimeId: string,
|
|
1516
|
+
data: unknown,
|
|
1517
|
+
context?: ProcessContext,
|
|
1518
|
+
): Promise<void> {
|
|
1519
|
+
if (!this.runMayContinue(context)) {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const next = this.nextRuntime(fromRuntimeId, this.config.runtimes);
|
|
1523
|
+
if (!next) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
if (isRemoteRuntime(next)) {
|
|
1528
|
+
const participant = this.live.has(next.id)
|
|
1529
|
+
? this.participants.get(next.id)
|
|
1530
|
+
: undefined;
|
|
1531
|
+
if (!participant) {
|
|
1532
|
+
// A required participant that is away: the board already says so.
|
|
1533
|
+
console.warn(
|
|
1534
|
+
`[coordinator] Next runtime "${next.id}" is not connected — dropping result`,
|
|
1535
|
+
);
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
participant.process(data, context);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (isBrowserRuntime(next)) {
|
|
1543
|
+
// The owner's browsers, and only those: a browser runtime is theirs to
|
|
1544
|
+
// run, and a member's bridge asked would leave the chain waiting on an
|
|
1545
|
+
// answer that is dropped when it comes.
|
|
1546
|
+
const targets = this.hostingBridges();
|
|
1547
|
+
if (targets.length === 0) {
|
|
1548
|
+
// A transient participant that is away. Nothing is wrong: the data
|
|
1549
|
+
// stops here, as it would at a service that returned null, and the
|
|
1550
|
+
// board goes on running.
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
const requestId = randomUUID();
|
|
1555
|
+
// Fan the work out to each of them so each client's UI (e.g. a
|
|
1556
|
+
// Monitor) updates. The first reply resolves the chain; later replies for
|
|
1557
|
+
// the same requestId are no-ops since its pending entry is already gone.
|
|
1558
|
+
const result = await new Promise<unknown>((resolve) => {
|
|
1559
|
+
const remaining =
|
|
1560
|
+
context?.actor.kind === "person"
|
|
1561
|
+
? Math.max(0, context.actor.expiresAt - Date.now())
|
|
1562
|
+
: this.limits.maxPersonRunAgeMs ??
|
|
1563
|
+
DEFAULT_MEMBER_LIMITS.maxPersonRunAgeMs;
|
|
1564
|
+
const timer = setTimeout(() => {
|
|
1565
|
+
this.pendingBrowserResults.delete(requestId);
|
|
1566
|
+
resolve(null);
|
|
1567
|
+
}, remaining);
|
|
1568
|
+
timer.unref?.();
|
|
1569
|
+
this.pendingBrowserResults.set(requestId, { resolve, timer });
|
|
1570
|
+
// The browser is told the run so that what it records belongs to it.
|
|
1571
|
+
// What continues the chain below is the context held here, not
|
|
1572
|
+
// whatever comes back.
|
|
1573
|
+
const header = {
|
|
1574
|
+
type: "processRuntime",
|
|
1575
|
+
runtimeId: next.id,
|
|
1576
|
+
requestId,
|
|
1577
|
+
...(context ? { context } : {}),
|
|
1578
|
+
};
|
|
1579
|
+
const payload =
|
|
1580
|
+
data instanceof BinaryPayload
|
|
1581
|
+
? encodeBinaryFrame(header, data)
|
|
1582
|
+
: JSON.stringify({ ...header, params: data });
|
|
1583
|
+
for (const target of targets) {
|
|
1584
|
+
target.ws.send(payload);
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
if (result !== null) {
|
|
1589
|
+
await this.routeResult(next.id, result, context);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
private nextRuntime(
|
|
1595
|
+
currentId: string,
|
|
1596
|
+
allRuntimes: CloudRuntimeDescriptor[],
|
|
1597
|
+
): CloudRuntimeDescriptor | null {
|
|
1598
|
+
const idx = allRuntimes.findIndex((rt) => rt.id === currentId);
|
|
1599
|
+
for (let i = idx + 1; i < allRuntimes.length; i++) {
|
|
1600
|
+
const next = allRuntimes[i];
|
|
1601
|
+
if (isBrowserRuntime(next)) {
|
|
1602
|
+
return next;
|
|
1603
|
+
}
|
|
1604
|
+
if (isRemoteRuntime(next)) {
|
|
1605
|
+
return next;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
}
|