@huynle/hub 0.6.1 → 0.7.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/CHANGELOG.md +870 -0
- package/Dockerfile +33 -0
- package/README.md +1007 -66
- package/package.json +47 -2
- package/packages/claude-code/package.json +12 -0
- package/packages/claude-code/src/bridge.ts +79 -0
- package/packages/claude-code/src/daemon.ts +339 -0
- package/packages/claude-code/src/hook-input.ts +44 -0
- package/packages/claude-code/src/ids.ts +27 -0
- package/packages/claude-code/src/index.ts +18 -0
- package/packages/claude-code/src/launcher.ts +265 -0
- package/packages/claude-code/src/main.ts +127 -0
- package/packages/claude-code/src/mcp.ts +109 -0
- package/packages/claude-code/src/paths.ts +34 -0
- package/packages/claude-code/src/plugin.ts +99 -0
- package/packages/claude-code/src/references.ts +51 -0
- package/packages/claude-code/src/sessions.ts +600 -0
- package/packages/claude-code/src/tools.ts +66 -0
- package/packages/claude-code/src/wire.ts +71 -0
- package/packages/claude-code/src/wrapper.ts +156 -0
- package/packages/cli/dist/hub-cli.js +21647 -0
- package/packages/cli/package.json +11 -0
- package/packages/cli/src/acting.ts +70 -0
- package/packages/cli/src/args.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/archive-stream.ts +276 -0
- package/packages/cli/src/bootstrap/adapters/claude/README.md +63 -0
- package/packages/cli/src/bootstrap/adapters/claude/adapter.ts +250 -0
- package/packages/cli/src/bootstrap/adapters/claude/index.ts +42 -0
- package/packages/cli/src/bootstrap/adapters/claude/node.ts +99 -0
- package/packages/cli/src/bootstrap/adapters/claude/openpgp.ts +221 -0
- package/packages/cli/src/bootstrap/adapters/claude/ports.ts +44 -0
- package/packages/cli/src/bootstrap/adapters/claude/source.ts +160 -0
- package/packages/cli/src/bootstrap/adapters/claude/verify.ts +165 -0
- package/packages/cli/src/bootstrap/adapters/codex/archive.ts +103 -0
- package/packages/cli/src/bootstrap/adapters/codex/index.ts +60 -0
- package/packages/cli/src/bootstrap/adapters/codex/plan.ts +129 -0
- package/packages/cli/src/bootstrap/adapters/codex/sigstore.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/codex/source.ts +102 -0
- package/packages/cli/src/bootstrap/adapters/codex/verify.ts +298 -0
- package/packages/cli/src/bootstrap/adapters/inflate.ts +433 -0
- package/packages/cli/src/bootstrap/adapters/node-services/control.ts +392 -0
- package/packages/cli/src/bootstrap/adapters/node-services/index.ts +23 -0
- package/packages/cli/src/bootstrap/adapters/node-services/launch.ts +223 -0
- package/packages/cli/src/bootstrap/adapters/node-services/plan.ts +222 -0
- package/packages/cli/src/bootstrap/adapters/node-services/render.ts +183 -0
- package/packages/cli/src/bootstrap/adapters/node-services/validate.ts +197 -0
- package/packages/cli/src/bootstrap/adapters/opencode/README.md +204 -0
- package/packages/cli/src/bootstrap/adapters/opencode/apply.ts +205 -0
- package/packages/cli/src/bootstrap/adapters/opencode/archive.ts +91 -0
- package/packages/cli/src/bootstrap/adapters/opencode/config.ts +212 -0
- package/packages/cli/src/bootstrap/adapters/opencode/index.ts +81 -0
- package/packages/cli/src/bootstrap/adapters/opencode/plan.ts +266 -0
- package/packages/cli/src/bootstrap/adapters/opencode/verify.ts +208 -0
- package/packages/cli/src/bootstrap/adapters/readiness.ts +19 -0
- package/packages/cli/src/bootstrap/ansible/index.ts +2 -0
- package/packages/cli/src/bootstrap/ansible/render.ts +279 -0
- package/packages/cli/src/bootstrap/contracts/actions.ts +470 -0
- package/packages/cli/src/bootstrap/contracts/artifact.ts +323 -0
- package/packages/cli/src/bootstrap/contracts/canonical.ts +54 -0
- package/packages/cli/src/bootstrap/contracts/credentials.ts +108 -0
- package/packages/cli/src/bootstrap/contracts/discovery.ts +215 -0
- package/packages/cli/src/bootstrap/contracts/envelope.ts +143 -0
- package/packages/cli/src/bootstrap/contracts/exec.ts +74 -0
- package/packages/cli/src/bootstrap/contracts/harness.ts +202 -0
- package/packages/cli/src/bootstrap/contracts/index.ts +28 -0
- package/packages/cli/src/bootstrap/contracts/journal.ts +198 -0
- package/packages/cli/src/bootstrap/contracts/paths.ts +91 -0
- package/packages/cli/src/bootstrap/contracts/plan.ts +155 -0
- package/packages/cli/src/bootstrap/contracts/platform.ts +62 -0
- package/packages/cli/src/bootstrap/contracts/preview.ts +190 -0
- package/packages/cli/src/bootstrap/contracts/profile.ts +105 -0
- package/packages/cli/src/bootstrap/contracts/secrets.ts +52 -0
- package/packages/cli/src/bootstrap/contracts/validate.ts +212 -0
- package/packages/cli/src/bootstrap/discovery/discover.ts +146 -0
- package/packages/cli/src/bootstrap/discovery/executables.ts +134 -0
- package/packages/cli/src/bootstrap/discovery/host.ts +96 -0
- package/packages/cli/src/bootstrap/discovery/index.ts +6 -0
- package/packages/cli/src/bootstrap/discovery/node-probe.ts +97 -0
- package/packages/cli/src/bootstrap/discovery/probe.ts +41 -0
- package/packages/cli/src/bootstrap/discovery/util.ts +13 -0
- package/packages/cli/src/bootstrap/enrollment/enroll.ts +226 -0
- package/packages/cli/src/bootstrap/enrollment/index.ts +7 -0
- package/packages/cli/src/bootstrap/enrollment/policy.ts +54 -0
- package/packages/cli/src/bootstrap/enrollment/result.ts +63 -0
- package/packages/cli/src/bootstrap/enrollment/status.ts +119 -0
- package/packages/cli/src/bootstrap/enrollment/store.ts +173 -0
- package/packages/cli/src/bootstrap/preview/index.ts +5 -0
- package/packages/cli/src/bootstrap/preview/links.ts +31 -0
- package/packages/cli/src/bootstrap/preview/plan.ts +242 -0
- package/packages/cli/src/bootstrap/preview/preview.ts +104 -0
- package/packages/cli/src/bootstrap/preview/render.ts +30 -0
- package/packages/cli/src/bootstrap/profiles/audit.ts +87 -0
- package/packages/cli/src/bootstrap/profiles/catalog.ts +49 -0
- package/packages/cli/src/bootstrap/profiles/desired.ts +96 -0
- package/packages/cli/src/bootstrap/profiles/drift.ts +286 -0
- package/packages/cli/src/bootstrap/profiles/history.ts +263 -0
- package/packages/cli/src/bootstrap/profiles/index.ts +11 -0
- package/packages/cli/src/bootstrap/profiles/upgrade.ts +276 -0
- package/packages/cli/src/bootstrap/profiles/versions.ts +50 -0
- package/packages/cli/src/bootstrap/release/README.md +115 -0
- package/packages/cli/src/bootstrap/release/assets.ts +39 -0
- package/packages/cli/src/bootstrap/release/cli.ts +332 -0
- package/packages/cli/src/bootstrap/release/harness-pins.json +94 -0
- package/packages/cli/src/bootstrap/release/manifest.ts +118 -0
- package/packages/cli/src/bootstrap/release/pins.ts +192 -0
- package/packages/cli/src/bootstrap/release/text-assets.d.ts +9 -0
- package/packages/cli/src/bootstrap/release/trust-root.json +13 -0
- package/packages/cli/src/bootstrap/release/trust-root.ts +39 -0
- package/packages/cli/src/bootstrap/release/verify.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/builtin.ts +312 -0
- package/packages/cli/src/bootstrap/transaction/engine.ts +994 -0
- package/packages/cli/src/bootstrap/transaction/executor.ts +112 -0
- package/packages/cli/src/bootstrap/transaction/fs.ts +194 -0
- package/packages/cli/src/bootstrap/transaction/index.ts +12 -0
- package/packages/cli/src/bootstrap/transaction/json-merge.ts +403 -0
- package/packages/cli/src/bootstrap/transaction/lock.ts +132 -0
- package/packages/cli/src/bootstrap/transaction/resolve.ts +48 -0
- package/packages/cli/src/bootstrap/transaction/result.ts +60 -0
- package/packages/cli/src/bootstrap/transaction/state.ts +88 -0
- package/packages/cli/src/bootstrap/transaction/store.ts +189 -0
- package/packages/cli/src/bootstrap/transaction/stream.ts +254 -0
- package/packages/cli/src/cli.ts +814 -0
- package/packages/cli/src/credentials.ts +75 -0
- package/packages/cli/src/decisions.ts +106 -0
- package/packages/cli/src/format.ts +185 -0
- package/packages/cli/src/grants.ts +105 -0
- package/packages/cli/src/http.ts +86 -0
- package/packages/cli/src/launches.ts +157 -0
- package/packages/cli/src/login.ts +160 -0
- package/packages/cli/src/main.ts +63 -0
- package/packages/cli/src/mcp.ts +879 -0
- package/packages/cli/src/messaging.ts +108 -0
- package/packages/cli/src/node.ts +279 -0
- package/packages/cli/src/openshell.ts +166 -0
- package/packages/cli/src/setup/commands.ts +809 -0
- package/packages/cli/src/setup/contributors.ts +171 -0
- package/packages/cli/src/setup/dispatch.ts +74 -0
- package/packages/cli/src/setup/launch-driver.ts +109 -0
- package/packages/cli/src/setup/runtime.ts +456 -0
- package/packages/cli/src/setup/spoke-probe.ts +120 -0
- package/packages/cli/src/setup/state.ts +244 -0
- package/packages/cli/src/setup/user-bin.ts +39 -0
- package/packages/cli/src/spoke-services.ts +109 -0
- package/packages/cli/src/watch.ts +28 -0
- package/packages/codex/package.json +12 -0
- package/packages/codex/src/app-server.ts +44 -0
- package/packages/codex/src/ids.ts +28 -0
- package/packages/codex/src/index.ts +11 -0
- package/packages/codex/src/jsonrpc.ts +168 -0
- package/packages/codex/src/main.ts +123 -0
- package/packages/codex/src/paths.ts +65 -0
- package/packages/codex/src/sessions.ts +578 -0
- package/packages/codex/src/spoke.ts +252 -0
- package/packages/codex/src/stdio.ts +101 -0
- package/packages/codex/src/websocket.ts +206 -0
- package/packages/codex/src/wrapper.ts +104 -0
- package/packages/node/package.json +12 -0
- package/packages/node/src/config.ts +184 -0
- package/packages/node/src/connection.ts +283 -0
- package/packages/node/src/daemon.ts +281 -0
- package/packages/node/src/git.ts +107 -0
- package/packages/node/src/harnesses.ts +95 -0
- package/packages/node/src/host.ts +319 -0
- package/packages/node/src/index.ts +17 -0
- package/packages/node/src/main.ts +11 -0
- package/packages/node/src/openshell.ts +73 -0
- package/packages/node/src/run.ts +106 -0
- package/packages/preset/package.json +12 -0
- package/packages/preset/presets/aider.json +8 -0
- package/packages/preset/presets/goose.json +8 -0
- package/packages/preset/src/index.ts +2 -0
- package/packages/preset/src/main.ts +195 -0
- package/packages/preset/src/presets.ts +87 -0
- package/packages/preset/src/spoke.ts +251 -0
- package/packages/preset/src/tmux.ts +160 -0
- package/packages/protocol/package.json +12 -0
- package/packages/protocol/src/harness-events.ts +207 -0
- package/packages/protocol/src/index.ts +1215 -0
- package/packages/protocol/src/launch.ts +172 -0
- package/packages/protocol/src/node.ts +409 -0
- package/packages/protocol/src/preset.ts +144 -0
- package/packages/protocol/src/webhook.ts +62 -0
- package/packages/protocol/src/wire.ts +54 -0
- package/packages/runtime-openshell/image/Dockerfile +36 -0
- package/packages/runtime-openshell/image/README.md +78 -0
- package/packages/runtime-openshell/image/build.ts +203 -0
- package/packages/runtime-openshell/image/entry.sh +163 -0
- package/packages/runtime-openshell/image/image.ts +119 -0
- package/packages/runtime-openshell/image/start.sh +24 -0
- package/packages/runtime-openshell/package.json +12 -0
- package/packages/runtime-openshell/src/cli.ts +584 -0
- package/packages/runtime-openshell/src/index.ts +6 -0
- package/packages/runtime-openshell/src/policy.ts +155 -0
- package/packages/runtime-openshell/src/profiles.ts +181 -0
- package/packages/runtime-openshell/src/runtime.ts +475 -0
- package/packages/runtime-sdk/package.json +12 -0
- package/packages/runtime-sdk/src/index.ts +87 -0
- package/packages/sdk/package.json +12 -0
- package/packages/sdk/src/address.ts +25 -0
- package/packages/sdk/src/client.ts +553 -0
- package/packages/sdk/src/envelope.ts +108 -0
- package/packages/sdk/src/errors.ts +107 -0
- package/packages/sdk/src/index.ts +10 -0
- package/packages/sdk/src/sse.ts +70 -0
- package/packages/sdk/src/types.ts +517 -0
- package/packages/sdk/src/webhook.ts +102 -0
- package/packages/spoke-sdk/package.json +12 -0
- package/packages/spoke-sdk/src/activity.ts +3 -0
- package/packages/spoke-sdk/src/connection.ts +295 -0
- package/packages/spoke-sdk/src/delivery.ts +119 -0
- package/packages/spoke-sdk/src/enroll.ts +101 -0
- package/packages/spoke-sdk/src/harness-hello.ts +79 -0
- package/packages/spoke-sdk/src/harness-spoke.ts +263 -0
- package/packages/spoke-sdk/src/index.ts +12 -0
- package/packages/spoke-sdk/src/prompt-refs.ts +154 -0
- package/packages/spoke-sdk/src/prompt-source.ts +8 -0
- package/packages/spoke-sdk/src/session-ids.ts +49 -0
- package/packages/spoke-sdk/src/session-tokens.ts +78 -0
- package/scripts/hub-claude-code.service +34 -0
- package/scripts/hub-codex-spoke.service +34 -0
- package/scripts/hub-node.service +35 -0
- package/scripts/io.github.huynle.hub-claude-code.plist +53 -0
- package/scripts/io.github.huynle.hub-codex-spoke.plist +53 -0
- package/scripts/io.github.huynle.hub-node.plist +52 -0
- package/src/central/actions.ts +233 -0
- package/src/central/agent/acting-routes.ts +133 -0
- package/src/central/agent/acting-switch.ts +93 -0
- package/src/central/agent/acting.ts +599 -0
- package/src/central/agent/actor.ts +193 -0
- package/src/central/agent/app-tokens.ts +152 -0
- package/src/central/agent/archive-inactive.ts +66 -0
- package/src/central/agent/audit.ts +185 -0
- package/src/central/agent/decisions.ts +530 -0
- package/src/central/agent/delivery.ts +678 -0
- package/src/central/agent/directory-routes.ts +328 -0
- package/src/central/agent/directory.ts +473 -0
- package/src/central/agent/envelope.ts +51 -0
- package/src/central/agent/errors.ts +74 -0
- package/src/central/agent/escalation.ts +219 -0
- package/src/central/agent/event-cursor.ts +49 -0
- package/src/central/agent/event-writers.ts +181 -0
- package/src/central/agent/events-routes.ts +148 -0
- package/src/central/agent/events.ts +277 -0
- package/src/central/agent/grant-requests.ts +221 -0
- package/src/central/agent/grant-routes.ts +70 -0
- package/src/central/agent/grants.ts +235 -0
- package/src/central/agent/launch-engine.ts +1025 -0
- package/src/central/agent/launch-routes.ts +252 -0
- package/src/central/agent/launch-spec.ts +289 -0
- package/src/central/agent/launch-store.ts +264 -0
- package/src/central/agent/launch-tokens.ts +206 -0
- package/src/central/agent/message-limits.ts +123 -0
- package/src/central/agent/message-routes.ts +385 -0
- package/src/central/agent/messages.ts +442 -0
- package/src/central/agent/names.ts +86 -0
- package/src/central/agent/placement.ts +419 -0
- package/src/central/agent/policy.ts +378 -0
- package/src/central/agent/read-routes.ts +385 -0
- package/src/central/agent/routes.ts +281 -0
- package/src/central/agent/session-tokens.ts +227 -0
- package/src/central/agent/versioned.ts +26 -0
- package/src/central/agent/watch.ts +112 -0
- package/src/central/agent/webhook-routes.ts +86 -0
- package/src/central/agent/webhooks.ts +493 -0
- package/src/central/agent-data.ts +177 -0
- package/src/central/auth.ts +350 -121
- package/src/central/backend/file-state.ts +240 -0
- package/src/central/backend/index.ts +116 -0
- package/src/central/backend/memory-bus.ts +93 -0
- package/src/central/backend/memory-catalog.ts +90 -0
- package/src/central/backend/memory-counters.ts +102 -0
- package/src/central/backend/memory-events.ts +45 -0
- package/src/central/backend/memory-kv.ts +38 -0
- package/src/central/backend/memory-state.ts +77 -0
- package/src/central/backend/postgres-migrate.ts +100 -0
- package/src/central/backend/postgres-state.ts +197 -0
- package/src/central/backend/types.ts +246 -0
- package/src/central/backend/valkey-bus.ts +425 -0
- package/src/central/backend/valkey-catalog.ts +190 -0
- package/src/central/backend/valkey-client.ts +36 -0
- package/src/central/backend/valkey-counters.ts +38 -0
- package/src/central/backend/valkey-events.ts +168 -0
- package/src/central/backend/valkey-kv.ts +54 -0
- package/src/central/backend/valkey-locks.ts +92 -0
- package/src/central/cluster/directory.ts +141 -0
- package/src/central/cluster/feed.ts +84 -0
- package/src/central/cluster/leader.ts +232 -0
- package/src/central/cluster/live.ts +151 -0
- package/src/central/cluster/recommend.ts +59 -0
- package/src/central/cluster/rpc.ts +150 -0
- package/src/central/cluster/view.ts +232 -0
- package/src/central/config.ts +197 -3
- package/src/central/done.ts +96 -0
- package/src/central/events-log.ts +100 -0
- package/src/central/http.ts +1149 -170
- package/src/central/import.ts +318 -0
- package/src/central/main.ts +211 -12
- package/src/central/mcp-http.ts +116 -0
- package/src/central/nodes.ts +314 -0
- package/src/central/oidc.ts +34 -14
- package/src/central/push-target.ts +118 -18
- package/src/central/push.ts +256 -82
- package/src/central/registry.ts +902 -92
- package/src/central/say-stream.ts +128 -0
- package/src/central/session-token-push.ts +233 -0
- package/src/central/speech/openai.ts +21 -14
- package/src/central/speech/openrouter.ts +27 -10
- package/src/central/speech/provider.ts +54 -0
- package/src/central/speech/service.ts +22 -7
- package/src/central/spoke-ws.ts +522 -44
- package/src/central/store-shared.ts +518 -0
- package/src/central/store.ts +625 -189
- package/src/central/token-keys.ts +165 -0
- package/src/config.ts +118 -7
- package/src/index.ts +195 -43
- package/src/protocol.ts +3 -650
- package/src/rpc.ts +27 -2
- package/src/spoke/client.ts +3 -272
- package/src/spoke/conversation.ts +16 -12
- package/src/spoke/deliver.ts +75 -0
- package/src/spoke/enroll.ts +2 -79
- package/src/spoke/env-inject.ts +160 -0
- package/src/spoke/hub-rpc.ts +115 -0
- package/src/spoke/hub-tool.ts +409 -0
- package/src/spoke/launch-source.ts +22 -0
- package/src/spoke/launchers.ts +174 -0
- package/src/spoke/locations.ts +3 -2
- package/src/spoke/node-roots.ts +61 -0
- package/src/spoke/prompt-refs.ts +116 -0
- package/src/spoke/session-tokens.ts +2 -0
- package/src/spoke/spoke.ts +465 -28
- package/src/spoke/taskboard.ts +243 -0
- package/src/spoke/timeline.ts +184 -6
- package/src/spoke/tracker.ts +79 -4
- package/src/status.ts +203 -10
- package/src/tui-commands.ts +13 -6
- package/src/tui-messaging.ts +93 -0
- package/src/tui.ts +15 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/highlight-y4rJE6Tt.js +5 -0
- package/web/dist/assets/index-DWnMhXk9.js +20 -0
- package/web/dist/assets/index-DmUXEnw9.css +1 -0
- package/web/dist/assets/ort-wasm-simd-threaded-CxTQ5xH-.wasm +0 -0
- package/web/dist/assets/silero-worker-C8-8vLVS.js +11 -0
- package/web/dist/assets/silero_vad-Dt9-bTDn.onnx +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/icon-192.png +0 -0
- package/web/dist/icon-512.png +0 -0
- package/web/dist/icon-maskable-512.png +0 -0
- package/web/dist/icon.svg +5 -5
- package/web/dist/index.html +17 -4
- package/web/dist/manifest.webmanifest +2 -2
- package/web/dist/sw.js +79 -15
- package/web/dist/assets/index-DClBvfLL.css +0 -1
- package/web/dist/assets/index-Djo_yF0a.js +0 -12
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// The node's settings and credentials. node.json (decision 6) sits next to the spoke's config.json, in the same
|
|
2
|
+
// directory (~/.config/hub, or wherever HUB_SPOKE_CONFIG points): the spoke's normalize() drops fields it does not
|
|
3
|
+
// know, so node settings never live in config.json. The node reads config.json only for the hub URL and the
|
|
4
|
+
// machine token (spoke and node share one machine record) and never writes it.
|
|
5
|
+
//
|
|
6
|
+
// node.json: { "workspaceRoots": ["/Users/huy/projects"], "labels": {"gpu": "no"}, "slots": 6, "memoryMB": 49152,
|
|
7
|
+
// "openshell": { "enabled": true, "gateway": "openshell", "image": "ghcr.io/huynle/hub-sandbox:0.1",
|
|
8
|
+
// "profiles": { "untrusted-code": { "providers": ["anthropic"] } } },
|
|
9
|
+
// "harnesses": { "claude-code": false } }
|
|
10
|
+
// `harnesses` overrides what the host runtime detects (harnesses.ts): true offers a harness, false withholds it.
|
|
11
|
+
import { readFileSync } from "node:fs"
|
|
12
|
+
import { homedir, hostname } from "node:os"
|
|
13
|
+
import path from "node:path"
|
|
14
|
+
import { isAbsolutePath, labelsProblem, NODE_ROOTS_MAX, NODE_SLOTS_MAX } from "../../protocol/src/index.ts"
|
|
15
|
+
import { normalizeOpenShellProfiles, type OpenShellProfiles } from "../../runtime-openshell/src/profiles.ts"
|
|
16
|
+
import { configPath, readConfig } from "../../../src/config.ts"
|
|
17
|
+
|
|
18
|
+
type Env = Readonly<Record<string, string | undefined>>
|
|
19
|
+
|
|
20
|
+
export const NODE_CONFIG_FILE = "node.json"
|
|
21
|
+
/** Concurrent launches when node.json sets no `slots`. */
|
|
22
|
+
export const DEFAULT_SLOTS = 4
|
|
23
|
+
const MEMORY_MB_MAX = 2 ** 31
|
|
24
|
+
const GATEWAY = /^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$/
|
|
25
|
+
const IMAGE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,254}$/
|
|
26
|
+
/** A harness id as hellos and launch specs spell it. */
|
|
27
|
+
const HARNESS = /^[a-z0-9][a-z0-9._-]{0,63}$/
|
|
28
|
+
|
|
29
|
+
export interface NodeConfig {
|
|
30
|
+
/** Every launch workspace must resolve (realpath) under one of these. Empty: the node prepares nothing. */
|
|
31
|
+
readonly workspaceRoots: readonly string[]
|
|
32
|
+
/** Labels the node adds to its own (os, arch); placement matches on them. */
|
|
33
|
+
readonly labels: Readonly<Record<string, string>>
|
|
34
|
+
readonly slots: number
|
|
35
|
+
readonly memoryMB?: number
|
|
36
|
+
/** The `openshell` runtime's settings; absent when node.json has no openshell section. */
|
|
37
|
+
readonly openshell?: OpenShellNodeConfig
|
|
38
|
+
/** Per-harness overrides of detection (true: offer it, false: withhold it); absent: detection alone. */
|
|
39
|
+
readonly harnesses?: Readonly<Record<string, boolean>>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** node.json `openshell`: whether the node offers sandboxes, through which gateway, with which image and profiles. */
|
|
43
|
+
export interface OpenShellNodeConfig {
|
|
44
|
+
readonly enabled: boolean
|
|
45
|
+
/** An OpenShell gateway name (`--gateway`); default: the CLI's active gateway. */
|
|
46
|
+
readonly gateway?: string
|
|
47
|
+
/** The image for profiles without their own (default: the runtime's). */
|
|
48
|
+
readonly image?: string
|
|
49
|
+
readonly profiles: OpenShellProfiles
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A config and what was wrong with the input (each bad field is dropped, with one problem line). */
|
|
53
|
+
export interface NodeConfigResult {
|
|
54
|
+
readonly config: NodeConfig
|
|
55
|
+
readonly problems: readonly string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** node.json in the spoke config's directory: ~/.config/hub/node.json, or next to $HUB_SPOKE_CONFIG. */
|
|
59
|
+
export function nodeConfigPath(env: Env = process.env, home: string = homedir()): string {
|
|
60
|
+
return path.join(path.dirname(configPath(env, home)), NODE_CONFIG_FILE)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
64
|
+
const whole = (v: unknown, min: number, max: number): v is number => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max
|
|
65
|
+
const show = (v: unknown) => JSON.stringify(v)?.slice(0, 80) ?? String(v)
|
|
66
|
+
|
|
67
|
+
/** Normalizes untrusted node.json content, field by field. */
|
|
68
|
+
export function normalizeNodeConfig(raw: unknown): NodeConfigResult {
|
|
69
|
+
const problems: string[] = []
|
|
70
|
+
let r: Record<string, unknown> = {}
|
|
71
|
+
if (isObject(raw)) r = raw
|
|
72
|
+
else if (raw !== undefined) problems.push("node.json must hold a JSON object")
|
|
73
|
+
|
|
74
|
+
const workspaceRoots: string[] = []
|
|
75
|
+
if (r.workspaceRoots !== undefined && !Array.isArray(r.workspaceRoots)) problems.push("workspaceRoots must be a list of absolute paths")
|
|
76
|
+
for (const root of Array.isArray(r.workspaceRoots) ? r.workspaceRoots : []) {
|
|
77
|
+
if (!isAbsolutePath(root) || root === "/") {
|
|
78
|
+
problems.push(`workspaceRoots: ${show(root)} is not an absolute path (without . or .. segments, and not /)`)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
const clean = root.replace(/\/+$/, "")
|
|
82
|
+
if (!workspaceRoots.includes(clean)) workspaceRoots.push(clean)
|
|
83
|
+
}
|
|
84
|
+
if (workspaceRoots.length > NODE_ROOTS_MAX) {
|
|
85
|
+
problems.push(`workspaceRoots: at most ${NODE_ROOTS_MAX} roots; the rest are ignored`)
|
|
86
|
+
workspaceRoots.length = NODE_ROOTS_MAX
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let labels: Record<string, string> = {}
|
|
90
|
+
if (r.labels !== undefined) {
|
|
91
|
+
const problem = labelsProblem(r.labels)
|
|
92
|
+
if (problem) problems.push(`labels: ${problem}`)
|
|
93
|
+
else labels = { ...(r.labels as Record<string, string>) }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let slots = DEFAULT_SLOTS
|
|
97
|
+
if (r.slots !== undefined) {
|
|
98
|
+
if (whole(r.slots, 1, NODE_SLOTS_MAX)) slots = r.slots
|
|
99
|
+
else problems.push(`slots must be a whole number from 1 to ${NODE_SLOTS_MAX}`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let memoryMB: number | undefined
|
|
103
|
+
if (r.memoryMB !== undefined) {
|
|
104
|
+
if (whole(r.memoryMB, 1, MEMORY_MB_MAX)) memoryMB = r.memoryMB
|
|
105
|
+
else problems.push("memoryMB must be a positive whole number")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const openshell = r.openshell === undefined ? undefined : normalizeOpenShell(r.openshell, problems)
|
|
109
|
+
const harnesses = r.harnesses === undefined ? undefined : normalizeHarnesses(r.harnesses, problems)
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
config: { workspaceRoots, labels, slots, ...(memoryMB !== undefined ? { memoryMB } : {}), ...(openshell ? { openshell } : {}), ...(harnesses ? { harnesses } : {}) },
|
|
113
|
+
problems,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeHarnesses(raw: unknown, problems: string[]): Record<string, boolean> | undefined {
|
|
118
|
+
if (!isObject(raw)) {
|
|
119
|
+
problems.push("harnesses must be an object of harness names to true or false")
|
|
120
|
+
return undefined
|
|
121
|
+
}
|
|
122
|
+
const out: Record<string, boolean> = {}
|
|
123
|
+
for (const [name, on] of Object.entries(raw)) {
|
|
124
|
+
if (!HARNESS.test(name)) problems.push(`harnesses: ${show(name)} is not a harness name`)
|
|
125
|
+
else if (typeof on !== "boolean") problems.push(`harnesses.${name} must be true or false`)
|
|
126
|
+
else out[name] = on
|
|
127
|
+
}
|
|
128
|
+
return out
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function normalizeOpenShell(raw: unknown, problems: string[]): OpenShellNodeConfig | undefined {
|
|
132
|
+
if (!isObject(raw)) {
|
|
133
|
+
problems.push("openshell must be an object")
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
let enabled = false
|
|
137
|
+
if (raw.enabled !== undefined) {
|
|
138
|
+
if (typeof raw.enabled === "boolean") enabled = raw.enabled
|
|
139
|
+
else problems.push("openshell.enabled must be true or false")
|
|
140
|
+
}
|
|
141
|
+
let gateway: string | undefined
|
|
142
|
+
if (raw.gateway !== undefined) {
|
|
143
|
+
if (typeof raw.gateway === "string" && GATEWAY.test(raw.gateway)) gateway = raw.gateway
|
|
144
|
+
else problems.push(`openshell.gateway: ${show(raw.gateway)} is not a gateway name`)
|
|
145
|
+
}
|
|
146
|
+
let image: string | undefined
|
|
147
|
+
if (raw.image !== undefined) {
|
|
148
|
+
if (typeof raw.image === "string" && IMAGE.test(raw.image)) image = raw.image
|
|
149
|
+
else problems.push(`openshell.image: ${show(raw.image)} is not an image reference`)
|
|
150
|
+
}
|
|
151
|
+
const profiles = normalizeOpenShellProfiles(raw.profiles)
|
|
152
|
+
problems.push(...profiles.problems)
|
|
153
|
+
return { enabled, ...(gateway ? { gateway } : {}), ...(image ? { image } : {}), profiles }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Reads node.json; a missing file is the defaults, unreadable or bad JSON the defaults with a problem. */
|
|
157
|
+
export function readNodeConfig(file: string): NodeConfigResult {
|
|
158
|
+
let text: string
|
|
159
|
+
try {
|
|
160
|
+
text = readFileSync(file, "utf8")
|
|
161
|
+
} catch (cause) {
|
|
162
|
+
if ((cause as NodeJS.ErrnoException).code === "ENOENT") return normalizeNodeConfig(undefined)
|
|
163
|
+
return { config: normalizeNodeConfig(undefined).config, problems: [`${file}: cannot read (${(cause as Error).message})`] }
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
return normalizeNodeConfig(JSON.parse(text))
|
|
167
|
+
} catch {
|
|
168
|
+
return { config: normalizeNodeConfig(undefined).config, problems: [`${file}: not valid JSON`] }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** What the node connects with: the hub URL, the machine token and the machine ID, from the spoke's config.json. */
|
|
173
|
+
export interface SpokeCredentials {
|
|
174
|
+
readonly server: string
|
|
175
|
+
readonly token: string
|
|
176
|
+
readonly machineID?: string
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The enrolled machine's credentials from config.json (read-only), or undefined when it holds no token. */
|
|
180
|
+
export function readSpokeCredentials(env: Env = process.env, home: string = homedir()): SpokeCredentials | undefined {
|
|
181
|
+
const config = readConfig(configPath(env, home), hostname())
|
|
182
|
+
if (!config.token) return undefined
|
|
183
|
+
return { server: config.server, token: config.token, ...(config.machineID ? { machineID: config.machineID } : {}) }
|
|
184
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// The node's single outbound WebSocket to the hub (design §6.3): /spoke/ws with the machine token (spoke and node
|
|
2
|
+
// share one machine record), a hello with role "node", then the hub's prepare/collect/cleanup/cancel requests
|
|
3
|
+
// answered in order of arrival. Reconnects with jittered exponential backoff; a hub that does not serve nodes (its
|
|
4
|
+
// welcome lacks "nodes", or it rejects the node hello before any welcome, as a hub older than nodes does) is
|
|
5
|
+
// retried only after a long wait, so an old hub is not hammered. A revoked machine stops for good. A bye carrying
|
|
6
|
+
// retryAfterMs (a hub pod draining, design §10) is followed by one short jittered reconnect (0–retryAfterMs) instead.
|
|
7
|
+
//
|
|
8
|
+
// The WebSocket is the runtime's global one with an Authorization header: Bun's accepts `{ headers }`, and so does
|
|
9
|
+
// Node's (undici, Node 22+), so this runs on both (bundled for node, or from source on bun).
|
|
10
|
+
import {
|
|
11
|
+
hubServesNodes,
|
|
12
|
+
MAX_FRAME,
|
|
13
|
+
NODE_ROLE,
|
|
14
|
+
parseNodeServerMessage,
|
|
15
|
+
PROTOCOL_VERSION,
|
|
16
|
+
type NodeCapacityFrame,
|
|
17
|
+
type NodeHello,
|
|
18
|
+
type NodeProgress,
|
|
19
|
+
type NodeRequest,
|
|
20
|
+
type Welcome,
|
|
21
|
+
} from "../../protocol/src/index.ts"
|
|
22
|
+
|
|
23
|
+
export type NodeConnectionState = "off" | "connecting" | "connected" | "unsupported" | "revoked" | "error"
|
|
24
|
+
|
|
25
|
+
export interface Backoff {
|
|
26
|
+
readonly minMs: number
|
|
27
|
+
readonly maxMs: number
|
|
28
|
+
readonly random?: () => number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Minimal WebSocket surface used here. */
|
|
32
|
+
export interface SocketLike {
|
|
33
|
+
readonly readyState: number
|
|
34
|
+
onopen: ((ev: unknown) => void) | null
|
|
35
|
+
onmessage: ((ev: { data: unknown }) => void) | null
|
|
36
|
+
onclose: ((ev: { code?: number; reason?: string }) => void) | null
|
|
37
|
+
onerror: ((ev: unknown) => void) | null
|
|
38
|
+
send(data: string): void
|
|
39
|
+
close(code?: number, reason?: string): void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NodeConnectionOptions {
|
|
43
|
+
readonly server: string
|
|
44
|
+
readonly token: string
|
|
45
|
+
/** Hello fields, computed per attempt so capacity, use and launches are current. */
|
|
46
|
+
readonly hello: () => Omit<NodeHello, "t" | "v" | "role">
|
|
47
|
+
/** Serves a hub request; a thrown error becomes `{ ok: false, error }`. */
|
|
48
|
+
readonly handle: (req: NodeRequest) => Promise<unknown>
|
|
49
|
+
readonly onWelcome?: (welcome: Welcome) => void
|
|
50
|
+
readonly onState?: (state: NodeConnectionState, error?: string) => void
|
|
51
|
+
readonly backoff?: Backoff
|
|
52
|
+
/** Wait before retrying a hub that does not serve nodes (default 30 minutes). */
|
|
53
|
+
readonly unsupportedRetryMs?: number
|
|
54
|
+
readonly socket?: (url: string, headers: Record<string, string>) => SocketLike
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const UNSUPPORTED_RETRY_MS = 30 * 60_000
|
|
58
|
+
|
|
59
|
+
export function backoffDelay(attempt: number, backoff: Backoff): number {
|
|
60
|
+
const base = Math.min(backoff.maxMs, backoff.minMs * 2 ** Math.min(attempt, 30))
|
|
61
|
+
return Math.round(base * (0.5 + 0.5 * (backoff.random ?? Math.random)()))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The reconnect delay a bye's retryAfterMs asks for: uniformly jittered over 0–retryAfterMs, so peers spread out. */
|
|
65
|
+
export function retryAfterDelay(retryAfterMs: number, random: () => number = Math.random): number {
|
|
66
|
+
return Math.round(retryAfterMs * random())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function wsURL(server: string): string {
|
|
70
|
+
return `${server.replace(/\/+$/, "").replace(/^http/, "ws")}/spoke/ws`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const defaultSocket = (url: string, headers: Record<string, string>): SocketLike =>
|
|
74
|
+
new (WebSocket as unknown as new (url: string, options: { headers: Record<string, string> }) => SocketLike)(url, { headers })
|
|
75
|
+
|
|
76
|
+
const message = (cause: unknown) => (cause instanceof Error ? cause.message : String(cause))
|
|
77
|
+
|
|
78
|
+
function text(data: unknown): string {
|
|
79
|
+
if (typeof data === "string") return data
|
|
80
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data).toString("utf8")
|
|
81
|
+
if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8")
|
|
82
|
+
return String(data)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The id of a frame that is a request, valid or not (so an invalid request still gets an answer). */
|
|
86
|
+
function requestID(raw: string): string | undefined {
|
|
87
|
+
if (raw.length > MAX_FRAME) return undefined
|
|
88
|
+
try {
|
|
89
|
+
const v = JSON.parse(raw)
|
|
90
|
+
return v && typeof v === "object" && v.t === "req" && typeof v.id === "string" && v.id.length > 0 && v.id.length <= 300 ? v.id : undefined
|
|
91
|
+
} catch {
|
|
92
|
+
return undefined
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class NodeConnection {
|
|
97
|
+
state: NodeConnectionState = "off"
|
|
98
|
+
lastError?: string
|
|
99
|
+
machineID?: string
|
|
100
|
+
publicURL?: string
|
|
101
|
+
|
|
102
|
+
private socket?: SocketLike
|
|
103
|
+
private welcomed = false
|
|
104
|
+
private stopped = true
|
|
105
|
+
private attempt = 0
|
|
106
|
+
private retry?: ReturnType<typeof setTimeout>
|
|
107
|
+
/** The last bye's retryAfterMs (a draining hub): the next reconnect waits about this instead of the backoff. */
|
|
108
|
+
private retryAfterMs?: number
|
|
109
|
+
private readonly backoff: Backoff
|
|
110
|
+
|
|
111
|
+
constructor(private readonly options: NodeConnectionOptions) {
|
|
112
|
+
this.backoff = options.backoff ?? { minMs: 1000, maxMs: 60_000 }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
start(): void {
|
|
116
|
+
if (!this.stopped) return
|
|
117
|
+
this.stopped = false
|
|
118
|
+
this.open()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
stop(): void {
|
|
122
|
+
this.stopped = true
|
|
123
|
+
clearTimeout(this.retry)
|
|
124
|
+
this.drop(1000, "stop")
|
|
125
|
+
this.setState("off")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Sends a capacity or progress frame once the hub has welcomed us. Returns false when it could not be sent. */
|
|
129
|
+
send(frame: NodeCapacityFrame | NodeProgress): boolean {
|
|
130
|
+
return this.write(frame)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private write(frame: unknown): boolean {
|
|
134
|
+
if (!this.socket || !this.welcomed || this.socket.readyState !== 1) return false
|
|
135
|
+
try {
|
|
136
|
+
this.socket.send(JSON.stringify(frame))
|
|
137
|
+
return true
|
|
138
|
+
} catch {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private open(): void {
|
|
144
|
+
if (this.stopped) return
|
|
145
|
+
this.setState(this.state === "error" || this.state === "unsupported" ? this.state : "connecting")
|
|
146
|
+
let socket: SocketLike
|
|
147
|
+
try {
|
|
148
|
+
socket = (this.options.socket ?? defaultSocket)(wsURL(this.options.server), { Authorization: `Bearer ${this.options.token}` })
|
|
149
|
+
} catch (cause) {
|
|
150
|
+
return this.fail(message(cause))
|
|
151
|
+
}
|
|
152
|
+
this.socket = socket
|
|
153
|
+
this.welcomed = false
|
|
154
|
+
let opened = false
|
|
155
|
+
let rejected = false
|
|
156
|
+
socket.onopen = () => {
|
|
157
|
+
opened = true
|
|
158
|
+
try {
|
|
159
|
+
socket.send(JSON.stringify({ t: "hello", v: PROTOCOL_VERSION, role: NODE_ROLE, ...this.options.hello() }))
|
|
160
|
+
} catch (cause) {
|
|
161
|
+
this.lastError = message(cause)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
socket.onmessage = (ev) => {
|
|
165
|
+
const raw = text(ev.data)
|
|
166
|
+
const frame = parseNodeServerMessage(raw)
|
|
167
|
+
if (!frame) {
|
|
168
|
+
const id = this.welcomed ? requestID(raw) : undefined
|
|
169
|
+
if (id) this.write({ t: "res", id, ok: false, error: "invalid request" })
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
switch (frame.t) {
|
|
173
|
+
case "welcome":
|
|
174
|
+
return this.welcome(frame)
|
|
175
|
+
case "bye":
|
|
176
|
+
if (frame.reason === "revoked") return this.revoked()
|
|
177
|
+
// A draining hub is no hub refusing nodes: come back soon (to another pod).
|
|
178
|
+
if (frame.retryAfterMs !== undefined) this.retryAfterMs = frame.retryAfterMs
|
|
179
|
+
else if (!this.welcomed) rejected = true
|
|
180
|
+
this.lastError = frame.reason
|
|
181
|
+
return
|
|
182
|
+
case "req":
|
|
183
|
+
if (this.welcomed) void this.answer(frame.id, frame.req)
|
|
184
|
+
return
|
|
185
|
+
case "res":
|
|
186
|
+
return // the node makes no requests of the hub
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
socket.onerror = () => {
|
|
190
|
+
this.lastError = this.welcomed ? "Connection error" : "Could not connect to the hub"
|
|
191
|
+
}
|
|
192
|
+
socket.onclose = (ev) => {
|
|
193
|
+
if (this.socket !== socket) return
|
|
194
|
+
this.socket = undefined
|
|
195
|
+
const wasWelcomed = this.welcomed
|
|
196
|
+
this.welcomed = false
|
|
197
|
+
if (this.stopped || this.state === "revoked") return
|
|
198
|
+
// Opened, then closed by the hub before any welcome: the hub refused the node hello.
|
|
199
|
+
if (!wasWelcomed && opened && (rejected || ev.code === 4000) && this.retryAfterMs === undefined) return this.unsupported(`The hub does not accept nodes${this.lastError ? ` (${this.lastError})` : ""}`)
|
|
200
|
+
this.fail(wasWelcomed ? `Connection closed${ev.reason ? `: ${ev.reason}` : ""}` : (this.lastError ?? "Could not connect"))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private welcome(frame: Welcome): void {
|
|
205
|
+
if (!hubServesNodes(frame)) {
|
|
206
|
+
this.drop(1000, "hub does not serve nodes")
|
|
207
|
+
return this.unsupported("The hub does not serve nodes (upgrade the hub)")
|
|
208
|
+
}
|
|
209
|
+
this.welcomed = true
|
|
210
|
+
this.attempt = 0
|
|
211
|
+
this.machineID = frame.machineID
|
|
212
|
+
this.publicURL = frame.publicURL
|
|
213
|
+
this.lastError = undefined
|
|
214
|
+
this.setState("connected")
|
|
215
|
+
try {
|
|
216
|
+
this.options.onWelcome?.(frame)
|
|
217
|
+
} catch (cause) {
|
|
218
|
+
console.error("[hub node] welcome handler failed", cause)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private async answer(id: string, req: NodeRequest): Promise<void> {
|
|
223
|
+
let reply: unknown
|
|
224
|
+
try {
|
|
225
|
+
reply = { t: "res", id, ok: true, result: await this.options.handle(req) }
|
|
226
|
+
} catch (cause) {
|
|
227
|
+
reply = { t: "res", id, ok: false, error: message(cause) }
|
|
228
|
+
}
|
|
229
|
+
if (JSON.stringify(reply).length > MAX_FRAME) reply = { t: "res", id, ok: false, error: "result too large to send" }
|
|
230
|
+
this.write(reply)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private revoked(): void {
|
|
234
|
+
this.stopped = true
|
|
235
|
+
clearTimeout(this.retry)
|
|
236
|
+
this.lastError = "This machine was revoked on the hub"
|
|
237
|
+
this.setState("revoked")
|
|
238
|
+
this.drop(1000, "revoked")
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private unsupported(error: string): void {
|
|
242
|
+
this.lastError = error
|
|
243
|
+
this.setState("unsupported")
|
|
244
|
+
this.schedule(this.options.unsupportedRetryMs ?? UNSUPPORTED_RETRY_MS)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private fail(error: string): void {
|
|
248
|
+
this.lastError = error
|
|
249
|
+
this.setState("error")
|
|
250
|
+
const retryAfterMs = this.retryAfterMs
|
|
251
|
+
this.retryAfterMs = undefined
|
|
252
|
+
this.schedule(retryAfterMs !== undefined ? retryAfterDelay(retryAfterMs, this.backoff.random) : backoffDelay(this.attempt++, this.backoff))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private schedule(ms: number): void {
|
|
256
|
+
if (this.stopped) return
|
|
257
|
+
clearTimeout(this.retry)
|
|
258
|
+
this.retry = setTimeout(() => this.open(), ms)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private drop(code: number, reason: string): void {
|
|
262
|
+
const socket = this.socket
|
|
263
|
+
this.socket = undefined
|
|
264
|
+
this.welcomed = false
|
|
265
|
+
if (!socket) return
|
|
266
|
+
socket.onopen = socket.onmessage = socket.onerror = socket.onclose = null
|
|
267
|
+
try {
|
|
268
|
+
socket.close(code, reason)
|
|
269
|
+
} catch {
|
|
270
|
+
// already closed
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private setState(state: NodeConnectionState): void {
|
|
275
|
+
if (state === this.state) return
|
|
276
|
+
this.state = state
|
|
277
|
+
try {
|
|
278
|
+
this.options.onState?.(state, this.lastError)
|
|
279
|
+
} catch {
|
|
280
|
+
// observers must not break the connection
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|