@fleetless/contracts 1.0.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/CHANGELOG.md +26 -0
- package/LICENSE +202 -0
- package/NOTICE +17 -0
- package/README.md +88 -0
- package/artifacts/constants.json +24 -0
- package/artifacts/openapi.json +17219 -0
- package/artifacts/routes.json +4605 -0
- package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
- package/artifacts/schema/action-config.schema.json +198 -0
- package/artifacts/schema/alert-list-response.schema.json +172 -0
- package/artifacts/schema/api-error.schema.json +20 -0
- package/artifacts/schema/app-auth-config.schema.json +106 -0
- package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
- package/artifacts/schema/app-invitation.schema.json +69 -0
- package/artifacts/schema/app-list-response.schema.json +82 -0
- package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
- package/artifacts/schema/app-mail-template.schema.json +54 -0
- package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
- package/artifacts/schema/app-oidc-provider.schema.json +80 -0
- package/artifacts/schema/app-user-list-response.schema.json +111 -0
- package/artifacts/schema/app-user.schema.json +98 -0
- package/artifacts/schema/app.schema.json +69 -0
- package/artifacts/schema/apply-error.schema.json +41 -0
- package/artifacts/schema/asset-list-response.schema.json +288 -0
- package/artifacts/schema/asset-sync-request.schema.json +17 -0
- package/artifacts/schema/asset-sync-response.schema.json +16 -0
- package/artifacts/schema/asset-sync-status.schema.json +136 -0
- package/artifacts/schema/asset.schema.json +68 -0
- package/artifacts/schema/audit-actor.schema.json +32 -0
- package/artifacts/schema/audit-event.schema.json +119 -0
- package/artifacts/schema/audit-list-response.schema.json +144 -0
- package/artifacts/schema/audit-query.schema.json +79 -0
- package/artifacts/schema/auth-error.schema.json +23 -0
- package/artifacts/schema/auth-me-response.schema.json +99 -0
- package/artifacts/schema/auth-ok.schema.json +115 -0
- package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
- package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
- package/artifacts/schema/bridge-assets-available.schema.json +25 -0
- package/artifacts/schema/bridge-camera-state.schema.json +78 -0
- package/artifacts/schema/bridge-config-applied.schema.json +67 -0
- package/artifacts/schema/bridge-hello.schema.json +65 -0
- package/artifacts/schema/bridge-introspect.schema.json +114 -0
- package/artifacts/schema/bridge-job-lost.schema.json +22 -0
- package/artifacts/schema/bridge-job-update.schema.json +100 -0
- package/artifacts/schema/bridge-pong.schema.json +19 -0
- package/artifacts/schema/bridge-pressure.schema.json +292 -0
- package/artifacts/schema/bridge-state.schema.json +24 -0
- package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
- package/artifacts/schema/busy-details.schema.json +115 -0
- package/artifacts/schema/camera-descriptor.schema.json +45 -0
- package/artifacts/schema/camera-list-response.schema.json +58 -0
- package/artifacts/schema/camera-source.schema.json +240 -0
- package/artifacts/schema/cancel-request.schema.json +20 -0
- package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
- package/artifacts/schema/client-auth.schema.json +18 -0
- package/artifacts/schema/client-cancel.schema.json +45 -0
- package/artifacts/schema/client-identity.schema.json +103 -0
- package/artifacts/schema/client-invoke.schema.json +45 -0
- package/artifacts/schema/client-login-request.schema.json +29 -0
- package/artifacts/schema/client-logout-request.schema.json +14 -0
- package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
- package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
- package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
- package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
- package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
- package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
- package/artifacts/schema/client-password-reset-request.schema.json +24 -0
- package/artifacts/schema/client-provider-list-query.schema.json +17 -0
- package/artifacts/schema/client-provider-list-response.schema.json +34 -0
- package/artifacts/schema/client-publish.schema.json +40 -0
- package/artifacts/schema/client-refresh-request.schema.json +14 -0
- package/artifacts/schema/client-register-request.schema.json +44 -0
- package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
- package/artifacts/schema/client-subscribe.schema.json +43 -0
- package/artifacts/schema/client-unsubscribe.schema.json +26 -0
- package/artifacts/schema/client-verify-email-request.schema.json +15 -0
- package/artifacts/schema/cloud-asset-request.schema.json +37 -0
- package/artifacts/schema/cloud-camera-start.schema.json +41 -0
- package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
- package/artifacts/schema/cloud-cancel.schema.json +33 -0
- package/artifacts/schema/cloud-config.schema.json +1635 -0
- package/artifacts/schema/cloud-hello-error.schema.json +23 -0
- package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
- package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
- package/artifacts/schema/cloud-invoke.schema.json +40 -0
- package/artifacts/schema/cloud-ping.schema.json +19 -0
- package/artifacts/schema/cloud-publish.schema.json +28 -0
- package/artifacts/schema/cloud-type-request.schema.json +30 -0
- package/artifacts/schema/command-result.schema.json +175 -0
- package/artifacts/schema/config-draft-response.schema.json +1695 -0
- package/artifacts/schema/config-state.schema.json +124 -0
- package/artifacts/schema/config-version-response.schema.json +1641 -0
- package/artifacts/schema/config-versions-response.schema.json +33 -0
- package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
- package/artifacts/schema/create-app-request.schema.json +30 -0
- package/artifacts/schema/create-app-user-request.schema.json +42 -0
- package/artifacts/schema/create-robot-request.schema.json +14 -0
- package/artifacts/schema/create-robot-response.schema.json +44 -0
- package/artifacts/schema/create-server-key-response.schema.json +65 -0
- package/artifacts/schema/create-team-invite-request.schema.json +43 -0
- package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
- package/artifacts/schema/datapoint-config.schema.json +366 -0
- package/artifacts/schema/datapoint-display.schema.json +31 -0
- package/artifacts/schema/datapoint-event.schema.json +34 -0
- package/artifacts/schema/datapoint-frame.schema.json +28 -0
- package/artifacts/schema/datapoint-list-response.schema.json +61 -0
- package/artifacts/schema/datapoint-value.schema.json +28 -0
- package/artifacts/schema/developer-login-request.schema.json +19 -0
- package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
- package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
- package/artifacts/schema/error-frame.schema.json +23 -0
- package/artifacts/schema/exposure-counts.schema.json +39 -0
- package/artifacts/schema/exposure-list-response.schema.json +43 -0
- package/artifacts/schema/fetch-types-request.schema.json +19 -0
- package/artifacts/schema/fetch-types-response.schema.json +163 -0
- package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
- package/artifacts/schema/fleetless-user.schema.json +60 -0
- package/artifacts/schema/history-buckets-response.schema.json +79 -0
- package/artifacts/schema/history-query.schema.json +58 -0
- package/artifacts/schema/history-response.schema.json +150 -0
- package/artifacts/schema/history-samples-response.schema.json +68 -0
- package/artifacts/schema/introspection-response.schema.json +118 -0
- package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
- package/artifacts/schema/invoke-request.schema.json +23 -0
- package/artifacts/schema/invoke-response.schema.json +125 -0
- package/artifacts/schema/job-actor.schema.json +34 -0
- package/artifacts/schema/job-event.schema.json +158 -0
- package/artifacts/schema/job-response.schema.json +123 -0
- package/artifacts/schema/job-run-list-response.schema.json +222 -0
- package/artifacts/schema/job-run-query.schema.json +95 -0
- package/artifacts/schema/job-run-summary-query.schema.json +23 -0
- package/artifacts/schema/job-run-summary.schema.json +33 -0
- package/artifacts/schema/job-run.schema.json +195 -0
- package/artifacts/schema/job-state.schema.json +11 -0
- package/artifacts/schema/job.schema.json +106 -0
- package/artifacts/schema/latency-bucket.schema.json +63 -0
- package/artifacts/schema/live-session-response.schema.json +41 -0
- package/artifacts/schema/mail-outcome.schema.json +20 -0
- package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
- package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
- package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
- package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
- package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
- package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
- package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
- package/artifacts/schema/missing-asset-query.schema.json +11 -0
- package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
- package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
- package/artifacts/schema/oauth-token-request.schema.json +47 -0
- package/artifacts/schema/oauth-token-response.schema.json +38 -0
- package/artifacts/schema/org-alerts-query.schema.json +15 -0
- package/artifacts/schema/org-event-dropped.schema.json +26 -0
- package/artifacts/schema/org-event-replay.schema.json +97 -0
- package/artifacts/schema/org-event-subscribe.schema.json +14 -0
- package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
- package/artifacts/schema/org-event.schema.json +75 -0
- package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
- package/artifacts/schema/org-health-query.schema.json +13 -0
- package/artifacts/schema/org-latency-query.schema.json +42 -0
- package/artifacts/schema/org-latency-response.schema.json +124 -0
- package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
- package/artifacts/schema/org-quota-usage.schema.json +102 -0
- package/artifacts/schema/org-quotas.schema.json +51 -0
- package/artifacts/schema/org-usage-query.schema.json +19 -0
- package/artifacts/schema/org-usage-response.schema.json +77 -0
- package/artifacts/schema/org.schema.json +30 -0
- package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
- package/artifacts/schema/parameter-spec.schema.json +120 -0
- package/artifacts/schema/parameter-violation.schema.json +24 -0
- package/artifacts/schema/password-change-request.schema.json +21 -0
- package/artifacts/schema/password-reset-confirm.schema.json +19 -0
- package/artifacts/schema/password-reset-request.schema.json +14 -0
- package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
- package/artifacts/schema/patch-app-user-request.schema.json +34 -0
- package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
- package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
- package/artifacts/schema/patch-org-request.schema.json +15 -0
- package/artifacts/schema/patch-org-response.schema.json +40 -0
- package/artifacts/schema/patch-robot-request.schema.json +15 -0
- package/artifacts/schema/patch-robot-response.schema.json +40 -0
- package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
- package/artifacts/schema/pending-team-invite.schema.json +39 -0
- package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
- package/artifacts/schema/publish-config-response.schema.json +21 -0
- package/artifacts/schema/publish-request.schema.json +17 -0
- package/artifacts/schema/publisher-config.schema.json +285 -0
- package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
- package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
- package/artifacts/schema/put-config-draft-request.schema.json +13 -0
- package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
- package/artifacts/schema/put-robot-details-request.schema.json +41 -0
- package/artifacts/schema/put-robot-details-response.schema.json +43 -0
- package/artifacts/schema/rate-limit-details.schema.json +15 -0
- package/artifacts/schema/refresh-request.schema.json +13 -0
- package/artifacts/schema/release-live-query.schema.json +13 -0
- package/artifacts/schema/rename-slug-request.schema.json +23 -0
- package/artifacts/schema/rename-slug-response.schema.json +24 -0
- package/artifacts/schema/resource-health-event.schema.json +72 -0
- package/artifacts/schema/resource-health-list-response.schema.json +80 -0
- package/artifacts/schema/resource-health-state.schema.json +68 -0
- package/artifacts/schema/robot-config-doc.schema.json +1616 -0
- package/artifacts/schema/robot-delete-query.schema.json +12 -0
- package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
- package/artifacts/schema/robot-detail-response.schema.json +262 -0
- package/artifacts/schema/robot-details-doc.schema.json +33 -0
- package/artifacts/schema/robot-jobs-response.schema.json +119 -0
- package/artifacts/schema/robot-latency-series.schema.json +81 -0
- package/artifacts/schema/robot-list-item.schema.json +94 -0
- package/artifacts/schema/robot-list-response.schema.json +106 -0
- package/artifacts/schema/robot.schema.json +30 -0
- package/artifacts/schema/role-list-response.schema.json +48 -0
- package/artifacts/schema/role-permissions.schema.json +61 -0
- package/artifacts/schema/role.schema.json +35 -0
- package/artifacts/schema/ros-graph.schema.json +99 -0
- package/artifacts/schema/server-key-list-response.schema.json +64 -0
- package/artifacts/schema/server-key.schema.json +51 -0
- package/artifacts/schema/service-call-response.schema.json +13 -0
- package/artifacts/schema/service-config.schema.json +198 -0
- package/artifacts/schema/session-tokens.schema.json +28 -0
- package/artifacts/schema/sign-up-request.schema.json +26 -0
- package/artifacts/schema/sign-up-response.schema.json +127 -0
- package/artifacts/schema/slug-usage-response.schema.json +32 -0
- package/artifacts/schema/snapshot-header.schema.json +44 -0
- package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
- package/artifacts/schema/subscribe-error.schema.json +31 -0
- package/artifacts/schema/team-invite.schema.json +57 -0
- package/artifacts/schema/tier-change-request.schema.json +17 -0
- package/artifacts/schema/type-definition.schema.json +144 -0
- package/artifacts/schema/types-response.schema.json +156 -0
- package/artifacts/schema/update-app-request.schema.json +32 -0
- package/artifacts/schema/urdf-completeness.schema.json +50 -0
- package/artifacts/schema/validation-issue.schema.json +43 -0
- package/artifacts/schema/waitlist-request.schema.json +15 -0
- package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
- package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
- package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
- package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
- package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
- package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
- package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
- package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
- package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
- package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
- package/dist/alerts.d.ts +255 -0
- package/dist/alerts.js +193 -0
- package/dist/app-users.d.ts +606 -0
- package/dist/app-users.js +696 -0
- package/dist/apps.d.ts +175 -0
- package/dist/apps.js +267 -0
- package/dist/assets.d.ts +434 -0
- package/dist/assets.js +546 -0
- package/dist/audit.d.ts +129 -0
- package/dist/audit.js +238 -0
- package/dist/client-auth.d.ts +409 -0
- package/dist/client-auth.js +487 -0
- package/dist/common.d.ts +186 -0
- package/dist/common.js +199 -0
- package/dist/config-issues.d.ts +175 -0
- package/dist/config-issues.js +339 -0
- package/dist/config.d.ts +862 -0
- package/dist/config.js +1988 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +786 -0
- package/dist/identity.d.ts +549 -0
- package/dist/identity.js +503 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/introspection.d.ts +99 -0
- package/dist/introspection.js +97 -0
- package/dist/jobs.d.ts +334 -0
- package/dist/jobs.js +345 -0
- package/dist/mcp.d.ts +239 -0
- package/dist/mcp.js +153 -0
- package/dist/oauth.d.ts +344 -0
- package/dist/oauth.js +488 -0
- package/dist/protocol.d.ts +781 -0
- package/dist/protocol.js +715 -0
- package/dist/realtime.d.ts +494 -0
- package/dist/realtime.js +512 -0
- package/dist/rest.d.ts +1989 -0
- package/dist/rest.js +1963 -0
- package/dist/routes.d.ts +94 -0
- package/dist/routes.js +2298 -0
- package/package.json +61 -0
package/dist/realtime.js
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { RESOURCE_HEALTH_STATES } from './rest.js';
|
|
4
|
+
import { MAX_PATIENCE_MS, MIN_PATIENCE_MS } from './protocol.js';
|
|
5
|
+
import { slug } from './common.js';
|
|
6
|
+
import { clientIdentity } from './client-auth.js';
|
|
7
|
+
import { job } from './jobs.js';
|
|
8
|
+
/**
|
|
9
|
+
* Client realtime protocol (spec §11.1): WebSocket subscriptions on
|
|
10
|
+
* datapoints. W1 scope: subscribe/unsubscribe plus the datapoint event
|
|
11
|
+
* stream; command parity arrives in W4.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The first frame a client sends after the socket opens (W3, spec §3.4).
|
|
15
|
+
*
|
|
16
|
+
* A browser cannot set an `Authorization` header on a WebSocket handshake,
|
|
17
|
+
* and a token in the query string would outlive the request in server,
|
|
18
|
+
* proxy and browser-history logs. So `/realtime` authenticates the way
|
|
19
|
+
* `/bridge` already does: with a frame. `token` is the same bearer value
|
|
20
|
+
* REST takes — a user's JWT or a server key (`flk_…`), told apart by prefix.
|
|
21
|
+
*
|
|
22
|
+
* Any `subscribe` before `auth_ok` is refused, and a socket that sends no
|
|
23
|
+
* auth frame in time is closed with **4002**, the code the bridge socket
|
|
24
|
+
* already uses for a missing hello.
|
|
25
|
+
*/
|
|
26
|
+
export const clientAuth = z.object({
|
|
27
|
+
type: z.literal('auth'),
|
|
28
|
+
token: z.string().min(1),
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Who the socket turned out to belong to. Carrying the identity here means a
|
|
32
|
+
* client never has to decode a JWT to render its own session — decoding a
|
|
33
|
+
* token client-side is how apps end up trusting claims nobody verified.
|
|
34
|
+
*/
|
|
35
|
+
export const authOk = z.object({
|
|
36
|
+
type: z.literal('auth_ok'),
|
|
37
|
+
identity: clientIdentity,
|
|
38
|
+
});
|
|
39
|
+
/** Refusal, followed by close 1008 — same shape as the bridge's hello_error. */
|
|
40
|
+
export const authError = z.object({
|
|
41
|
+
type: z.literal('auth_error'),
|
|
42
|
+
code: z.string().min(1),
|
|
43
|
+
message: z.string().min(1),
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Command parity (spec §11.1): everything REST can do — invoke an action,
|
|
47
|
+
* call a service, publish, cancel — also travels over this socket.
|
|
48
|
+
*
|
|
49
|
+
* **Every command carries a `request_id` and every reply echoes it.** A
|
|
50
|
+
* subscribe that gets dropped is self-healing: the client resubscribes on
|
|
51
|
+
* reconnect and nothing was promised. A *command* that gets dropped is an
|
|
52
|
+
* instruction someone believes they issued and no one will ever run — on a
|
|
53
|
+
* machine that may be moving. Correlation is what makes the difference
|
|
54
|
+
* observable instead of silent.
|
|
55
|
+
*/
|
|
56
|
+
export const clientInvoke = z.object({
|
|
57
|
+
type: z.literal('invoke'),
|
|
58
|
+
request_id: z.string().min(1).max(64),
|
|
59
|
+
robot_id: z.uuid(),
|
|
60
|
+
slug,
|
|
61
|
+
/** Parameters by field path, validated against the config's rules (§4.4). */
|
|
62
|
+
params: z.record(z.string(), z.unknown()),
|
|
63
|
+
/**
|
|
64
|
+
* How long this one call is worth waiting for (W6b) — the same field,
|
|
65
|
+
* meaning and cap as `invokeRequest.patience_ms`; absent means
|
|
66
|
+
* `DEFAULT_PATIENCE_MS`.
|
|
67
|
+
*
|
|
68
|
+
* It is here because **§11.1 parity is a rule, not a preference**: what REST
|
|
69
|
+
* can do travels over this socket. The first version of this delta gave
|
|
70
|
+
* `patience_ms` to the REST body only — and the SDK invokes exclusively over
|
|
71
|
+
* the realtime channel, so the field would have been unreachable for every
|
|
72
|
+
* SDK caller while appearing in the documentation. W6a shipped four SDK
|
|
73
|
+
* methods no SDK caller could invoke; this is the same defect caught before
|
|
74
|
+
* it shipped, by the SDK owner rather than by a reviewer.
|
|
75
|
+
*/
|
|
76
|
+
patience_ms: z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS).optional(),
|
|
77
|
+
});
|
|
78
|
+
export const clientCancel = z.object({
|
|
79
|
+
type: z.literal('cancel'),
|
|
80
|
+
request_id: z.string().min(1).max(64),
|
|
81
|
+
robot_id: z.uuid(),
|
|
82
|
+
/** Which slug — required, and the only address a cancel had until W6b. */
|
|
83
|
+
slug,
|
|
84
|
+
/**
|
|
85
|
+
* Which job on that slug (W6b), or `null` for *whatever is running there*.
|
|
86
|
+
*
|
|
87
|
+
* The two are different requests and both are legitimate. An operator
|
|
88
|
+
* hitting a stop button means the second: stop the machine, whatever it is
|
|
89
|
+
* doing. A client cancelling the job it started means the first — and until
|
|
90
|
+
* this field existed it could not say so, so a cancel that arrived just
|
|
91
|
+
* after its own job ended stopped the next caller's job instead. Same slug,
|
|
92
|
+
* same wire frame, entirely different machine behaviour, and nothing in the
|
|
93
|
+
* protocol able to tell them apart.
|
|
94
|
+
*
|
|
95
|
+
* A named id that is not running answers `not_found` rather than falling
|
|
96
|
+
* back to the slug. Falling back would be the platform deciding that the
|
|
97
|
+
* caller did not really mean the id they typed.
|
|
98
|
+
*/
|
|
99
|
+
job_id: z.uuid().nullable(),
|
|
100
|
+
});
|
|
101
|
+
export const clientPublish = z.object({
|
|
102
|
+
type: z.literal('publish'),
|
|
103
|
+
request_id: z.string().min(1).max(64),
|
|
104
|
+
robot_id: z.uuid(),
|
|
105
|
+
slug,
|
|
106
|
+
message: z.record(z.string(), z.unknown()),
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* The reply to exactly one command. `ok:false` carries the stable code —
|
|
110
|
+
* `busy`, `robot_offline`, `parameter_invalid`, `forbidden` — so a caller
|
|
111
|
+
* branches without parsing prose.
|
|
112
|
+
*
|
|
113
|
+
* **`request_id` is the only correlation, and the order these arrive in is
|
|
114
|
+
* not promised.** Commands sent on one socket reach the robot in the order
|
|
115
|
+
* they were sent — that ordering is guaranteed and is the point of the
|
|
116
|
+
* command chain — but their *replies* may arrive in any order, and a client
|
|
117
|
+
* that pairs them up by arrival order will attribute an outcome to the wrong
|
|
118
|
+
* command.
|
|
119
|
+
*
|
|
120
|
+
* This is not theoretical and not jitter. The ordering chain is released once
|
|
121
|
+
* a command has reached the bridge, deliberately, so that the next command —
|
|
122
|
+
* a stop, say — is never held up behind bookkeeping. Work that follows the
|
|
123
|
+
* send therefore runs unordered: a publish that *acquires* a slug writes an
|
|
124
|
+
* audit record before answering, while an immediately following publish by
|
|
125
|
+
* the now-current holder has nothing to write and answers at once. Its reply
|
|
126
|
+
* overtakes. Measured in W5: exactly one reversal in fifty-six zero-gap
|
|
127
|
+
* bursts, which is the signature of that cause — it can happen only once per
|
|
128
|
+
* identity and slug — and not of a race.
|
|
129
|
+
*
|
|
130
|
+
* Serialising the replies would mean putting that bookkeeping in front of
|
|
131
|
+
* every following command, including the stop. The ordering that matters is
|
|
132
|
+
* the one on the wire to the robot, and it is kept.
|
|
133
|
+
*/
|
|
134
|
+
export const commandResult = z.object({
|
|
135
|
+
type: z.literal('command_result'),
|
|
136
|
+
request_id: z.string().min(1).max(64),
|
|
137
|
+
ok: z.boolean(),
|
|
138
|
+
/**
|
|
139
|
+
* The job this reply is *about* — which is not always the caller's own.
|
|
140
|
+
*
|
|
141
|
+
* - `ok:true` on an invoke or a call: the job that was just created.
|
|
142
|
+
* - `ok:true` on a cancel: the job the cancel was sent to.
|
|
143
|
+
* - `ok:false, code:'busy'`: **the job that is already running** — the
|
|
144
|
+
* caller has none. This is the §11.3 "inkl. Information, was läuft", and
|
|
145
|
+
* it is the whole reason a busy refusal is useful: the caller learns
|
|
146
|
+
* whether to wait or to give up (see `busyDetails`).
|
|
147
|
+
* - any other refusal: `null`.
|
|
148
|
+
*/
|
|
149
|
+
job: job.nullable(),
|
|
150
|
+
/**
|
|
151
|
+
* The **kind** of the slug this command addressed, when the slug resolved.
|
|
152
|
+
*
|
|
153
|
+
* Invoke and call share a route on purpose — both create the job on the
|
|
154
|
+
* slug — but a *client* library distinguishes them: `services.call` waits
|
|
155
|
+
* for a terminal state, `actions.invoke` does not. Without the kind coming
|
|
156
|
+
* back, calling a service helper on an action slug **starts the real action
|
|
157
|
+
* on the robot** and then blames the robot for not finishing, half a minute
|
|
158
|
+
* later. Returning the kind lets a client refuse its own mistake at once,
|
|
159
|
+
* instead of reporting it as the machine's.
|
|
160
|
+
*/
|
|
161
|
+
kind: z.enum(['datapoint', 'action', 'service', 'publisher', 'camera']).nullable(),
|
|
162
|
+
code: z.string().nullable(),
|
|
163
|
+
message: z.string().nullable(),
|
|
164
|
+
/**
|
|
165
|
+
* The same payload the REST envelope carries in `apiError.details` — for
|
|
166
|
+
* `parameter_invalid`, a `parameterInvalidDetails`.
|
|
167
|
+
*
|
|
168
|
+
* Added because it was missing, and its absence quietly broke §11.1: this
|
|
169
|
+
* socket is supposed to do *everything* REST can do, but a
|
|
170
|
+
* `parameter_invalid` arriving here had nowhere to put its violations, so
|
|
171
|
+
* the same refusal was actionable over HTTP and opaque over the socket.
|
|
172
|
+
* A client cannot bind an error to the input that caused it from a code
|
|
173
|
+
* alone — which is the entire point of the flat parameter shape.
|
|
174
|
+
*/
|
|
175
|
+
details: z.unknown().optional(),
|
|
176
|
+
});
|
|
177
|
+
/**
|
|
178
|
+
* A well-formed frame this server does not understand. The socket **stays
|
|
179
|
+
* open** — closing it would mean a newer client against an older cloud
|
|
180
|
+
* reconnects, resends, and takes every unrelated subscription down with it
|
|
181
|
+
* on every attempt. Close 1008 is reserved for frames that are not parseable
|
|
182
|
+
* JSON objects at all.
|
|
183
|
+
*/
|
|
184
|
+
export const errorFrame = z.object({
|
|
185
|
+
type: z.literal('error'),
|
|
186
|
+
code: z.string().min(1),
|
|
187
|
+
message: z.string().min(1),
|
|
188
|
+
});
|
|
189
|
+
/**
|
|
190
|
+
* Subscribe to a slug's stream (spec §11.3: **state is observed by slug**).
|
|
191
|
+
*
|
|
192
|
+
* Which kinds are subscribable, and why it is not a matter of taste:
|
|
193
|
+
*
|
|
194
|
+
* - **datapoint** — its values.
|
|
195
|
+
* - **action** — its `jobEvent`s.
|
|
196
|
+
* - **service** — its `jobEvent`s too. A service call mints a job like an
|
|
197
|
+
* action does; the only difference is that the caller usually gets the
|
|
198
|
+
* result inline. But when they do *not* — a socket that died before the
|
|
199
|
+
* reply, answered with `command_outcome_unknown` — the documented recovery
|
|
200
|
+
* is to observe the slug. Refusing that leaves the caller with an error
|
|
201
|
+
* that names a remedy the platform does not offer.
|
|
202
|
+
* - **publisher** — not subscribable: there is no job and no stream. It must
|
|
203
|
+
* still be refused *honestly*, with a code saying so, and never as though
|
|
204
|
+
* the slug did not exist. A caller who was granted a slug is entitled to
|
|
205
|
+
* be told the truth about it.
|
|
206
|
+
*/
|
|
207
|
+
export const clientSubscribe = z.object({
|
|
208
|
+
type: z.literal('subscribe'),
|
|
209
|
+
robot_id: z.uuid(),
|
|
210
|
+
slug,
|
|
211
|
+
/**
|
|
212
|
+
* What the subscriber expects, and how it wants it (W5).
|
|
213
|
+
*
|
|
214
|
+
* `kind` lets the server answer **`wrong_kind`** instead of accepting a
|
|
215
|
+
* subscribe the client will then filter to silence — and silence is
|
|
216
|
+
* indistinguishable from an idle slug, so it tells a developer nothing.
|
|
217
|
+
* Optional, so an older client that omits it keeps today's behaviour.
|
|
218
|
+
*
|
|
219
|
+
* `options` is where a camera says what it wants; a datapoint needs none.
|
|
220
|
+
* It exists now rather than later because adding a field to a frame three
|
|
221
|
+
* repos parse is cheap once and expensive twice.
|
|
222
|
+
*/
|
|
223
|
+
/**
|
|
224
|
+
* `publisher` is here even though a publisher is not subscribable: a client
|
|
225
|
+
* that models the five grantable kinds and honestly names one gets the
|
|
226
|
+
* informative `not_subscribable` the cloud already computes, instead of a
|
|
227
|
+
* `validation_error` reciting an enum. Refusing the *word* rather than the
|
|
228
|
+
* request was the same mistake as the silent wrong-verb subscribe this
|
|
229
|
+
* field was added to fix.
|
|
230
|
+
*/
|
|
231
|
+
kind: z.enum(['datapoint', 'action', 'service', 'publisher', 'camera']).optional(),
|
|
232
|
+
options: z.record(z.string(), z.unknown()).optional(),
|
|
233
|
+
});
|
|
234
|
+
export const clientUnsubscribe = z.object({
|
|
235
|
+
type: z.literal('unsubscribe'),
|
|
236
|
+
robot_id: z.uuid(),
|
|
237
|
+
slug,
|
|
238
|
+
});
|
|
239
|
+
/**
|
|
240
|
+
* Refusal of a subscribe, addressed by the (robot_id, slug) it refers to.
|
|
241
|
+
* Codes follow the §11.5 error culture: stable code + human message.
|
|
242
|
+
* robot_id/slug are plain strings ECHOING what the client sent — the frame
|
|
243
|
+
* must be constructible precisely when those values are malformed, so that
|
|
244
|
+
* a bad robot_id or slug gets a diagnosis instead of a dead socket.
|
|
245
|
+
*/
|
|
246
|
+
export const subscribeError = z.object({
|
|
247
|
+
type: z.literal('subscribe_error'),
|
|
248
|
+
robot_id: z.string(),
|
|
249
|
+
slug: z.string(),
|
|
250
|
+
code: z.string().min(1),
|
|
251
|
+
message: z.string().min(1),
|
|
252
|
+
});
|
|
253
|
+
/**
|
|
254
|
+
* One datapoint sample pushed to a subscriber. The current value arrives
|
|
255
|
+
* immediately on subscribe, then every change. `timestamp_ms` semantics as
|
|
256
|
+
* in `datapointValue` (capture time; cloud-observed for `bridge_state`).
|
|
257
|
+
*/
|
|
258
|
+
export const datapointEvent = z.object({
|
|
259
|
+
type: z.literal('datapoint'),
|
|
260
|
+
robot_id: z.uuid(),
|
|
261
|
+
slug,
|
|
262
|
+
value: z.unknown(),
|
|
263
|
+
timestamp_ms: z.number().int().nonnegative(),
|
|
264
|
+
});
|
|
265
|
+
/**
|
|
266
|
+
* A change in the health of something the developer configured (W6a).
|
|
267
|
+
*
|
|
268
|
+
* The push half of `resourceHealthState`; the REST list is the snapshot half,
|
|
269
|
+
* and neither is useful alone — a page that loads after the change would see
|
|
270
|
+
* nothing, and a page that never reloads would never learn.
|
|
271
|
+
*
|
|
272
|
+
* Delivered on the **developer** socket and scoped to the org, not to a
|
|
273
|
+
* subscription: the whole point is to reach somebody who is *not* currently
|
|
274
|
+
* looking at the thing that broke.
|
|
275
|
+
*/
|
|
276
|
+
/**
|
|
277
|
+
* Why a live camera session ended (W9a).
|
|
278
|
+
*
|
|
279
|
+
* **The reason travels WITH the ending, and that is the whole point of this
|
|
280
|
+
* enum existing rather than a state somebody reads afterwards.** W6a put a
|
|
281
|
+
* `cause` on the wire, the console named the real reason, and the lead
|
|
282
|
+
* observed the gate step and closed it — and the review then found it still
|
|
283
|
+
* could not tell, for a different reason: `stopped_by_config_change` is
|
|
284
|
+
* **sticky**, nothing moves a camera out of it, and `LiveCameraRow` read that
|
|
285
|
+
* *current* state at the moment a stream ended. A config change at 10:00 and
|
|
286
|
+
* an unrelated release at 10:30 therefore reported the same cause (DEF-070).
|
|
287
|
+
*
|
|
288
|
+
* A state read after the fact answers "what is true now". A viewer needs
|
|
289
|
+
* "what happened to my session", and only an event carries that.
|
|
290
|
+
*/
|
|
291
|
+
export const liveSessionEndReason = z.enum([
|
|
292
|
+
/** Another holder of this camera released it — another tab, or another client. */
|
|
293
|
+
'released_by_peer',
|
|
294
|
+
/** The robot's configuration was published and this camera changed with it. */
|
|
295
|
+
'config_changed',
|
|
296
|
+
/** The robot said it could not publish. `detail` carries its own words. */
|
|
297
|
+
'publish_failed',
|
|
298
|
+
/** The bridge stopped answering. */
|
|
299
|
+
'robot_offline',
|
|
300
|
+
/** The grant this session was minted under was withdrawn. */
|
|
301
|
+
'revoked',
|
|
302
|
+
/** The session's own lifetime ran out. */
|
|
303
|
+
'expired',
|
|
304
|
+
/** The robot was deleted out from under the session. */
|
|
305
|
+
'robot_deleted',
|
|
306
|
+
/**
|
|
307
|
+
* The cloud ended it and cannot say which of the above applied. **Kept
|
|
308
|
+
* deliberately**: a channel that cannot say "I do not know" will say
|
|
309
|
+
* something false instead, and this project has paid for that four times in
|
|
310
|
+
* the camera path alone.
|
|
311
|
+
*/
|
|
312
|
+
'unknown',
|
|
313
|
+
]);
|
|
314
|
+
/**
|
|
315
|
+
* A live camera session ended, told to the **client that holds it** (W9a).
|
|
316
|
+
*
|
|
317
|
+
* This is the channel `DEF-051`, `DEF-052`, `DEF-053` and `DEF-070` each
|
|
318
|
+
* described from a different direction across four waves. Until now the only
|
|
319
|
+
* vehicle was `camera_state`, which the cloud stores in `publishState` and
|
|
320
|
+
* reads in exactly one place — refusing a *later* joiner — so reporting a
|
|
321
|
+
* failure would have written to a dead end.
|
|
322
|
+
*
|
|
323
|
+
* Unlike `resourceHealthEvent`, which is developer-only and org-scoped, this
|
|
324
|
+
* one is addressed to the **holder of the session**: it names `session_id`
|
|
325
|
+
* (W6b gave `liveSessionResponse` one precisely so a session could be
|
|
326
|
+
* addressed) and is delivered only to the identity that session was minted
|
|
327
|
+
* for. A developer watching the same robot learns about the *resource* health;
|
|
328
|
+
* the viewer learns about *their own session*. Two questions, two channels,
|
|
329
|
+
* on purpose.
|
|
330
|
+
*/
|
|
331
|
+
export const liveSessionEvent = z.object({
|
|
332
|
+
type: z.literal('live_session'),
|
|
333
|
+
robot_id: z.uuid(),
|
|
334
|
+
slug,
|
|
335
|
+
session_id: z.uuid(),
|
|
336
|
+
state: z.literal('ended'),
|
|
337
|
+
reason: liveSessionEndReason,
|
|
338
|
+
/**
|
|
339
|
+
* **Classified text the cloud produced, never text the robot sent.**
|
|
340
|
+
*
|
|
341
|
+
* An earlier draft of this comment said *"the robot's own words when it has
|
|
342
|
+
* any"*, which reads as permission to pass `bridgeCameraState.error.message`
|
|
343
|
+
* straight through. Nothing sanitises that field, and this codebase has a
|
|
344
|
+
* documented incident of a password reaching a developer surface through
|
|
345
|
+
* exactly that route — `camera-health.ts`'s fixed-string `REASON` discipline
|
|
346
|
+
* exists because of it. Nimbus-W9a stopped at the sentence and asked rather
|
|
347
|
+
* than taking the permission it appeared to give (2026-08-19).
|
|
348
|
+
*
|
|
349
|
+
* So: `null` unless the cloud itself has something classified to say. If a
|
|
350
|
+
* developer needs the robot's own diagnosis later, it arrives as a mapped
|
|
351
|
+
* code with fixed text, the way camera health already does it — not as
|
|
352
|
+
* forwarded foreign text on a channel a client reads.
|
|
353
|
+
*/
|
|
354
|
+
detail: z.string().max(200).nullable(),
|
|
355
|
+
/** When it ended — not when this frame was sent. Same reasoning as `changed_at_ms`. */
|
|
356
|
+
ended_at_ms: z.number().int().nonnegative(),
|
|
357
|
+
});
|
|
358
|
+
/**
|
|
359
|
+
* A resource's health entry was **withdrawn** (W9a, DEF-071).
|
|
360
|
+
*
|
|
361
|
+
* The store's `invalidate()` deliberately emitted nothing, reasoning that
|
|
362
|
+
* "withdrawing a claim nobody can currently stand behind is not new
|
|
363
|
+
* information — the next `GET` already reflects it." That holds for a page
|
|
364
|
+
* that loads later. **It is false for a page that is already open, because
|
|
365
|
+
* there is no next `GET`:** `ensureSnapshot()` runs on `acquire` and nowhere
|
|
366
|
+
* else, there is no interval, and the event handler only ever *writes* keys.
|
|
367
|
+
* A camera retargeted to a source that never reports — which is the case the
|
|
368
|
+
* clearing exists for — leaves an open tab showing the old value indefinitely.
|
|
369
|
+
*
|
|
370
|
+
* **A separate event type rather than a nullable `state` on the existing
|
|
371
|
+
* one**, so a consumer's `switch` has to name it. A nullable field invites
|
|
372
|
+
* `if (state)` and fails silently when somebody forgets; an unhandled variant
|
|
373
|
+
* fails `tsc`, which is the difference between a rule and a mechanism.
|
|
374
|
+
*/
|
|
375
|
+
export const resourceHealthCleared = z.object({
|
|
376
|
+
type: z.literal('resource_health_cleared'),
|
|
377
|
+
robot_id: z.uuid(),
|
|
378
|
+
kind: z.enum(['camera']),
|
|
379
|
+
ref: z.string().min(1).max(64),
|
|
380
|
+
facet: z.enum(['source', 'publish']),
|
|
381
|
+
cleared_at_ms: z.number().int().nonnegative(),
|
|
382
|
+
});
|
|
383
|
+
export const resourceHealthEvent = z.object({
|
|
384
|
+
type: z.literal('resource_health'),
|
|
385
|
+
robot_id: z.uuid(),
|
|
386
|
+
kind: z.enum(['camera']),
|
|
387
|
+
ref: z.string().min(1).max(64),
|
|
388
|
+
/** Which of the two questions this entry answers — see `resourceHealthState.facet`. */
|
|
389
|
+
facet: z.enum(['source', 'publish']),
|
|
390
|
+
state: z.enum(RESOURCE_HEALTH_STATES),
|
|
391
|
+
reason: z.string().max(200).nullable(),
|
|
392
|
+
changed_at_ms: z.number().int().nonnegative(),
|
|
393
|
+
});
|
|
394
|
+
/**
|
|
395
|
+
* One line of the developer console's activity panel (spec
|
|
396
|
+
* `2026-08-20-org-event-stream`).
|
|
397
|
+
*
|
|
398
|
+
* **This is an activity log for humans, not a complete feed.** It is throttled
|
|
399
|
+
* and sampled. `orgEventDropped` still reports a drop on the wire, but since
|
|
400
|
+
* FL-001 **no Fleetless surface renders it** — the console's gap banner was
|
|
401
|
+
* removed on request, and nothing replaced it. A reader of this stream
|
|
402
|
+
* therefore cannot tell a complete window from a sampled one, and this
|
|
403
|
+
* comment says so rather than implying a notice that exists only in the
|
|
404
|
+
* protocol. Anything that needs completeness reads the audit log or the
|
|
405
|
+
* job-run history, both durable, both 90 days.
|
|
406
|
+
*/
|
|
407
|
+
export const ORG_EVENT_SAMPLE_INTERVAL_MS = 1_000;
|
|
408
|
+
/** The backstop above the per-slug cap: a fleet larger than the panel could serve anyway. */
|
|
409
|
+
export const ORG_EVENT_ORG_CEILING_PER_SECOND = 50;
|
|
410
|
+
/** Roughly 25 screens of scrollback. */
|
|
411
|
+
export const ORG_EVENT_BUFFER_SIZE = 200;
|
|
412
|
+
/** An org's buffer is dropped after this long without an event, so memory follows active orgs rather than all of them. */
|
|
413
|
+
export const ORG_EVENT_BUFFER_IDLE_MS = 3_600_000;
|
|
414
|
+
/** A log line, not a payload: a datapoint value is `unknown` and a LaserScan is megabytes. */
|
|
415
|
+
export const ORG_EVENT_DETAIL_MAX_BYTES = 4_096;
|
|
416
|
+
/**
|
|
417
|
+
* `'alert'` — a transition of a datapoint alert (`ok ⇄ firing`, spec
|
|
418
|
+
* `2026-08-28-alerts-and-datapoint-modal-design` D2). A firing event carries
|
|
419
|
+
* the alert's own `severity`; a resolved event is always `info` — resolving
|
|
420
|
+
* is good news regardless of how bad the firing was.
|
|
421
|
+
*
|
|
422
|
+
* `'datapoint'` — since FL-001, **no producer emits this kind**: the
|
|
423
|
+
* datapoint producer was made a deliberate no-op (Task 5/6, this stream's own
|
|
424
|
+
* per-slug sampling made it redundant with what the datapoint history route
|
|
425
|
+
* already serves). The member stays in the enum rather than being removed,
|
|
426
|
+
* because a reader may still hold a pre-deploy frame of this kind sitting in
|
|
427
|
+
* a buffer (a reconnect replay, a client that hasn't refreshed) and must be
|
|
428
|
+
* able to parse it rather than fail closed on an old, valid value. Same shape
|
|
429
|
+
* as the correction on `ORG_EVENT_SAMPLE_INTERVAL_MS`'s comment just above:
|
|
430
|
+
* name what the wire no longer does instead of leaving a value the cloud can
|
|
431
|
+
* never send undocumented.
|
|
432
|
+
*/
|
|
433
|
+
export const orgEventKind = z.enum(['datapoint', 'health', 'job', 'bridge', 'audit', 'alert']);
|
|
434
|
+
/**
|
|
435
|
+
* Assigned by the **producer**, never derived by the reader. The console's
|
|
436
|
+
* `errors` filter cuts across all five kinds, and only the source knows whether
|
|
437
|
+
* an `auth_failed` is bad. A reader guessing from `detail` guesses differently
|
|
438
|
+
* for each source.
|
|
439
|
+
*/
|
|
440
|
+
export const orgEventSeverity = z.enum(['info', 'warning', 'error']);
|
|
441
|
+
export const orgEvent = z
|
|
442
|
+
.object({
|
|
443
|
+
type: z.literal('org_event'),
|
|
444
|
+
/**
|
|
445
|
+
* **Per org, per process.** Like `job.seq` and unlike `job_runs.seq`, which
|
|
446
|
+
* is a postgres `bigserial` and durable. All three say which they are,
|
|
447
|
+
* because anyone who confuses them will confuse them in both directions.
|
|
448
|
+
*/
|
|
449
|
+
seq: z.number().int().positive(),
|
|
450
|
+
at: z.iso.datetime(),
|
|
451
|
+
kind: orgEventKind,
|
|
452
|
+
severity: orgEventSeverity,
|
|
453
|
+
/** `null` for an org-level event — an invitation, a quota change — which belongs to no robot. */
|
|
454
|
+
robot_id: z.uuid().nullable(),
|
|
455
|
+
/** What the line is about: a slug, a camera, an actor's email. */
|
|
456
|
+
subject: z.string().min(1).max(200),
|
|
457
|
+
/**
|
|
458
|
+
* Kind-specific, and **capped at `ORG_EVENT_DETAIL_MAX_BYTES`** — above it
|
|
459
|
+
* the producer substitutes `{ omitted: 'too_large', bytes }`. Truncated,
|
|
460
|
+
* and saying so.
|
|
461
|
+
*
|
|
462
|
+
* Never a pre-formatted line: the reader decides language, number format
|
|
463
|
+
* and truncation, so changing how a line reads is not a cloud deploy.
|
|
464
|
+
*/
|
|
465
|
+
detail: z.unknown().nullable(),
|
|
466
|
+
})
|
|
467
|
+
.strict();
|
|
468
|
+
/** Sent by a developer's socket to start the stream. Answered by `orgEventReplay`, then live `orgEvent`s. */
|
|
469
|
+
export const orgEventSubscribe = z.object({ type: z.literal('org_event_subscribe') }).strict();
|
|
470
|
+
export const orgEventUnsubscribe = z.object({ type: z.literal('org_event_unsubscribe') }).strict();
|
|
471
|
+
/**
|
|
472
|
+
* What the cloud still remembers, oldest first, sent once before the live
|
|
473
|
+
* stream starts — so the panel is filled on arrival rather than blank until
|
|
474
|
+
* something happens. A blank panel is indistinguishable from a broken one.
|
|
475
|
+
*/
|
|
476
|
+
export const orgEventReplay = z
|
|
477
|
+
.object({
|
|
478
|
+
type: z.literal('org_event_replay'),
|
|
479
|
+
events: z.array(orgEvent).max(ORG_EVENT_BUFFER_SIZE),
|
|
480
|
+
/**
|
|
481
|
+
* **`false` means three different things, on purpose**: the buffer was
|
|
482
|
+
* already full, the cloud restarted, or this org's buffer had expired. All
|
|
483
|
+
* three mean the same thing to a reader — *something is missing above this
|
|
484
|
+
* line* — and a field separating them would claim a distinction nobody
|
|
485
|
+
* would act on differently.
|
|
486
|
+
*/
|
|
487
|
+
complete: z.boolean(),
|
|
488
|
+
})
|
|
489
|
+
.strict();
|
|
490
|
+
/**
|
|
491
|
+
* Events this socket will never see. Two causes, reported alike: the cloud
|
|
492
|
+
* sampled them away, or this socket's send buffer was too far behind. Both mean
|
|
493
|
+
* *there was more than you are being shown*.
|
|
494
|
+
*/
|
|
495
|
+
export const orgEventDropped = z
|
|
496
|
+
.object({
|
|
497
|
+
type: z.literal('org_event_dropped'),
|
|
498
|
+
/**
|
|
499
|
+
* **An epoch instant in milliseconds (`Date.now()`), not a duration.**
|
|
500
|
+
* The moment this socket last reported a drop — or the moment it
|
|
501
|
+
* subscribed, if this is its first such frame. The window the `dropped`
|
|
502
|
+
* count covers is `since_ms` to now, so a reader wanting an age
|
|
503
|
+
* subtracts: `Date.now() - since_ms`. Spelled out because the type
|
|
504
|
+
* admits both readings and the wrong one is silent: a consumer treating
|
|
505
|
+
* it as "milliseconds ago" renders a drop that happened seconds ago as
|
|
506
|
+
* having happened in 1970.
|
|
507
|
+
*/
|
|
508
|
+
since_ms: z.number().int().nonnegative(),
|
|
509
|
+
/** Always at least one — a frame reporting nothing lost is noise on a channel built to be quiet. */
|
|
510
|
+
dropped: z.number().int().positive(),
|
|
511
|
+
})
|
|
512
|
+
.strict();
|