@songsid/agend 2.1.12 → 2.2.0-alpha.2
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 +4 -2
- package/README.zh-TW.md +4 -2
- package/dist/agent-cli.js +6 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +68 -12
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-token-header.d.ts +13 -0
- package/dist/agent-token-header.js +16 -0
- package/dist/agent-token-header.js.map +1 -0
- package/dist/auth/one-time-code.d.ts +19 -0
- package/dist/auth/one-time-code.js +49 -0
- package/dist/auth/one-time-code.js.map +1 -0
- package/dist/auth-api.d.ts +52 -0
- package/dist/auth-api.js +346 -0
- package/dist/auth-api.js.map +1 -0
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +4 -0
- package/dist/backend/claude-code.js +97 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +59 -30
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro-agent.d.ts +66 -0
- package/dist/backend/kiro-agent.js +302 -0
- package/dist/backend/kiro-agent.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +80 -0
- package/dist/backend/kiro-identity.js +537 -0
- package/dist/backend/kiro-identity.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.d.ts +1 -0
- package/dist/backend/kiro-v2-store-worker.js +87 -0
- package/dist/backend/kiro-v2-store-worker.js.map +1 -0
- package/dist/backend/kiro-v2-store.d.ts +54 -0
- package/dist/backend/kiro-v2-store.js +131 -0
- package/dist/backend/kiro-v2-store.js.map +1 -0
- package/dist/backend/kiro-v3-identity.d.ts +5 -0
- package/dist/backend/kiro-v3-identity.js +22 -0
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +90 -7
- package/dist/backend/kiro.js +500 -124
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +1 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +51 -3
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-analysis.d.ts +92 -0
- package/dist/cache-analysis.js +147 -0
- package/dist/cache-analysis.js.map +1 -0
- package/dist/cache-ledger.d.ts +108 -0
- package/dist/cache-ledger.js +230 -0
- package/dist/cache-ledger.js.map +1 -0
- package/dist/cache-prices.d.ts +32 -0
- package/dist/cache-prices.js +73 -0
- package/dist/cache-prices.js.map +1 -0
- package/dist/cache-scan.d.ts +39 -0
- package/dist/cache-scan.js +115 -0
- package/dist/cache-scan.js.map +1 -0
- package/dist/cache-service.d.ts +115 -0
- package/dist/cache-service.js +417 -0
- package/dist/cache-service.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +1 -0
- package/dist/channel/adapters/discord.js +29 -10
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +1 -0
- package/dist/channel/adapters/telegram.js +13 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +9 -1
- package/dist/classic-channel-manager.d.ts +18 -10
- package/dist/classic-channel-manager.js +45 -18
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli-entry.d.ts +19 -0
- package/dist/cli-entry.js +36 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli.js +471 -146
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +5 -3
- package/dist/command-table.js +29 -27
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +46 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.d.ts +6 -0
- package/dist/connection-secrets.js +4 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/daemon.d.ts +95 -4
- package/dist/daemon.js +748 -111
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.d.ts +6 -1
- package/dist/darwin-memory.js +17 -6
- package/dist/darwin-memory.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +85 -0
- package/dist/delivery-consumed-watch.js +244 -0
- package/dist/delivery-consumed-watch.js.map +1 -0
- package/dist/delivery-outbox.d.ts +61 -0
- package/dist/delivery-outbox.js +120 -2
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +14 -5
- package/dist/delivery-reconciliation.js +27 -4
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/delivery-reconciliation.test.js +35 -1
- package/dist/delivery-reconciliation.test.js.map +1 -1
- package/dist/event-log.d.ts +4 -1
- package/dist/event-log.js +6 -3
- package/dist/event-log.js.map +1 -1
- package/dist/event-loop-watch.d.ts +28 -0
- package/dist/event-loop-watch.js +75 -3
- package/dist/event-loop-watch.js.map +1 -1
- package/dist/fleet-context.d.ts +22 -2
- package/dist/fleet-control-audit.d.ts +5 -0
- package/dist/fleet-control-audit.js +7 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-level-config.d.ts +2 -2
- package/dist/fleet-level-config.js +28 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +339 -12
- package/dist/fleet-manager.js +2623 -461
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.js +2 -3
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
- package/dist/host-memory.d.ts +4 -0
- package/dist/host-memory.js.map +1 -1
- package/dist/install-lock.d.ts +49 -0
- package/dist/install-lock.js +200 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/instance-config-impact.js +8 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -5
- package/dist/instance-lifecycle.js +162 -53
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +164 -16
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +2 -0
- package/dist/login-controller.js +17 -5
- package/dist/login-controller.js.map +1 -1
- package/dist/memory-pressure.d.ts +7 -8
- package/dist/memory-pressure.js +76 -15
- package/dist/memory-pressure.js.map +1 -1
- package/dist/monotonic-budget.d.ts +2 -0
- package/dist/monotonic-budget.js +28 -0
- package/dist/monotonic-budget.js.map +1 -0
- package/dist/needs-you-hub.d.ts +114 -0
- package/dist/needs-you-hub.js +370 -0
- package/dist/needs-you-hub.js.map +1 -0
- package/dist/needs-you-live.d.ts +116 -0
- package/dist/needs-you-live.js +228 -0
- package/dist/needs-you-live.js.map +1 -0
- package/dist/needs-you.d.ts +132 -0
- package/dist/needs-you.js +164 -0
- package/dist/needs-you.js.map +1 -0
- package/dist/node-version-guard.d.ts +30 -0
- package/dist/node-version-guard.js +50 -0
- package/dist/node-version-guard.js.map +1 -0
- package/dist/operator-control-paths.d.ts +4 -0
- package/dist/operator-control-paths.js +24 -0
- package/dist/operator-control-paths.js.map +1 -0
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +9 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +5 -5
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.d.ts +26 -0
- package/dist/package-preimage.js +96 -0
- package/dist/package-preimage.js.map +1 -0
- package/dist/profile-control.js +4 -20
- package/dist/profile-control.js.map +1 -1
- package/dist/public-web-gateway.d.ts +18 -0
- package/dist/public-web-gateway.js +141 -0
- package/dist/public-web-gateway.js.map +1 -0
- package/dist/public-web-link.d.ts +53 -0
- package/dist/public-web-link.js +175 -0
- package/dist/public-web-link.js.map +1 -0
- package/dist/quickstart-api.d.ts +8 -1
- package/dist/quickstart-api.js +86 -48
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +1 -0
- package/dist/quickstart.js +16 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/restart-guard.d.ts +52 -0
- package/dist/restart-guard.js +109 -0
- package/dist/restart-guard.js.map +1 -0
- package/dist/scheduler/db.d.ts +7 -1
- package/dist/scheduler/db.js +44 -2
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -1
- package/dist/scheduler/retry-label.d.ts +11 -0
- package/dist/scheduler/retry-label.js +24 -0
- package/dist/scheduler/retry-label.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +55 -2
- package/dist/scheduler/scheduler.js +220 -8
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/types.d.ts +22 -0
- package/dist/scheduler/types.js.map +1 -1
- package/dist/secret-store.d.ts +5 -0
- package/dist/secret-store.js +20 -0
- package/dist/secret-store.js.map +1 -1
- package/dist/service-activation.d.ts +152 -0
- package/dist/service-activation.js +407 -0
- package/dist/service-activation.js.map +1 -0
- package/dist/service-installer.d.ts +63 -1
- package/dist/service-installer.js +141 -16
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +65 -0
- package/dist/service-plan.js +108 -0
- package/dist/service-plan.js.map +1 -0
- package/dist/service-restart-selection.d.ts +6 -5
- package/dist/service-restart-selection.js +6 -5
- package/dist/service-restart-selection.js.map +1 -1
- package/dist/settings-api.d.ts +10 -2
- package/dist/settings-api.js +216 -72
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.d.ts +20 -0
- package/dist/settings-baseline.js +104 -0
- package/dist/settings-baseline.js.map +1 -0
- package/dist/settings-change.d.ts +21 -0
- package/dist/settings-change.js +161 -0
- package/dist/settings-change.js.map +1 -0
- package/dist/settings-confirmation.d.ts +105 -0
- package/dist/settings-confirmation.js +232 -0
- package/dist/settings-confirmation.js.map +1 -0
- package/dist/settings-control.d.ts +41 -0
- package/dist/settings-control.js +294 -0
- package/dist/settings-control.js.map +1 -0
- package/dist/settings-effect.d.ts +24 -0
- package/dist/settings-effect.js +169 -0
- package/dist/settings-effect.js.map +1 -0
- package/dist/settings-http-confirmation.d.ts +29 -0
- package/dist/settings-http-confirmation.js +123 -0
- package/dist/settings-http-confirmation.js.map +1 -0
- package/dist/settings-instance-patch.d.ts +4 -0
- package/dist/settings-instance-patch.js +26 -0
- package/dist/settings-instance-patch.js.map +1 -0
- package/dist/settings-request-capability.d.ts +16 -0
- package/dist/settings-request-capability.js +32 -0
- package/dist/settings-request-capability.js.map +1 -0
- package/dist/settings-transaction.d.ts +65 -0
- package/dist/settings-transaction.js +223 -0
- package/dist/settings-transaction.js.map +1 -0
- package/dist/setup-auth.d.ts +10 -18
- package/dist/setup-auth.js +14 -38
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +75 -18
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-guide.d.ts +13 -0
- package/dist/setup-guide.js +25 -0
- package/dist/setup-guide.js.map +1 -0
- package/dist/setup-host.d.ts +16 -0
- package/dist/setup-host.js +199 -35
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +5 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/slash-authz.d.ts +17 -3
- package/dist/slash-authz.js +5 -0
- package/dist/slash-authz.js.map +1 -1
- package/dist/spawn-gate.js +3 -3
- package/dist/spawn-gate.js.map +1 -1
- package/dist/sqlite-open-errors.d.ts +12 -0
- package/dist/sqlite-open-errors.js +13 -0
- package/dist/sqlite-open-errors.js.map +1 -0
- package/dist/statusline-watcher.d.ts +5 -0
- package/dist/statusline-watcher.js +2 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/steer-capability.d.ts +6 -0
- package/dist/steer-capability.js +8 -0
- package/dist/steer-capability.js.map +1 -1
- package/dist/sync-work-attribution.d.ts +14 -0
- package/dist/sync-work-attribution.js +43 -8
- package/dist/sync-work-attribution.js.map +1 -1
- package/dist/tips.js +10 -5
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.d.ts +57 -5
- package/dist/tmux-control.js +379 -61
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.d.ts +20 -6
- package/dist/tmux-manager.js +90 -47
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +59 -0
- package/dist/tmux-read.js +213 -0
- package/dist/tmux-read.js.map +1 -0
- package/dist/topic-commands.d.ts +21 -10
- package/dist/topic-commands.js +205 -63
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.js +2 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +2 -0
- package/dist/tunnel/cloudflared-install.js +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +14 -0
- package/dist/tunnel/purpose-lane.js +43 -0
- package/dist/tunnel/purpose-lane.js.map +1 -0
- package/dist/turn-reply-guard.d.ts +2 -0
- package/dist/turn-reply-guard.js +10 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/ui/app.html +18 -0
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +478 -0
- package/dist/ui/chat-store.js +318 -0
- package/dist/ui/chat-strings.js +89 -0
- package/dist/ui/chat-thread.js +246 -0
- package/dist/ui/chat-tour.js +116 -0
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/fleet-org.js +142 -0
- package/dist/ui/panel-chat.js +504 -0
- package/dist/ui/panel-fleet.js +427 -0
- package/dist/ui/panel-needs.js +202 -0
- package/dist/ui/panel-settings.js +754 -0
- package/dist/ui/preview-panel.js +202 -0
- package/dist/ui/preview.js +270 -0
- package/dist/ui/settings-apply.js +239 -0
- package/dist/ui/settings-confirm.js +186 -0
- package/dist/ui/settings-dialogs.js +546 -0
- package/dist/ui/settings-model.js +277 -0
- package/dist/ui/settings-strings.js +654 -0
- package/dist/ui/settings-wizard.js +140 -0
- package/dist/ui/shared/agend-auth.js +93 -0
- package/dist/ui/shared/app-ctx.js +80 -0
- package/dist/ui/shared/app-html.js +9 -0
- package/dist/ui/shared/app-i18n.js +95 -0
- package/dist/ui/shared/app-nav.js +73 -0
- package/dist/ui/shared/app-needs.js +114 -0
- package/dist/ui/shared/app-route.js +73 -0
- package/dist/ui/shared/app-session.js +66 -0
- package/dist/ui/shared/app-shell.js +346 -0
- package/dist/ui/shared/app-store.js +51 -0
- package/dist/ui/shared/app-stream.js +201 -0
- package/dist/ui/shared/app.css +820 -0
- package/dist/ui/shared/app.js +88 -0
- package/dist/ui/shared/fonts/OFL.txt +92 -0
- package/dist/ui/shared/fonts/README.md +21 -0
- package/dist/ui/shared/fonts/inter.woff2 +0 -0
- package/dist/ui/shared/htm.module.js +1 -0
- package/dist/ui/shared/panel-view.js +655 -0
- package/dist/ui/shared/preact-hooks.module.js +2 -0
- package/dist/ui/shared/preact.module.js +2 -0
- package/dist/ui/shared/signin.js +142 -0
- package/dist/ui/shared/theme.js +29 -0
- package/dist/ui/shared/tokens.css +153 -0
- package/dist/ui/shared/ui-confirm.js +43 -0
- package/dist/ui/shared/ui-dialog.js +45 -0
- package/dist/ui/shared/ui-icons.js +63 -0
- package/dist/ui/shared/ui-menu.js +39 -0
- package/dist/ui/shared/ui-states.js +20 -0
- package/dist/ui/shared/ui-toast.js +18 -0
- package/dist/ui/shared/vendor/htm.LICENSE +202 -0
- package/dist/ui/shared/vendor/preact.LICENSE +21 -0
- package/dist/ui/shared/vendor/vendor.json +39 -0
- package/dist/ui/shared/view-strings.js +309 -0
- package/dist/ui/signin.html +52 -0
- package/dist/update-check.d.ts +6 -2
- package/dist/update-check.js +12 -3
- package/dist/update-check.js.map +1 -1
- package/dist/update-dispatch.d.ts +28 -0
- package/dist/update-dispatch.js +73 -0
- package/dist/update-dispatch.js.map +1 -0
- package/dist/update-install.d.ts +129 -0
- package/dist/update-install.js +262 -0
- package/dist/update-install.js.map +1 -0
- package/dist/upgrade-notices.d.ts +17 -0
- package/dist/upgrade-notices.js +93 -0
- package/dist/upgrade-notices.js.map +1 -0
- package/dist/usage/statusline-usage.d.ts +2 -0
- package/dist/usage/statusline-usage.js +14 -10
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +6 -1
- package/dist/view-api.js +62 -51
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +85 -14
- package/dist/web-api.js +557 -77
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +71 -27
- package/dist/web-auth.js +169 -69
- package/dist/web-auth.js.map +1 -1
- package/dist/web-body.d.ts +3 -0
- package/dist/web-body.js +55 -0
- package/dist/web-body.js.map +1 -0
- package/dist/web-channel-echo.d.ts +9 -0
- package/dist/web-channel-echo.js +55 -0
- package/dist/web-channel-echo.js.map +1 -0
- package/dist/web-chat-history.d.ts +125 -0
- package/dist/web-chat-history.js +184 -0
- package/dist/web-chat-history.js.map +1 -0
- package/dist/web-commands.d.ts +77 -0
- package/dist/web-commands.js +81 -0
- package/dist/web-commands.js.map +1 -0
- package/dist/web-continuation.d.ts +7 -0
- package/dist/web-continuation.js +22 -0
- package/dist/web-continuation.js.map +1 -0
- package/dist/web-host-guard.d.ts +34 -0
- package/dist/web-host-guard.js +57 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-login.d.ts +72 -0
- package/dist/web-login.js +132 -0
- package/dist/web-login.js.map +1 -0
- package/dist/web-org.d.ts +55 -0
- package/dist/web-org.js +67 -0
- package/dist/web-org.js.map +1 -0
- package/dist/web-preview.d.ts +77 -0
- package/dist/web-preview.js +240 -0
- package/dist/web-preview.js.map +1 -0
- package/dist/web-request-context.d.ts +10 -0
- package/dist/web-request-context.js +13 -0
- package/dist/web-request-context.js.map +1 -0
- package/dist/web-session.d.ts +167 -0
- package/dist/web-session.js +389 -0
- package/dist/web-session.js.map +1 -0
- package/dist/web-shell-routes.d.ts +59 -0
- package/dist/web-shell-routes.js +96 -0
- package/dist/web-shell-routes.js.map +1 -0
- package/dist/web-upload.d.ts +183 -0
- package/dist/web-upload.js +468 -0
- package/dist/web-upload.js.map +1 -0
- package/dist/website-tips-renderer.d.ts +2 -2
- package/dist/website-tips-renderer.js +3 -3
- package/dist/website-tips-renderer.js.map +1 -1
- package/launcher/agend +110 -0
- package/launcher/agend-agent +110 -0
- package/launcher/agend-agent.cjs +4 -0
- package/launcher/agend.cjs +4 -0
- package/launcher/install-admission.cjs +105 -0
- package/launcher/launch.cjs +64 -0
- package/launcher/postinstall.cjs +114 -0
- package/launcher/run-in-process.cjs +13 -0
- package/launcher/runtime-platform.cjs +86 -0
- package/launcher/runtime-select.cjs +217 -0
- package/package.json +18 -7
- package/scripts/preinstall-guard.cjs +55 -0
- package/templates/launchd.plist.ejs +2 -2
- package/templates/systemd.service.ejs +1 -1
- package/dist/ui/dashboard.html +0 -774
- package/dist/ui/settings.html +0 -2165
- package/dist/ui/view.html +0 -1139
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// #1408 §5: Settings' Apply belongs to the app from the moment it is confirmed. The panel hands over an operation —
|
|
2
|
+
// the staged changes as data (method, URL, body, label; never a DOM closure) — before the first write, and this
|
|
3
|
+
// runner performs the writes one by one, the POST /api/settings/apply (one key, made now, reused by every retry), and
|
|
4
|
+
// the one watcher of its job. It publishes progress to the app store (appStore.settingsOp) and never touches a panel's
|
|
5
|
+
// DOM: Settings renders from the store and re-attaches when it mounts again, the shell shows a line in any other panel.
|
|
6
|
+
//
|
|
7
|
+
// A write an admin must confirm (#1423) waits here, in the operation, until it is decided (settings-confirm.js).
|
|
8
|
+
// Secrets (a bot token being rotated) live only inside the operation and are dropped as their write finishes, or when
|
|
9
|
+
// the operation stops.
|
|
10
|
+
import { appStore } from "/assets/app-store.js";
|
|
11
|
+
import { t } from "/assets/app-i18n.js";
|
|
12
|
+
import { toast } from "/assets/ui-toast.js";
|
|
13
|
+
import "./settings-strings.js";
|
|
14
|
+
import { api, confirmedWrite, newKey } from "./settings-confirm.js";
|
|
15
|
+
|
|
16
|
+
const WATCH_LIMIT_MS = 10 * 60_000;
|
|
17
|
+
const POST_RETRIES = [1000, 2000, 4000];
|
|
18
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
19
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
20
|
+
const ACTIVE = new Set(["writing", "posting", "watching"]);
|
|
21
|
+
|
|
22
|
+
let op = null; // the operation in hand (the store holds its public view)
|
|
23
|
+
let seq = 0;
|
|
24
|
+
|
|
25
|
+
function view() {
|
|
26
|
+
if (!op) return null;
|
|
27
|
+
return {
|
|
28
|
+
id: op.id, phase: op.phase, error: op.error, job: op.job, lostJob: op.lostJob, restart: op.restart,
|
|
29
|
+
steps: op.steps.map(({ label, impact, status, error, pendingId }) => ({ label, impact, status, error: error || null, pendingId: pendingId || null })),
|
|
30
|
+
leftover: op.leftover,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function publish() { appStore.set({ settingsOp: view() }); }
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An operation is in hand and not finished — its writes, its apply job, or a restart of AgEnD it asked for (from the
|
|
37
|
+
* press until that restart's watch ends, #1453 review): Apply stays off, and no other operation can take its place.
|
|
38
|
+
*/
|
|
39
|
+
export function operationActive() { return !!op && (ACTIVE.has(op.phase) || op.restart === "busy"); }
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Take over an Apply. `changes`: [{ label, impact, request: { method, url, body, sensitive? }, key? } | { label, impact,
|
|
43
|
+
* connectionSecret: { id, secret } }], in order. Returns false (and does nothing) while another operation is active.
|
|
44
|
+
* A change that brings its `key` is the retry of a write whose outcome was never learned: the same key rejoins the
|
|
45
|
+
* request the server may already hold. A `sensitive` request body (the wizard's token) is dropped when its write
|
|
46
|
+
* completes (answered, refused or failed), and such a change is never staged again.
|
|
47
|
+
*/
|
|
48
|
+
export function startOperation(changes) {
|
|
49
|
+
if (operationActive()) return false;
|
|
50
|
+
op = {
|
|
51
|
+
id: ++seq,
|
|
52
|
+
phase: "writing", error: null, job: null, lostJob: false, restart: "idle", leftover: [],
|
|
53
|
+
applyKey: newKey("apply"), // made now, before the first write; every POST retry reuses it
|
|
54
|
+
steps: changes.map((c) => ({ ...c, request: c.request ? { ...c.request } : undefined, key: c.key || newKey("write"), status: "queued" })),
|
|
55
|
+
};
|
|
56
|
+
publish();
|
|
57
|
+
run(op);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Forget a finished operation (its card's ×). */
|
|
62
|
+
export function dismissOperation() {
|
|
63
|
+
if (op && !ACTIVE.has(op.phase) && op.restart !== "busy") { op = null; publish(); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Unrun changes of a failed operation, as data, for the panel to stage again (secrets excluded); taken once. */
|
|
67
|
+
export function takeLeftover() {
|
|
68
|
+
if (!op || !op.leftover.length) return [];
|
|
69
|
+
const out = op.leftover;
|
|
70
|
+
op.leftover = [];
|
|
71
|
+
publish();
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const mine = (o) => o === op;
|
|
76
|
+
|
|
77
|
+
async function run(o) {
|
|
78
|
+
for (const step of o.steps) {
|
|
79
|
+
if (!mine(o)) return;
|
|
80
|
+
step.status = "running"; publish();
|
|
81
|
+
let res;
|
|
82
|
+
// A transport failure leaves the outcome unknown: the server may have taken the write (or made it a pending
|
|
83
|
+
// request). That is not a refusal — the step keeps its key, so trying again rejoins it (#1453 review).
|
|
84
|
+
try { res = await perform(step, o); }
|
|
85
|
+
catch (err) { res = { ok: false, ambiguous: true, body: { error: err && err.message ? err.message : "network" } }; }
|
|
86
|
+
finally {
|
|
87
|
+
if (step.connectionSecret) step.connectionSecret.secret = "";
|
|
88
|
+
if (step.request && step.request.sensitive) step.request.body = null;
|
|
89
|
+
}
|
|
90
|
+
if (!mine(o)) return;
|
|
91
|
+
step.pendingId = null;
|
|
92
|
+
// The wizard's commit says when ~/.agend/.env is readable by others.
|
|
93
|
+
if (res && res.ok && res.body && res.body.secret_mode_ok === false) toast(t("settings.wizardEnvPerms"), false);
|
|
94
|
+
if (!res || !res.ok) {
|
|
95
|
+
step.status = "failed";
|
|
96
|
+
step.ambiguous = !!(res && res.ambiguous);
|
|
97
|
+
step.error = (res && res.body && (res.body.error || res.body.result)) || "failed";
|
|
98
|
+
stop(o, step);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
step.status = "done"; publish();
|
|
102
|
+
}
|
|
103
|
+
if (!mine(o)) return;
|
|
104
|
+
o.phase = "posting"; publish();
|
|
105
|
+
const started = await postApply(o);
|
|
106
|
+
if (!mine(o)) return;
|
|
107
|
+
if (!started) return;
|
|
108
|
+
o.job = started; o.phase = "watching"; publish();
|
|
109
|
+
await watch(o, started.id);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The step's write; a connection's token rotation is verify → apply → its own short job. */
|
|
113
|
+
async function perform(step, o) {
|
|
114
|
+
if (step.request) {
|
|
115
|
+
const { method, url, body } = step.request;
|
|
116
|
+
return confirmedWrite(url, { method, body, key: step.key, label: step.label,
|
|
117
|
+
onPending: (id) => { if (mine(o)) { step.pendingId = id; step.status = "waiting"; publish(); } } });
|
|
118
|
+
}
|
|
119
|
+
if (step.connectionSecret) return rotateConnectionSecret(step, o);
|
|
120
|
+
return { ok: false, body: { error: "unknown change" } };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function rotateConnectionSecret(step, o) {
|
|
124
|
+
const { id } = step.connectionSecret;
|
|
125
|
+
const base = `/api/settings/connections/${encodeURIComponent(id)}/secret`;
|
|
126
|
+
const verified = await api(`${base}/verify`, { method: "POST", headers: { "Idempotency-Key": step.key },
|
|
127
|
+
body: JSON.stringify({ secret: step.connectionSecret.secret, idempotency_key: step.key }) });
|
|
128
|
+
step.connectionSecret.secret = ""; // the server holds it from here (by verification id)
|
|
129
|
+
if (!verified.ok) return verified;
|
|
130
|
+
const applied = await confirmedWrite(`${base}/apply`, { method: "POST", key: step.key, label: step.label,
|
|
131
|
+
body: { verification_id: verified.body && verified.body.verification_id, idempotency_key: step.key },
|
|
132
|
+
onPending: (pid) => { if (mine(o)) { step.pendingId = pid; step.status = "waiting"; publish(); } } });
|
|
133
|
+
if (!applied.ok && applied.status !== 409) return applied;
|
|
134
|
+
let result = applied.body || {};
|
|
135
|
+
for (let n = 0; result.job_id && result.result === "applying" && n < 60; n++) {
|
|
136
|
+
await sleep(250);
|
|
137
|
+
const status = await api(`${base}/apply/${encodeURIComponent(result.job_id)}`).catch(() => null);
|
|
138
|
+
if (status && status.ok) result = status.body || result;
|
|
139
|
+
}
|
|
140
|
+
const ok = result.result === "applied" || result.result === "restart_required";
|
|
141
|
+
return { ok, status: ok ? 200 : 409, body: ok ? result : { error: result.error || result.result || "apply failed" } };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stop(o, failed) {
|
|
145
|
+
o.phase = "failed";
|
|
146
|
+
o.error = failed ? failed.error : o.error;
|
|
147
|
+
let after = false;
|
|
148
|
+
for (const step of o.steps) {
|
|
149
|
+
if (step === failed) after = true;
|
|
150
|
+
else if (after && step.status === "queued") step.status = "skipped";
|
|
151
|
+
if (step.connectionSecret) step.connectionSecret.secret = "";
|
|
152
|
+
}
|
|
153
|
+
// What did not land can be staged again, except a secret, which has to be entered again. A write whose outcome is
|
|
154
|
+
// unknown goes back with its key; one the server refused, or that never ran, gets a new key when it is applied.
|
|
155
|
+
o.leftover = o.steps.filter((s) => (s.status === "failed" || s.status === "skipped") && s.request && !s.request.sensitive)
|
|
156
|
+
.map(({ label, impact, request, stageKey, key, ambiguous }) => ({ label, impact, request, stageKey, ...(ambiguous ? { key } : {}) }));
|
|
157
|
+
publish();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function postApply(o) {
|
|
161
|
+
for (let attempt = 0; ; attempt++) {
|
|
162
|
+
let res;
|
|
163
|
+
try {
|
|
164
|
+
res = await api("/api/settings/apply", { method: "POST", headers: { "Idempotency-Key": o.applyKey },
|
|
165
|
+
body: JSON.stringify({ idempotency_key: o.applyKey }) });
|
|
166
|
+
} catch {
|
|
167
|
+
// The answer was lost (a phone between networks): the same key rejoins the same job, never a second one.
|
|
168
|
+
if (!mine(o)) return null;
|
|
169
|
+
if (attempt < POST_RETRIES.length) { await sleep(POST_RETRIES[attempt]); if (!mine(o)) return null; continue; }
|
|
170
|
+
res = { ok: false, status: 0, body: { error: "network" } };
|
|
171
|
+
}
|
|
172
|
+
if (!mine(o)) return null;
|
|
173
|
+
if (res.status === 409) { o.phase = "failed"; o.error = "busy"; publish(); return null; }
|
|
174
|
+
if (!res.ok || !res.body || !res.body.id) { o.phase = "failed"; o.error = (res.body && res.body.error) || "failed"; publish(); return null; }
|
|
175
|
+
return res.body;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** GET is the authority; a restart takes the server away under the request, which is the middle of it, not the end. */
|
|
180
|
+
async function watch(o, id) {
|
|
181
|
+
const deadline = now() + WATCH_LIMIT_MS;
|
|
182
|
+
let current = o.job;
|
|
183
|
+
while (current && current.status === "running" && now() < deadline) {
|
|
184
|
+
await sleep(1000);
|
|
185
|
+
if (!mine(o)) return;
|
|
186
|
+
let res;
|
|
187
|
+
try { res = await api(`/api/settings/apply/${encodeURIComponent(id)}`); } catch { continue; }
|
|
188
|
+
if (!mine(o)) return;
|
|
189
|
+
// The job outlives the process that made it, so a 404 means it aged out of the store, not that it never ran.
|
|
190
|
+
if (res.status === 404) { o.lostJob = true; o.phase = "done"; publish(); return; }
|
|
191
|
+
if (!res.ok || !res.body || !res.body.id) continue;
|
|
192
|
+
current = res.body; o.job = current; publish();
|
|
193
|
+
}
|
|
194
|
+
if (!mine(o)) return;
|
|
195
|
+
o.phase = "done"; publish();
|
|
196
|
+
// Said once, wherever the person is now: a restart reminder and a partial result are errors, a clean apply is not.
|
|
197
|
+
const rows = (current && current.targets) || [];
|
|
198
|
+
const failed = rows.some((r) => r.status === "failed"), restart = rows.some((r) => r.status === "restart-required");
|
|
199
|
+
if (!o.lostJob) toast(t(failed ? "settings.applyPartial" : restart ? "settings.applyRestartNeeded" : "settings.changesApplied"), !(failed || restart));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* A full AgEnD restart for a job that needs one: its own confirmation (the caller's), its own key, one at a time. The
|
|
204
|
+
* job's fleet row moves to "running" on the server; that job is watched again.
|
|
205
|
+
*/
|
|
206
|
+
export async function restartFleet(forJob) {
|
|
207
|
+
const o = op;
|
|
208
|
+
if (!o || !o.job || o.restart === "busy" || ACTIVE.has(o.phase)) return { ok: false };
|
|
209
|
+
// A confirmation given for one job never restarts for another (the operation was replaced meanwhile).
|
|
210
|
+
if (forJob && (forJob.opId !== o.id || forJob.jobId !== o.job.id)) return { ok: false, stale: true };
|
|
211
|
+
o.restart = "busy"; publish();
|
|
212
|
+
const key = newKey("restart");
|
|
213
|
+
let res;
|
|
214
|
+
try {
|
|
215
|
+
res = await api("/api/settings/restart-fleet", { method: "POST", headers: { "Idempotency-Key": key },
|
|
216
|
+
body: JSON.stringify({ job_id: o.job.id, confirm: "restart-agend", idempotency_key: key }) });
|
|
217
|
+
} catch { res = { ok: false, status: 0, body: null }; }
|
|
218
|
+
if (!mine(o)) return res;
|
|
219
|
+
if (!res.ok) { o.restart = "idle"; publish(); return res; }
|
|
220
|
+
toast(t("settings.restartFleetStarted")); // when it is accepted; how it ends is said when it settles
|
|
221
|
+
const fresh = await api(`/api/settings/apply/${encodeURIComponent(o.job.id)}`).catch(() => null);
|
|
222
|
+
if (!mine(o)) return res;
|
|
223
|
+
o.job = fresh && fresh.ok && fresh.body && fresh.body.id ? fresh.body
|
|
224
|
+
: { ...o.job, status: "running", targets: o.job.targets.map((row) => (row.target === "fleet" ? { ...row, status: "running" } : row)) };
|
|
225
|
+
o.phase = "watching"; publish();
|
|
226
|
+
try { await watch(o, o.job.id); } finally { if (mine(o)) { o.restart = "idle"; publish(); } }
|
|
227
|
+
return res;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Leaving while writes or the POST are still to come would lose them: the browser asks first. Once the job exists,
|
|
231
|
+
// a reload re-attaches nothing but loses nothing either (the job runs on the server).
|
|
232
|
+
if (typeof window !== "undefined") {
|
|
233
|
+
window.addEventListener("beforeunload", (e) => {
|
|
234
|
+
if (op && (op.phase === "writing" || op.phase === "posting")) { e.preventDefault(); e.returnValue = ""; }
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** For tests. */
|
|
239
|
+
export function resetOperation() { op = null; publish(); }
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// #1408 step 3 + #1423: a Settings write an admin has to confirm. Changing access lists, secrets, connections, public
|
|
2
|
+
// exposure or control-bearing instance settings answers 202 { result: "pending_confirmation", pending_change } instead
|
|
3
|
+
// of applying; a fleet admin confirms the redacted diff in chat (or on the host: `agend settings confirm <id>`).
|
|
4
|
+
//
|
|
5
|
+
// This module is the page's one record of those requests, whatever panel made them (Settings, Fleet's config, Chat's
|
|
6
|
+
// delete, New instance). It lives as long as the page (ES module state), not as long as a panel: a request keeps being
|
|
7
|
+
// followed after Settings unmounts, and the shell shows it in any panel (appStore.pendingChanges, app-shell.js).
|
|
8
|
+
//
|
|
9
|
+
// Following one is a passive read (GET /api/settings/pending/:id is on the #1374 passive list and the public link's
|
|
10
|
+
// manifest): one request at a time, every POLL_MS while anything is open, nothing while nothing is. The countdown is
|
|
11
|
+
// the server's remaining_ms against a monotonic clock, never the display timestamp.
|
|
12
|
+
import { appStore } from "/assets/app-store.js";
|
|
13
|
+
|
|
14
|
+
const POLL_MS = 2000;
|
|
15
|
+
const SHOW_DONE_MS = 4000; // an applied request stays on screen this long, then goes
|
|
16
|
+
const TERMINAL = new Set(["applied", "rejected", "expired", "stale", "failed"]);
|
|
17
|
+
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
18
|
+
|
|
19
|
+
/** A key for one request, reused by every retry of it: a retry with the same key and body is the same request. */
|
|
20
|
+
export function newKey(prefix = "k") {
|
|
21
|
+
return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** fetch → { ok, status, body } (body null when it is not JSON). A network failure throws, as fetch does. */
|
|
25
|
+
export async function api(path, opts = {}) {
|
|
26
|
+
const headers = { ...(opts.headers || {}) };
|
|
27
|
+
if (opts.body !== undefined) headers["Content-Type"] = "application/json";
|
|
28
|
+
const r = await fetch(path, { ...opts, headers });
|
|
29
|
+
let body = null;
|
|
30
|
+
try { body = await r.json(); } catch { /* not JSON */ }
|
|
31
|
+
return { ok: r.ok, status: r.status, body };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A request id we can follow: a non-empty string (the server's are 32 hex characters). */
|
|
35
|
+
const usableId = (v) => typeof v === "string" && v.length > 0;
|
|
36
|
+
/** The 202 that means "waiting for an admin", not "done". */
|
|
37
|
+
export function isPendingResponse(res) {
|
|
38
|
+
return !!res && res.status === 202 && !!res.body && res.body.result === "pending_confirmation" && !!res.body.pending_change
|
|
39
|
+
&& usableId(res.body.pending_change.id);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A 202 whose body cannot be read, or that says pending_confirmation without a request to follow: the server may hold
|
|
43
|
+
* a request we cannot see. It is not done and not refused — unknown; the caller keeps the key to ask again with it.
|
|
44
|
+
*/
|
|
45
|
+
function isUnreadableAccept(res) {
|
|
46
|
+
return !!res && res.status === 202 && (!res.body || typeof res.body !== "object"
|
|
47
|
+
|| (res.body.result === "pending_confirmation" && !(res.body.pending_change && usableId(res.body.pending_change.id))));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const tracked = new Map(); // id → { view, at, label, waiters, done }
|
|
51
|
+
// Requests seen decided on this page, kept after their card is dismissed: an older read that still says "pending"
|
|
52
|
+
// (a reload's list answered late) must not bring one back (#1453 review).
|
|
53
|
+
const finals = new Map(); // id → its decided view
|
|
54
|
+
let timer = null, polling = false;
|
|
55
|
+
|
|
56
|
+
function publish() {
|
|
57
|
+
appStore.set({
|
|
58
|
+
pendingChanges: [...tracked.values()].map(({ view, at, label }) => ({
|
|
59
|
+
...view,
|
|
60
|
+
label: label || null,
|
|
61
|
+
// Monotonic: when the server's countdown reaches zero on this page's clock.
|
|
62
|
+
deadline: at + Math.max(0, Number(view.remaining_ms) || 0),
|
|
63
|
+
withdraw: view.can_withdraw && view.state === "pending" ? () => withdraw(view.id) : null,
|
|
64
|
+
dismiss: TERMINAL.has(view.state) ? () => forget(view.id) : null,
|
|
65
|
+
})),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function forget(id) {
|
|
70
|
+
const e = tracked.get(id);
|
|
71
|
+
if (!e) return;
|
|
72
|
+
if (e.doneTimer) clearTimeout(e.doneTimer);
|
|
73
|
+
tracked.delete(id);
|
|
74
|
+
publish();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Every read of a request (a poll, Withdraw's answer, the 202 itself) takes a number when it is sent; only an answer
|
|
79
|
+
* newer than the last one applied may change the view, and a decided request never goes back to undecided — so an
|
|
80
|
+
* older "pending" that arrives after Withdraw's "rejected" cannot bring it back (#1453 review).
|
|
81
|
+
*/
|
|
82
|
+
function ticket(e) { return ++e.issued; }
|
|
83
|
+
function update(view, token) {
|
|
84
|
+
const e = tracked.get(view.id);
|
|
85
|
+
if (!e) return;
|
|
86
|
+
if (token !== undefined && token < e.applied) return;
|
|
87
|
+
if (e.done && !TERMINAL.has(view.state)) return;
|
|
88
|
+
if (token !== undefined) e.applied = token;
|
|
89
|
+
e.view = view; e.at = now();
|
|
90
|
+
if (TERMINAL.has(view.state) && !e.done) {
|
|
91
|
+
e.done = true;
|
|
92
|
+
finals.set(view.id, view);
|
|
93
|
+
for (const resolve of e.waiters.splice(0)) resolve(view);
|
|
94
|
+
// A request that applied goes away by itself; one that did not stays until it is dismissed, so it is read.
|
|
95
|
+
if (view.state === "applied") e.doneTimer = setTimeout(() => forget(view.id), SHOW_DONE_MS);
|
|
96
|
+
}
|
|
97
|
+
publish();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function schedule() {
|
|
101
|
+
if (timer || polling) return;
|
|
102
|
+
if (![...tracked.values()].some(e => !e.done)) return;
|
|
103
|
+
timer = setTimeout(poll, POLL_MS);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function poll() {
|
|
107
|
+
timer = null;
|
|
108
|
+
polling = true;
|
|
109
|
+
try {
|
|
110
|
+
for (const e of [...tracked.values()]) {
|
|
111
|
+
if (e.done) continue;
|
|
112
|
+
let res;
|
|
113
|
+
const token = ticket(e);
|
|
114
|
+
try { res = await api(`/api/settings/pending/${encodeURIComponent(e.view.id)}`); } catch { continue; } // offline: next round
|
|
115
|
+
if (res.ok && res.body && res.body.id === e.view.id) update(res.body, token);
|
|
116
|
+
// 404: no longer this session's, or aged out of the store. Either way it will not apply now.
|
|
117
|
+
else if (res.status === 404) update({ ...e.view, state: "expired", can_withdraw: false,
|
|
118
|
+
outcome: { state: "expired", reason_code: "not_found", message: "" } }, token);
|
|
119
|
+
}
|
|
120
|
+
} finally { polling = false; schedule(); }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Follow a pending request until it is decided. Resolves with its final view (applied, rejected, expired, stale or
|
|
125
|
+
* failed). The same id twice is followed once.
|
|
126
|
+
*/
|
|
127
|
+
export function track(view, label) {
|
|
128
|
+
if (finals.has(view.id) && !tracked.has(view.id)) return Promise.resolve(finals.get(view.id)); // decided and dismissed
|
|
129
|
+
let e = tracked.get(view.id);
|
|
130
|
+
if (!e) { e = { view, at: now(), label, waiters: [], done: false, issued: 0, applied: 0 }; tracked.set(view.id, e); }
|
|
131
|
+
else if (label && !e.label) e.label = label;
|
|
132
|
+
const settled = e.done ? Promise.resolve(e.view) : new Promise((resolve) => e.waiters.push(resolve));
|
|
133
|
+
update(view, ticket(e));
|
|
134
|
+
schedule();
|
|
135
|
+
return settled;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Withdraw a request nobody has decided yet. The view it answers with is the truth (decided meanwhile, or not). */
|
|
139
|
+
export async function withdraw(id) {
|
|
140
|
+
const e = tracked.get(id);
|
|
141
|
+
if (!e) return false;
|
|
142
|
+
const token = ticket(e);
|
|
143
|
+
let res;
|
|
144
|
+
try { res = await api(`/api/settings/pending/${encodeURIComponent(id)}`, { method: "DELETE" }); } catch { return false; }
|
|
145
|
+
if (res.ok && res.body && res.body.id === id) { update(res.body, token); return true; }
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** This session's requests still open on the server (after a reload): followed like new ones. */
|
|
150
|
+
export async function attach() {
|
|
151
|
+
let res;
|
|
152
|
+
try { res = await api("/api/settings/pending"); } catch { return; }
|
|
153
|
+
if (!res.ok || !Array.isArray(res.body)) return;
|
|
154
|
+
for (const view of res.body) if (view && view.id && !TERMINAL.has(view.state)) track(view);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A write that may need confirmation. A 202 pending_confirmation is followed until decided and answered as the write
|
|
159
|
+
* itself would have been: applied → { ok: true, body: what the write answered }, anything else → { ok: false } with
|
|
160
|
+
* the reason. An accepted answer that cannot be read is { ok: false, ambiguous: true } (see isUnreadableAccept); a
|
|
161
|
+
* network failure throws. Every other answer is returned as it is. `key` is the request's Idempotency-Key: pass the same one to
|
|
162
|
+
* retry the same request. `onPending(id)` is told when it starts waiting.
|
|
163
|
+
*/
|
|
164
|
+
export async function confirmedWrite(path, { method = "POST", body, key = newKey("w"), label, headers, onPending } = {}) {
|
|
165
|
+
const res = await api(path, { method, headers: { ...(headers || {}), "Idempotency-Key": key }, body: body === undefined ? undefined : JSON.stringify(body) });
|
|
166
|
+
if (isUnreadableAccept(res)) return { ok: false, status: 202, ambiguous: true, body: { error: "unreadable_confirmation_response" } };
|
|
167
|
+
if (!isPendingResponse(res)) return res;
|
|
168
|
+
if (onPending) onPending(res.body.pending_change.id);
|
|
169
|
+
const final = await track(res.body.pending_change, label);
|
|
170
|
+
return outcomeResponse(final);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A decided request as the write's own answer. */
|
|
174
|
+
export function outcomeResponse(view) {
|
|
175
|
+
if (view.state === "applied") return { ok: true, status: 200, body: (view.outcome && view.outcome.result) || {}, confirmed: view };
|
|
176
|
+
const reason = (view.outcome && (view.outcome.message || view.outcome.reason_code)) || view.state;
|
|
177
|
+
return { ok: false, status: 409, body: { error: reason, pending_state: view.state }, confirmed: view };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** For tests: forget everything and stop following. */
|
|
181
|
+
export function resetConfirmations() {
|
|
182
|
+
for (const e of tracked.values()) if (e.doneTimer) clearTimeout(e.doneTimer);
|
|
183
|
+
tracked.clear(); finals.clear();
|
|
184
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
185
|
+
appStore.set({ pendingChanges: [] });
|
|
186
|
+
}
|