@fleetless/sdk 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/index.cjs +3700 -274
- package/dist/index.d.cts +560 -327
- package/dist/index.d.ts +560 -327
- package/dist/index.js +3700 -274
- package/package.json +3 -2
package/dist/index.d.cts
CHANGED
|
@@ -543,7 +543,7 @@ type ParameterInvalidDetails = z.infer<typeof parameterInvalidDetails>;
|
|
|
543
543
|
* list is the shared vocabulary, not a closed set, so a new refusal never
|
|
544
544
|
* needs a contracts release before it can be reported honestly.
|
|
545
545
|
*/
|
|
546
|
-
declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "invite_expired", "invite_used", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "asset_too_large", "dynamic_registration_disabled", "client_limit_reached", "identity_conflict", "identity_not_provisioned", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "group_not_deletable", "group_in_use", "target_state_conflict", "mcp_access_denied", "signup_closed"];
|
|
546
|
+
declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "invite_expired", "invite_used", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "asset_too_large", "dynamic_registration_disabled", "client_limit_reached", "identity_conflict", "identity_not_provisioned", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "group_not_deletable", "group_in_use", "target_state_conflict", "mcp_access_denied", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml"];
|
|
547
547
|
type ErrorCode = (typeof ERROR_CODES)[number];
|
|
548
548
|
|
|
549
549
|
/**
|
|
@@ -620,7 +620,7 @@ type ConsentRevokeResponse = z.infer<typeof consentRevokeResponse>;
|
|
|
620
620
|
* Codes the SDK produces itself rather than relaying from the server. Kept
|
|
621
621
|
* out of `@fleetless/contracts`' `ERROR_CODES` deliberately — that list is
|
|
622
622
|
* the *wire* vocabulary, every entry something a server may actually send,
|
|
623
|
-
* and none of these are.
|
|
623
|
+
* and none of these are.
|
|
624
624
|
*
|
|
625
625
|
* - `no_session` / `no_websocket`: a client-side refusal *before* a request
|
|
626
626
|
* ever reaches the network (not logged in; no WebSocket implementation
|
|
@@ -628,102 +628,106 @@ type ConsentRevokeResponse = z.infer<typeof consentRevokeResponse>;
|
|
|
628
628
|
* caller can tell "the server refused me" from "the SDK refused before
|
|
629
629
|
* asking" by the code alone.
|
|
630
630
|
* - `unparseable_error`: the opposite direction — a real response *did*
|
|
631
|
-
* arrive, its body just
|
|
632
|
-
* refusal at all, just "we
|
|
633
|
-
* - `command_timeout
|
|
634
|
-
*
|
|
631
|
+
* arrive, its body just was not shaped like the platform's error format.
|
|
632
|
+
* Not a refusal at all, just "we do not know what the server said."
|
|
633
|
+
* - `command_timeout`: a realtime command (`invoke`/`cancel`/`publish`) got
|
|
634
|
+
* no `command_result` within its timeout. The server may still answer
|
|
635
635
|
* later on the same socket — nobody knows — but the caller cannot be made
|
|
636
636
|
* to wait forever for that.
|
|
637
|
-
* - `command_outcome_unknown
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
637
|
+
* - `command_outcome_unknown`: worse than a timeout, and told apart from it
|
|
638
|
+
* on purpose — the realtime connection that carried the command was
|
|
639
|
+
* replaced by a new one (a reconnect) before any reply arrived. A reply
|
|
640
|
+
* can now never come: the server, if it answered at all, answered a
|
|
641
641
|
* socket that no longer exists. The command may or may not have run.
|
|
642
642
|
* Never retried automatically — that could run an action twice — the
|
|
643
643
|
* caller recovers by reading the job (e.g. `actions.subscribe`), since
|
|
644
644
|
* state is observed by slug regardless of which connection asked for it.
|
|
645
|
-
* - `unexpected_response
|
|
645
|
+
* - `unexpected_response`: the server answered `ok:true` but left out
|
|
646
646
|
* something the command is defined to always return (e.g. no `job` on a
|
|
647
647
|
* successful `invoke`) — a contract violation the SDK noticed, not a
|
|
648
648
|
* refusal.
|
|
649
|
-
* - `invalid_option
|
|
650
|
-
*
|
|
651
|
-
* `timeoutMs < patienceMs` on `invoke`/`call`
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
* `
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
* *empty* strings with `!==` is `false`, so without this
|
|
687
|
-
* caller with nothing persisted at all could reach
|
|
688
|
-
* comparison having contributed no defence whatsoever.
|
|
689
|
-
* - `aborted
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* `
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
700
|
-
*
|
|
701
|
-
* - `state_mismatch
|
|
702
|
-
*
|
|
649
|
+
* - `invalid_option`: the caller passed an *SDK-level* argument or option
|
|
650
|
+
* that cannot mean what it looks like it means. Three cases so far:
|
|
651
|
+
* `timeoutMs < patienceMs` on `invoke`/`call` — the SDK would give up
|
|
652
|
+
* locally before the platform's own patience runs out, and report
|
|
653
|
+
* `command_timeout` for a call the platform never actually refused; a
|
|
654
|
+
* non-string, non-null, non-omitted `jobId` on `cancel` — almost always a
|
|
655
|
+
* caller who upgraded past the older `cancel(robotId, slug, options?)`
|
|
656
|
+
* signature and is still passing an options object third; and a
|
|
657
|
+
* `concurrency` on `assets.prepareUrdfScene` that is not a positive
|
|
658
|
+
* integer, which would otherwise fetch nothing and return a scene that
|
|
659
|
+
* renders blank with no error to explain why. All three are refused
|
|
660
|
+
* before any request is sent — as a rejection, since every one of those
|
|
661
|
+
* methods is `async`. A client-side mistake to fix, not something a
|
|
662
|
+
* server response could ever produce, which is why this code belongs here
|
|
663
|
+
* and not in `@fleetless/contracts`' `ERROR_CODES`.
|
|
664
|
+
* - `untrusted_absolute_url`: the SDK refused to fetch an absolute URL
|
|
665
|
+
* whose origin does not match this client's own `apiUrl` — thrown before
|
|
666
|
+
* the request is ever sent, so no `Authorization` header is ever built for
|
|
667
|
+
* it, let alone attached. The one caller that fetches an absolute URL at
|
|
668
|
+
* all is `assets.createMeshLoader`, following a URDF's rewritten mesh
|
|
669
|
+
* URIs — and a URDF is ROS graph input, not first-party data, so an app
|
|
670
|
+
* rendering one must not silently trust wherever it points.
|
|
671
|
+
* `assets.createMeshLoader`'s `onComplete` surfaces this the same way it
|
|
672
|
+
* surfaces a network failure: `(null, err)`.
|
|
673
|
+
* - `no_urdf_synced`: `assets.prepareUrdfScene` looked for a `kind: 'urdf'`
|
|
674
|
+
* row in `assets.list()` and found none. Thrown before any asset fetch,
|
|
675
|
+
* rather than left to surface as a confusing downstream failure from
|
|
676
|
+
* `URDFLoader.parse(undefined)` or similar — the caller's fix is "sync a
|
|
677
|
+
* URDF first", which this error can say directly.
|
|
678
|
+
* - `no_hosted_login_attempt`: `auth.completeHostedLogin()` was called with
|
|
679
|
+
* an empty `expectedState` — nothing was persisted for this attempt. A
|
|
680
|
+
* callback landing in a different tab or window than the one that called
|
|
681
|
+
* `beginHostedLogin`, a restored session, or storage cleared in between
|
|
682
|
+
* all produce exactly this, and none of them is an attack. Told apart
|
|
683
|
+
* from `state_mismatch` on purpose: the two diagnoses have different
|
|
684
|
+
* remedies ("check how you persisted the value" versus "this response
|
|
685
|
+
* belongs to a login you did not start"). It also closes a real gap —
|
|
686
|
+
* comparing two *empty* strings with `!==` is `false`, so without this
|
|
687
|
+
* check first, a caller with nothing persisted at all could reach
|
|
688
|
+
* `state_mismatch`'s comparison having contributed no defence whatsoever.
|
|
689
|
+
* - `aborted`: `assets.prepareUrdfScene()` was given an `AbortSignal` and it
|
|
690
|
+
* fired — either already-aborted before the call started, or mid-flight
|
|
691
|
+
* while a fetch was in progress. Normalized to this one code regardless of
|
|
692
|
+
* which stage the abort landed in, rather than surfacing whatever shape
|
|
693
|
+
* the underlying `fetch()` rejects an aborted request with (a
|
|
694
|
+
* `DOMException` named `AbortError` in a browser, an `Error` named
|
|
695
|
+
* `AbortError` under Node's `fetch` — two different shapes a caller would
|
|
696
|
+
* otherwise have to detect themselves to tell "I cancelled this" from "the
|
|
697
|
+
* network actually failed"). Every partial resource this call had already
|
|
698
|
+
* created (`blob:` URLs) is revoked before this throws — an aborted load
|
|
699
|
+
* must not leak what it fetched before the signal fired, the same
|
|
700
|
+
* guarantee a failed load already had.
|
|
701
|
+
* - `state_mismatch`: `auth.completeHostedLogin()` was called with a `state`
|
|
702
|
+
* that does not match the `expectedState` its own `beginHostedLogin()`
|
|
703
703
|
* returned for this attempt (or with no `state` at all — `beginHostedLogin`
|
|
704
704
|
* always sets one, so a callback carrying none does not look like a reply
|
|
705
705
|
* to a flow this client started). Thrown before `/oauth/token` is ever
|
|
706
|
-
* called
|
|
706
|
+
* called: RFC 6749 section 10.12's whole point is that a caller must not
|
|
707
707
|
* complete an authorization response it did not itself request, so this
|
|
708
708
|
* check happens client-side, first, rather than being left to the server
|
|
709
|
-
* to catch
|
|
710
|
-
* attempted for a flow this client never started
|
|
709
|
+
* to catch — by which point a code exchange would already have been
|
|
710
|
+
* attempted for a flow this client never started.
|
|
711
711
|
*/
|
|
712
712
|
declare const SDK_ERROR_CODES: readonly ["no_session", "no_websocket", "unparseable_error", "command_timeout", "command_outcome_unknown", "unexpected_response", "invalid_option", "untrusted_absolute_url", "state_mismatch", "no_hosted_login_attempt", "no_urdf_synced", "aborted"];
|
|
713
|
+
/**
|
|
714
|
+
* The union of `SDK_ERROR_CODES` — the SDK's own client-side error
|
|
715
|
+
* vocabulary. A `FleetlessError` whose `code` is one of these was raised by
|
|
716
|
+
* this SDK rather than relayed from the server.
|
|
717
|
+
*/
|
|
713
718
|
type SdkErrorCode = (typeof SDK_ERROR_CODES)[number];
|
|
714
719
|
/**
|
|
715
720
|
* A stable code a caller can branch on: a server-defined code (open-ended —
|
|
716
|
-
* see `ErrorCode`'s own doc comment), one of
|
|
717
|
-
* codes
|
|
721
|
+
* see `ErrorCode`'s own doc comment), one of `SdkErrorCode`'s client-side
|
|
722
|
+
* codes, or, since neither list is exhaustive, any other string.
|
|
718
723
|
* `(string & {})` is the standard trick to keep autocomplete on the known
|
|
719
724
|
* values while still accepting an arbitrary one.
|
|
720
725
|
*/
|
|
721
726
|
type FleetlessErrorCode = ErrorCode | SdkErrorCode | (string & {});
|
|
722
727
|
/**
|
|
723
|
-
* The
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
* parse `message` — it is not part of the contract, only `code` is.
|
|
728
|
+
* The third argument of `FleetlessError`'s constructor: what a thrower can
|
|
729
|
+
* attach beyond the code and the message. Both fields are optional, and
|
|
730
|
+
* both are absent on the codes the SDK raises before any request is sent.
|
|
727
731
|
*/
|
|
728
732
|
interface FleetlessErrorOptions {
|
|
729
733
|
/** Field-level detail for validation errors, passed through verbatim. */
|
|
@@ -731,79 +735,126 @@ interface FleetlessErrorOptions {
|
|
|
731
735
|
/** The HTTP status of the response that produced this error, if any. */
|
|
732
736
|
status?: number;
|
|
733
737
|
}
|
|
738
|
+
/**
|
|
739
|
+
* The one error type the SDK throws for a refused API call: a stable
|
|
740
|
+
* machine-readable `code` a caller can branch on (`forbidden` versus
|
|
741
|
+
* `token_expired`) plus a human `message` for logs and debugging. Never
|
|
742
|
+
* parse `message` — it is not part of the contract, only `code` is.
|
|
743
|
+
*
|
|
744
|
+
* Catch it by shape rather than by class where you can (`err.code`), since
|
|
745
|
+
* a bundler that ends up with two copies of the SDK also ends up with two
|
|
746
|
+
* classes and `instanceof` then answers `false` for a genuine one.
|
|
747
|
+
*/
|
|
734
748
|
declare class FleetlessError extends Error {
|
|
749
|
+
/** What went wrong, as a stable string — the field to branch on. */
|
|
735
750
|
readonly code: FleetlessErrorCode;
|
|
751
|
+
/**
|
|
752
|
+
* Structured detail the server sent with the refusal, if any: the
|
|
753
|
+
* violations behind `parameter_invalid`, the running job behind `busy`,
|
|
754
|
+
* `retry_after_ms` behind `rate_limited`. Parse it rather than assume
|
|
755
|
+
* its shape — `parameterInvalidDetails` is exported for exactly that.
|
|
756
|
+
*/
|
|
736
757
|
readonly details?: unknown;
|
|
758
|
+
/** The HTTP status of the response that produced this error, if it came from one. */
|
|
737
759
|
readonly status?: number;
|
|
760
|
+
/**
|
|
761
|
+
* Builds an error. `code` is what a caller branches on and `message` is
|
|
762
|
+
* for a human; anything else the refusal carried goes in `options`.
|
|
763
|
+
*/
|
|
738
764
|
constructor(code: FleetlessErrorCode, message: string, options?: FleetlessErrorOptions);
|
|
739
765
|
}
|
|
740
766
|
|
|
767
|
+
/**
|
|
768
|
+
* The options every realtime command accepts — `actions.cancel` and
|
|
769
|
+
* `publishers.publish` take exactly these; `InvokeOptions` extends them for
|
|
770
|
+
* `actions.invoke` and `services.call`.
|
|
771
|
+
*/
|
|
741
772
|
interface SendCommandOptions {
|
|
742
773
|
/** How long to wait for a `command_result` before rejecting `command_timeout`. Default 10s. */
|
|
743
774
|
timeoutMs?: number;
|
|
744
775
|
}
|
|
745
776
|
/**
|
|
746
|
-
* `invoke
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
* long as it runs and is observed, not awaited).
|
|
750
|
-
*
|
|
751
|
-
* Optional; absent means the platform's own default (`DEFAULT_PATIENCE_MS`,
|
|
752
|
-
* 15s) — exactly today's behaviour for a caller who names no preference.
|
|
753
|
-
* Outside `[MIN_PATIENCE_MS, MAX_PATIENCE_MS]` the platform refuses with
|
|
754
|
-
* `validation_error` rather than clamping, and this SDK does not clamp
|
|
755
|
-
* locally or retry — surfacing the refusal is the whole of what it does
|
|
756
|
-
* with this field. The floor exists because impatience reaches the robot,
|
|
757
|
-
* not just the platform: a patience too short to survive a goal-acceptance
|
|
758
|
-
* round trip made the bridge report `goal_timeout` and then issue a
|
|
759
|
-
* *corrective cancel* against a goal an action server accepted a moment
|
|
760
|
-
* later — a caller who names an unreachable deadline was causing a real
|
|
761
|
-
* cancellation on the machine, repeatably, not just receiving an error.
|
|
762
|
-
*
|
|
763
|
-
* `timeoutMs` bounds how long *this SDK* waits locally for a reply on the
|
|
764
|
-
* wire it already sent on; `patienceMs` travels to the platform and bounds
|
|
765
|
-
* what *it* is willing to wait for from the robot. **They are not set
|
|
766
|
-
* independently of each other, and D3a exists because the first version of
|
|
767
|
-
* this doc comment said they were.** `timeoutMs` left unset is *derived*
|
|
768
|
-
* from `patienceMs` (see `resolveLocalWaitMs`), not defaulted to a fixed
|
|
769
|
-
* number that might be shorter — the SDK giving up locally before the
|
|
770
|
-
* platform's own deadline would report `command_timeout` for a call the
|
|
771
|
-
* platform never actually refused, which is exactly the two-clocks defect
|
|
772
|
-
* this wave exists to remove, just relocated into this SDK instead of
|
|
773
|
-
* between the cloud and the bridge. Setting both explicitly with
|
|
774
|
-
* `timeoutMs < patienceMs` is refused with `invalid_option` before any
|
|
775
|
-
* request is sent, for the same reason.
|
|
777
|
+
* What `actions.invoke` and `services.call` accept on top of
|
|
778
|
+
* `SendCommandOptions`: the two clocks a command runs under, one local to
|
|
779
|
+
* this SDK and one on the platform.
|
|
776
780
|
*/
|
|
777
781
|
interface InvokeOptions extends SendCommandOptions {
|
|
782
|
+
/**
|
|
783
|
+
* How long **the platform itself** should wait for this one call before
|
|
784
|
+
* giving up on the robot — the whole wait for a service call, goal
|
|
785
|
+
* *acceptance* only for an action (once accepted, a job runs as long as it
|
|
786
|
+
* runs and is observed, not awaited).
|
|
787
|
+
*
|
|
788
|
+
* Optional; absent means the platform's own `DEFAULT_PATIENCE_MS`, 15s —
|
|
789
|
+
* exactly the behaviour of a caller who names no preference. Outside
|
|
790
|
+
* `MIN_PATIENCE_MS` to `MAX_PATIENCE_MS` (1s to 120s, both exported by
|
|
791
|
+
* `@fleetless/contracts`) the platform refuses with `validation_error`
|
|
792
|
+
* rather than clamping, and this SDK does not clamp locally or retry:
|
|
793
|
+
* surfacing the refusal is the whole of what it does with this field.
|
|
794
|
+
* The floor exists because
|
|
795
|
+
* impatience reaches the robot, not just the platform — a patience too
|
|
796
|
+
* short to survive a goal-acceptance round trip made the bridge report
|
|
797
|
+
* `goal_timeout` and then issue a *corrective cancel* against a goal an
|
|
798
|
+
* action server accepted a moment later, so a caller who names an
|
|
799
|
+
* unreachable deadline was causing a real cancellation on the machine,
|
|
800
|
+
* repeatably, not just receiving an error.
|
|
801
|
+
*
|
|
802
|
+
* `timeoutMs` bounds how long *this SDK* waits locally for a reply on the
|
|
803
|
+
* wire it already sent on; `patienceMs` travels to the platform and bounds
|
|
804
|
+
* what *it* is willing to wait for from the robot. **They are not set
|
|
805
|
+
* independently of each other.** `timeoutMs` left unset is *derived* from
|
|
806
|
+
* `patienceMs`, not defaulted to a fixed number that might be shorter —
|
|
807
|
+
* the SDK giving up locally before the platform's own deadline would
|
|
808
|
+
* report `command_timeout` for a call the platform never actually refused.
|
|
809
|
+
* Setting both explicitly with `timeoutMs < patienceMs` is refused with
|
|
810
|
+
* `invalid_option` before any request is sent, for the same reason.
|
|
811
|
+
*/
|
|
778
812
|
patienceMs?: number;
|
|
779
813
|
}
|
|
780
814
|
|
|
815
|
+
/**
|
|
816
|
+
* The callbacks `actions.subscribe` reports through: one for every job
|
|
817
|
+
* update on the slug, one for a refusal of the subscription itself.
|
|
818
|
+
*/
|
|
781
819
|
interface JobSubscriptionHandlers {
|
|
782
820
|
/** Called on every update pushed for the slug's current job — state, feedback, progress and result. */
|
|
783
821
|
onJob(event: JobEvent): void;
|
|
784
|
-
/** Called once if the subscription is refused
|
|
822
|
+
/** Called once if the subscription is refused, e.g. `forbidden` or an unknown slug. */
|
|
785
823
|
onError?(error: FleetlessError): void;
|
|
786
824
|
}
|
|
825
|
+
/** A live job subscription, returned by `actions.subscribe`. */
|
|
787
826
|
interface JobSubscription {
|
|
788
|
-
/**
|
|
827
|
+
/**
|
|
828
|
+
* Stops this subscription. The `unsubscribe` frame reaches the server only
|
|
829
|
+
* when this was the **last** holder of the robot and slug pair, and only if
|
|
830
|
+
* the channel is connected — subscriptions are reference-counted across
|
|
831
|
+
* kinds, so releasing this one while a second `actions.subscribe`, a
|
|
832
|
+
* `datapoints.subscribe` or an in-flight `services.call` still holds the
|
|
833
|
+
* same pair leaves that one's stream running untouched. Safe to call more
|
|
834
|
+
* than once.
|
|
835
|
+
*/
|
|
789
836
|
unsubscribe(): void;
|
|
790
837
|
}
|
|
791
838
|
|
|
839
|
+
/**
|
|
840
|
+
* Long-running work on a robot, reachable as `client.actions`. An action is
|
|
841
|
+
* a ROS action the developer exposed under a slug: it is invoked, it runs
|
|
842
|
+
* for as long as it runs, and it reports back while it does.
|
|
843
|
+
*/
|
|
792
844
|
interface ActionsApi {
|
|
793
845
|
/**
|
|
794
|
-
* Invokes an action
|
|
846
|
+
* Invokes an action. Resolves as soon as the job is created
|
|
795
847
|
* — the job id is informative, not the result. Feedback, progress and the
|
|
796
848
|
* eventual result arrive separately over `subscribe`. A second invoke of
|
|
797
849
|
* the same slug while one is already running is refused `busy`, with
|
|
798
850
|
* `error.details.running` naming the job that is running.
|
|
799
851
|
*
|
|
800
|
-
* `options.patienceMs` bounds goal *acceptance* only
|
|
852
|
+
* `options.patienceMs` bounds goal *acceptance* only — once a goal
|
|
801
853
|
* is accepted this call has already resolved; the job then runs as long
|
|
802
854
|
* as it runs, observed via `subscribe`, never awaited. `options.timeoutMs`
|
|
803
855
|
* (this SDK's own local wait for the acceptance reply) is derived from
|
|
804
856
|
* `patienceMs` when left unset, and the combination `timeoutMs <
|
|
805
|
-
* patienceMs` is refused with `invalid_option` rather than raced
|
|
806
|
-
* `resolveLocalWaitMs` in `commands.ts`, D3a.
|
|
857
|
+
* patienceMs` is refused with `invalid_option` rather than raced.
|
|
807
858
|
*/
|
|
808
859
|
invoke(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<Job>;
|
|
809
860
|
/**
|
|
@@ -811,10 +862,9 @@ interface ActionsApi {
|
|
|
811
862
|
* Resolves with the `Job` the cancel was actually sent to, or `null` if
|
|
812
863
|
* nothing matched.
|
|
813
864
|
*
|
|
814
|
-
* **Two different requests, both legitimate
|
|
865
|
+
* **Two different requests, both legitimate:**
|
|
815
866
|
* - `cancel(robotId, slug)` — no `jobId` — is the operator's stop button:
|
|
816
|
-
* whatever is running on this slug, stop it.
|
|
817
|
-
* before W6b.
|
|
867
|
+
* whatever is running on this slug, stop it.
|
|
818
868
|
* - `cancel(robotId, slug, jobId)` cancels **that** job specifically. If
|
|
819
869
|
* it is not the one running, the platform answers `not_found` — this
|
|
820
870
|
* never silently falls back to stopping whatever *is* running, because
|
|
@@ -830,7 +880,7 @@ interface ActionsApi {
|
|
|
830
880
|
cancel(robotId: string, slug: string, jobId?: string | null, options?: SendCommandOptions): Promise<Job | null>;
|
|
831
881
|
/**
|
|
832
882
|
* Subscribes to the slug's job: state, feedback, progress and result, as
|
|
833
|
-
* they happen. State is observed **by slug**, not by job id
|
|
883
|
+
* they happen. State is observed **by slug**, not by job id — this
|
|
834
884
|
* is what makes late delivery after a reconnect and a second observer
|
|
835
885
|
* watching the same job both work without special-casing either. Naming a
|
|
836
886
|
* job to `cancel` does not change this: a slug is still a *place a job may
|
|
@@ -841,7 +891,9 @@ interface ActionsApi {
|
|
|
841
891
|
|
|
842
892
|
/** An asset's bytes plus its declared media type — the shape `assets.get` answers with. */
|
|
843
893
|
interface AssetBytes {
|
|
894
|
+
/** The asset's raw bytes, exactly as stored. */
|
|
844
895
|
body: Uint8Array;
|
|
896
|
+
/** The declared media type, or `null` if the store did not record one. */
|
|
845
897
|
mime: string | null;
|
|
846
898
|
}
|
|
847
899
|
/**
|
|
@@ -856,6 +908,7 @@ interface AssetBytes {
|
|
|
856
908
|
* not only that one library.
|
|
857
909
|
*/
|
|
858
910
|
type MeshLoaderDelegate = (path: string, manager: unknown, material: unknown, onComplete: (obj: unknown | null, err?: Error) => void) => void;
|
|
911
|
+
/** What `assets.createMeshLoader` accepts beyond the robot and the delegate. */
|
|
859
912
|
interface CreateMeshLoaderOptions {
|
|
860
913
|
/**
|
|
861
914
|
* How long to wait for `delegate`'s `onComplete` before giving up.
|
|
@@ -867,12 +920,14 @@ interface CreateMeshLoaderOptions {
|
|
|
867
920
|
timeoutMs?: number;
|
|
868
921
|
}
|
|
869
922
|
/**
|
|
870
|
-
* three.js's own `LoadingManager.setURLModifier(callback)` shape
|
|
923
|
+
* three.js's own `LoadingManager.setURLModifier(callback)` shape — the
|
|
924
|
+
* only thing `assets.prepareUrdfScene` needs from a three.js
|
|
925
|
+
* `LoadingManager`, so a caller passes theirs straight in.
|
|
871
926
|
* Every load the manager oversees is routed through `callback` first — not
|
|
872
927
|
* only the loader you handed the manager to, but every loader it constructs
|
|
873
928
|
* internally on the same manager (`ColladaLoader`'s own `TextureLoader` for
|
|
874
929
|
* a `.dae`'s `<init_from>` images, in particular). That is the one hook
|
|
875
|
-
* that exists
|
|
930
|
+
* that exists one level up, for everything, where `createMeshLoader`'s
|
|
876
931
|
* per-loader `loadMeshCb` override does not reach: `TextureLoader` has no
|
|
877
932
|
* override hook of its own.
|
|
878
933
|
*
|
|
@@ -880,8 +935,10 @@ interface CreateMeshLoaderOptions {
|
|
|
880
935
|
* or `urdf-loader`, same discipline as `MeshLoaderDelegate` above.
|
|
881
936
|
*/
|
|
882
937
|
interface UrdfSceneManager {
|
|
938
|
+
/** Installs a callback every load through this manager is routed through first. */
|
|
883
939
|
setURLModifier(callback: (url: string) => string): unknown;
|
|
884
940
|
}
|
|
941
|
+
/** What `assets.prepareUrdfScene` accepts beyond the robot and the manager. */
|
|
885
942
|
interface PrepareUrdfSceneOptions {
|
|
886
943
|
/**
|
|
887
944
|
* How many assets to fetch in parallel. Default 6 — a default that keeps
|
|
@@ -889,24 +946,23 @@ interface PrepareUrdfSceneOptions {
|
|
|
889
946
|
* meshes), not a number with a sweep behind it. Pass your own if you have
|
|
890
947
|
* a reason to.
|
|
891
948
|
*
|
|
892
|
-
* Must be a positive integer — `0` or negative
|
|
893
|
-
* rather than silently fetching nothing and returning a
|
|
894
|
-
* renders completely blank with no error to explain why.
|
|
949
|
+
* Must be a positive integer — `0` or negative rejects with
|
|
950
|
+
* `invalid_option` rather than silently fetching nothing and returning a
|
|
951
|
+
* scene that renders completely blank with no error to explain why.
|
|
895
952
|
*/
|
|
896
953
|
concurrency?: number;
|
|
897
954
|
/**
|
|
898
|
-
* Cancels this call
|
|
955
|
+
* Cancels this call — a caller who navigates away
|
|
899
956
|
* or switches to a different robot mid-load can abort every in-flight
|
|
900
957
|
* fetch this method has started, not merely stop it from starting new
|
|
901
958
|
* ones. Checked before the first request; if it fires while a request is
|
|
902
|
-
* already in progress,
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
* background.
|
|
959
|
+
* already in progress, the SDK forwards it straight to `fetch()`, so the
|
|
960
|
+
* connection itself is torn down, not just abandoned by this SDK while it
|
|
961
|
+
* keeps running in the background.
|
|
906
962
|
*
|
|
907
963
|
* Every `blob:` URL already created before the abort is revoked before
|
|
908
964
|
* this call rejects with `FleetlessError('aborted', ...)` — the same
|
|
909
|
-
* guarantee a load that fails outright already had
|
|
965
|
+
* guarantee a load that fails outright already had: an aborted load
|
|
910
966
|
* must not leak what it had already fetched.
|
|
911
967
|
*
|
|
912
968
|
* `manager`'s URL modifier is only ever installed once every asset has
|
|
@@ -916,6 +972,10 @@ interface PrepareUrdfSceneOptions {
|
|
|
916
972
|
*/
|
|
917
973
|
signal?: AbortSignal;
|
|
918
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* What `assets.prepareUrdfScene` resolves with: the URDF text to parse, what
|
|
977
|
+
* the sync could not resolve, and the cleanup for everything it fetched.
|
|
978
|
+
*/
|
|
919
979
|
interface UrdfSceneResources {
|
|
920
980
|
/**
|
|
921
981
|
* The robot's URDF as raw text — `package://` URIs intact, not rewritten
|
|
@@ -926,24 +986,21 @@ interface UrdfSceneResources {
|
|
|
926
986
|
urdfText: string;
|
|
927
987
|
/**
|
|
928
988
|
* The same entries `assets.list()`'s `urdf.missing` reports — verbatim,
|
|
929
|
-
* not reduced to bare strings
|
|
989
|
+
* not reduced to bare strings. Each carries `element`
|
|
930
990
|
* (`'mesh' | 'texture'`) alongside `uri`: before this, both kinds arrived
|
|
931
991
|
* as an undifferentiated `string[]` and a caller could only ever say "N
|
|
932
|
-
* meshes missing", wrongly, for a URDF whose gap was actually a texture
|
|
933
|
-
*
|
|
934
|
-
*
|
|
992
|
+
* meshes missing", wrongly, for a URDF whose gap was actually a texture.
|
|
993
|
+
* Reducing this field back to `string[]` here would throw the distinction
|
|
994
|
+
* away again at
|
|
935
995
|
* exactly the point a caller would render it. If you only need the URIs,
|
|
936
996
|
* `missing.map(m => m.uri)`.
|
|
937
997
|
*
|
|
938
|
-
* **Top-level only — not a `.dae`'s internal references
|
|
939
|
-
*
|
|
998
|
+
* **Top-level only — not a `.dae`'s internal references.** The cloud
|
|
999
|
+
* builds this list from the URDF text
|
|
940
1000
|
* alone (`<mesh>`/`<texture>` `filename` attributes), which is the only
|
|
941
1001
|
* place it can see without parsing every `.dae` a sync touches; it never
|
|
942
|
-
* has and never can include an internal `<init_from>` reference.
|
|
943
|
-
*
|
|
944
|
-
* as a correction rather than silently widening the sentence, since a
|
|
945
|
-
* caller who trusted "both" to mean both would build a completeness check
|
|
946
|
-
* against a list that structurally cannot report the second half. A
|
|
1002
|
+
* has and never can include an internal `<init_from>` reference. Do not
|
|
1003
|
+
* build a completeness check on it as though it covered both: a
|
|
947
1004
|
* `.dae`-internal reference the sync could not resolve surfaces through
|
|
948
1005
|
* the sync's own failure reporting instead, not here.
|
|
949
1006
|
*
|
|
@@ -955,44 +1012,53 @@ interface UrdfSceneResources {
|
|
|
955
1012
|
*/
|
|
956
1013
|
missing: UrdfCompleteness['missing'];
|
|
957
1014
|
/**
|
|
958
|
-
* Revokes every
|
|
959
|
-
* finished loading (success or failure) or on unmount
|
|
960
|
-
* than once.
|
|
1015
|
+
* Revokes every object URL this call created that carries bytes. Call
|
|
1016
|
+
* once the scene has finished loading (success or failure) or on unmount
|
|
1017
|
+
* — safe to call more than once.
|
|
1018
|
+
*
|
|
1019
|
+
* One object URL is deliberately kept: the shared zero-byte placeholder
|
|
1020
|
+
* every refused reference resolves to. It costs nothing to leave alive,
|
|
1021
|
+
* and leaving it is what lets this method stay simple — the installed URL
|
|
1022
|
+
* modifier goes on refusing an owned-but-gone reference correctly, rather
|
|
1023
|
+
* than falling back to the original string once the map is empty.
|
|
961
1024
|
*
|
|
962
|
-
* **Does not touch `manager`'s URL modifier
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
1025
|
+
* **Does not touch `manager`'s URL modifier.** Resetting it to the
|
|
1026
|
+
* identity function here would reopen the very hole the modifier exists to
|
|
1027
|
+
* close, the moment the same manager was used again — for a second robot,
|
|
1028
|
+
* or for anything else — until a later `prepareUrdfScene` call happened to
|
|
1029
|
+
* overwrite it. The installed modifier is left running,
|
|
967
1030
|
* and with this call's map now empty it already refuses anything it would
|
|
968
1031
|
* have owned and passes through anything it would not have, correctly,
|
|
969
1032
|
* on its own.
|
|
970
1033
|
*/
|
|
971
1034
|
dispose(): void;
|
|
972
1035
|
}
|
|
1036
|
+
/**
|
|
1037
|
+
* A robot's synced files, reachable as `client.assets`: its URDF, the meshes
|
|
1038
|
+
* and textures that URDF references, and the glue a three.js renderer needs
|
|
1039
|
+
* to fetch them with this client's credentials.
|
|
1040
|
+
*/
|
|
973
1041
|
interface AssetsApi {
|
|
974
1042
|
/**
|
|
975
|
-
* Every asset a robot has, plus whether its URDF is complete
|
|
1043
|
+
* Every asset a robot has, plus whether its URDF is complete.
|
|
976
1044
|
* `urdf.missing` names the `package://` URIs the sync could not resolve —
|
|
977
|
-
*
|
|
1045
|
+
* a count alone ("2 meshes missing") sends a developer looking through a
|
|
978
1046
|
* workspace by hand, the URIs are what they can act on.
|
|
979
1047
|
*/
|
|
980
1048
|
list(robotId: string): Promise<AssetListResponse>;
|
|
981
1049
|
/**
|
|
982
1050
|
* The status of one sync by id — for **reconnecting** to a sync already in
|
|
983
|
-
* flight, not for starting one
|
|
1051
|
+
* flight, not for starting one.
|
|
984
1052
|
*
|
|
985
|
-
* **Starting a sync stays out of this SDK, on purpose
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
1053
|
+
* **Starting a sync stays out of this SDK, on purpose.** Assets are
|
|
1054
|
+
* transferred only on a developer's explicit request from the console —
|
|
1055
|
+
* an Owner-tier action, done once. This method is a different thing:
|
|
1056
|
+
* `list()`'s
|
|
989
1057
|
* `active_sync` (or a `busy` refusal's `assetSyncBusyDetails`) hands a
|
|
990
1058
|
* caller a `sync_id` for a sync that is **already running**, and before
|
|
991
1059
|
* this method existed there was no way for anything built on this SDK to
|
|
992
|
-
* do anything with that id except throw it away
|
|
993
|
-
*
|
|
994
|
-
* (`GET .../assets/sync/{id}` has existed since W7) — nothing reachable
|
|
995
|
-
* from this SDK ever called it.
|
|
1060
|
+
* do anything with that id except throw it away, even though the sync was
|
|
1061
|
+
* readable server-side the whole time.
|
|
996
1062
|
*
|
|
997
1063
|
* A page reload is the case this exists for: whatever held the `sync_id`
|
|
998
1064
|
* in memory is gone, `list()` (or a fresh `busy` refusal) hands it back,
|
|
@@ -1010,11 +1076,11 @@ interface AssetsApi {
|
|
|
1010
1076
|
*/
|
|
1011
1077
|
urdf(robotId: string): Promise<string>;
|
|
1012
1078
|
/**
|
|
1013
|
-
* The mesh callback for `urdf-loader
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1079
|
+
* The mesh callback for `urdf-loader`: an `<img>` tag and the default
|
|
1080
|
+
* three.js loaders cannot set an `Authorization` header, and the platform
|
|
1081
|
+
* deliberately has no signed URLs and no token in the query string, so
|
|
1082
|
+
* every app would otherwise write this glue itself, and each one
|
|
1083
|
+
* differently.
|
|
1018
1084
|
*
|
|
1019
1085
|
* Returns a function with `loadMeshCb`'s own signature — assign it
|
|
1020
1086
|
* directly:
|
|
@@ -1043,7 +1109,7 @@ interface AssetsApi {
|
|
|
1043
1109
|
createMeshLoader(robotId: string, delegate: MeshLoaderDelegate, options?: CreateMeshLoaderOptions): MeshLoaderDelegate;
|
|
1044
1110
|
/**
|
|
1045
1111
|
* Authenticated loading for everything three.js fetches to render a
|
|
1046
|
-
* textured robot — not only meshes
|
|
1112
|
+
* textured robot — not only meshes. Installs
|
|
1047
1113
|
* `manager.setURLModifier` so **every** load `manager` oversees resolves
|
|
1048
1114
|
* to a pre-fetched `blob:` URL: a top-level `<mesh>`, a `<material>`'s
|
|
1049
1115
|
* `<texture>`, and an image a `.dae` references internally via
|
|
@@ -1065,9 +1131,8 @@ interface AssetsApi {
|
|
|
1065
1131
|
* **Do not also install `createMeshLoader` on the same manager.** The two
|
|
1066
1132
|
* consume different URDF sources — this method fetches the URDF's *raw*
|
|
1067
1133
|
* bytes, `createMeshLoader` is meant to pair with `urdf()`'s
|
|
1068
|
-
* cloud-rewritten text. **
|
|
1069
|
-
*
|
|
1070
|
-
* and found it wrong.** What actually happens is asymmetric breakage,
|
|
1134
|
+
* cloud-rewritten text. **It is not that every mesh gets fetched twice.**
|
|
1135
|
+
* What actually happens is asymmetric breakage,
|
|
1071
1136
|
* whichever URDF text the combination ends up parsing: paired with
|
|
1072
1137
|
* *this* method's raw text, `createMeshLoader` receives urdf-loader's
|
|
1073
1138
|
* `resolvePath()` output (`/pkg/rel`) rather than an absolute Fleetless
|
|
@@ -1079,12 +1144,11 @@ interface AssetsApi {
|
|
|
1079
1144
|
* debug the wrong symptom, which is worse than the original (already
|
|
1080
1145
|
* wrong) warning being merely unhelpful.
|
|
1081
1146
|
*
|
|
1082
|
-
* **Enforced, not only documented
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
* this paragraph. See `managersWithPreparedUrdfScene`.
|
|
1147
|
+
* **Enforced, not only documented.** `createMeshLoader`'s returned
|
|
1148
|
+
* callback checks whether the `manager` it is handed already has this
|
|
1149
|
+
* method's URL modifier installed and fails loudly via
|
|
1150
|
+
* `onComplete(null, err)` before ever touching the network, rather than
|
|
1151
|
+
* relying on a developer having read this paragraph.
|
|
1088
1152
|
*
|
|
1089
1153
|
* **Why raw bytes, not `urdf()`'s rewritten text.** Both `urdf-loader`'s
|
|
1090
1154
|
* default mesh loading and `ColladaLoader` compute the base path they use
|
|
@@ -1099,9 +1163,8 @@ interface AssetsApi {
|
|
|
1099
1163
|
* in sync.
|
|
1100
1164
|
*
|
|
1101
1165
|
* **`urdf-loader` resolves `package://` itself, before any of this runs —
|
|
1102
|
-
* a second resolution stage this method has to account for,
|
|
1103
|
-
*
|
|
1104
|
-
* reading the source.** `URDFLoader.parse()`'s own `resolvePath()`
|
|
1166
|
+
* a second resolution stage this method has to account for, measured in a
|
|
1167
|
+
* real browser rather than read off the source.** `URDFLoader.parse()`'s own `resolvePath()`
|
|
1105
1168
|
* rewrites `package://pkg/rel` using `this.packages` (default `''`) to
|
|
1106
1169
|
* `/pkg/rel` — a root-relative URL — and *that* is what reaches
|
|
1107
1170
|
* `loadMeshCb`/`ColladaLoader`/`manager.resolveURL()`, not the original
|
|
@@ -1121,29 +1184,29 @@ interface AssetsApi {
|
|
|
1121
1184
|
* outside what this method can predict — see the ownership rule below
|
|
1122
1185
|
* for what happens to that reference.
|
|
1123
1186
|
*
|
|
1124
|
-
* **This method only claims what it owns
|
|
1125
|
-
*
|
|
1187
|
+
* **This method only claims what it owns — not every unmapped
|
|
1188
|
+
* reference.** `manager` is frequently the
|
|
1126
1189
|
* caller's own scene-wide `LoadingManager`, shared for an HDRI, an
|
|
1127
1190
|
* environment map, a font atlas, a ground texture — none of which have
|
|
1128
|
-
* anything to do with this robot.
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1191
|
+
* anything to do with this robot. Refusing everything unmapped would
|
|
1192
|
+
* silently empty every one of those the moment a caller shares their
|
|
1193
|
+
* manager. So the rule is narrower: a `package://`
|
|
1131
1194
|
* reference, or a root-relative path whose leading segment names a ROS
|
|
1132
1195
|
* package this robot's assets (or `missing`) actually mention, is this
|
|
1133
1196
|
* method's to resolve or refuse; an unmapped one falls back to the
|
|
1134
|
-
* normalized form (
|
|
1197
|
+
* normalized form (below) and then to a shared, inert, page-local
|
|
1135
1198
|
* `blob:` URL — never the original string, so a hostile URDF naming an
|
|
1136
1199
|
* unsynced or off-namespace reference still cannot make three.js touch
|
|
1137
1200
|
* the network for it. Anything else — not in that namespace — is left
|
|
1138
1201
|
* completely alone, **except** an absolute `http(s)` URL, which is
|
|
1139
1202
|
* refused regardless of namespace: the one case this method cannot leave
|
|
1140
1203
|
* ambiguous, because a hostile URDF naming an attacker's host directly
|
|
1141
|
-
* (bypassing `package://` entirely) is exactly what
|
|
1142
|
-
* and three.js would otherwise fetch it for real, off-origin, the moment
|
|
1204
|
+
* (bypassing `package://` entirely) is exactly what this rule exists to
|
|
1205
|
+
* close, and three.js would otherwise fetch it for real, off-origin, the moment
|
|
1143
1206
|
* the direct and namespace checks both miss.
|
|
1144
1207
|
*
|
|
1145
1208
|
* **A `.dae`'s own internal reference gets a second-chance, normalized
|
|
1146
|
-
* lookup
|
|
1209
|
+
* lookup, reproduced in a real browser.** three.js
|
|
1147
1210
|
* builds the request for one by plain string concatenation — no `..`/`.`
|
|
1148
1211
|
* collapsing — while `asset.name` carries the *normalized* tail
|
|
1149
1212
|
* (`@fleetless/contracts`' naming rule). So `../textures/skin.png` or
|
|
@@ -1154,17 +1217,17 @@ interface AssetsApi {
|
|
|
1154
1217
|
* verbatim and `resolvePath()` rewrites it by the same unnormalized
|
|
1155
1218
|
* concatenation on both sides, so the direct key already matches.
|
|
1156
1219
|
*
|
|
1157
|
-
* **`dispose()` does not undo any of this
|
|
1158
|
-
*
|
|
1220
|
+
* **`dispose()` does not undo any of this.** See its own doc comment on
|
|
1221
|
+
* `UrdfSceneResources`.
|
|
1159
1222
|
*
|
|
1160
1223
|
* **Pre-fetch is unavoidable**, not merely a choice: a URL modifier
|
|
1161
1224
|
* cannot be asynchronous, so every asset it might be asked for has to
|
|
1162
1225
|
* already be a `blob:` URL before `URDFLoader.parse` runs. Bounded by
|
|
1163
1226
|
* `options.concurrency` (default 6) and scoped to only `kind: 'mesh'` and
|
|
1164
|
-
* `kind: 'texture'` assets — which is already "what the URDF references"
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1227
|
+
* `kind: 'texture'` assets — which is already "what the URDF references",
|
|
1228
|
+
* since a re-sync reconciles and assets the current URDF no longer
|
|
1229
|
+
* references stop belonging to the robot. Not an unbounded fetch of
|
|
1230
|
+
* everything the robot has ever had.
|
|
1168
1231
|
*/
|
|
1169
1232
|
prepareUrdfScene(robotId: string, manager: UrdfSceneManager, options?: PrepareUrdfSceneOptions): Promise<UrdfSceneResources>;
|
|
1170
1233
|
}
|
|
@@ -1182,35 +1245,56 @@ type StoredSession = SessionTokens;
|
|
|
1182
1245
|
* assumes a browser, or any storage, exists.
|
|
1183
1246
|
*/
|
|
1184
1247
|
interface TokenStore {
|
|
1248
|
+
/**
|
|
1249
|
+
* Returns the stored session, or `null` when nobody is logged in. May be
|
|
1250
|
+
* async, so a store backed by a native keystore or an IndexedDB read
|
|
1251
|
+
* works without a synchronous cache in front of it.
|
|
1252
|
+
*/
|
|
1185
1253
|
load(): StoredSession | null | Promise<StoredSession | null>;
|
|
1254
|
+
/**
|
|
1255
|
+
* Writes the session, or clears it when passed `null`. Called after a
|
|
1256
|
+
* login, after every silent refresh, and on logout — so an implementation
|
|
1257
|
+
* that persists must expect to be called often, not once.
|
|
1258
|
+
*/
|
|
1186
1259
|
save(session: StoredSession | null): void | Promise<void>;
|
|
1187
1260
|
}
|
|
1188
1261
|
/** The default store: works out of the box, forgets the session on reload. */
|
|
1189
1262
|
declare class InMemoryTokenStore implements TokenStore {
|
|
1190
1263
|
#private;
|
|
1264
|
+
/** Creates an empty store. Nothing is loaded from anywhere — a client built with it starts logged out. */
|
|
1265
|
+
constructor();
|
|
1266
|
+
/** Returns the session held in memory, or `null` if there is none. */
|
|
1191
1267
|
load(): StoredSession | null;
|
|
1268
|
+
/** Replaces the session held in memory; `null` clears it. */
|
|
1192
1269
|
save(session: StoredSession | null): void;
|
|
1193
1270
|
}
|
|
1194
1271
|
|
|
1195
|
-
/**
|
|
1272
|
+
/**
|
|
1273
|
+
* `beginHostedLogin()`'s input: the OAuth client the developer registered
|
|
1274
|
+
* for this app, where the browser should come back to, and optionally what
|
|
1275
|
+
* the resulting token should be usable against.
|
|
1276
|
+
*/
|
|
1196
1277
|
interface BeginHostedLoginOptions {
|
|
1197
1278
|
/**
|
|
1198
1279
|
* The opaque `client_id` issued when the developer registered this app's
|
|
1199
1280
|
* OAuth client (console, App Settings). **Never `appIdentifier`** — they
|
|
1200
|
-
* are deliberately different identifiers
|
|
1201
|
-
* `
|
|
1202
|
-
* not the app identifier").
|
|
1281
|
+
* are deliberately different identifiers — the contracts call this "the
|
|
1282
|
+
* `client_id` on the wire: opaque, and not the app identifier".
|
|
1203
1283
|
*/
|
|
1204
1284
|
clientId: string;
|
|
1205
1285
|
/**
|
|
1206
1286
|
* Must be registered, byte-for-byte, as one of that client's
|
|
1207
1287
|
* `redirect_uris` — matching at the server is exact-string, never a
|
|
1208
|
-
* prefix (
|
|
1288
|
+
* prefix (the contracts' `redirectUri` doc comment says why).
|
|
1209
1289
|
*/
|
|
1210
1290
|
redirectUri: string;
|
|
1291
|
+
/**
|
|
1292
|
+
* The OAuth scopes to request, space-separated. Omit it to get the
|
|
1293
|
+
* client's registered default, which is what an ordinary app login wants.
|
|
1294
|
+
*/
|
|
1211
1295
|
scope?: string;
|
|
1212
1296
|
/**
|
|
1213
|
-
* RFC 8707 audience binding
|
|
1297
|
+
* RFC 8707 audience binding: the resource this
|
|
1214
1298
|
* session's token should be usable against. **Omit it for an ordinary app
|
|
1215
1299
|
* login.** A token with no `resource` carries no `aud` and works
|
|
1216
1300
|
* unrestricted against this app's own REST surface exactly as it always
|
|
@@ -1220,7 +1304,7 @@ interface BeginHostedLoginOptions {
|
|
|
1220
1304
|
*
|
|
1221
1305
|
* **`/oauth/authorize` accepts exactly two shapes** — anything else is
|
|
1222
1306
|
* refused with `invalid_target` on the redirect back, before a code is
|
|
1223
|
-
* ever issued (
|
|
1307
|
+
* ever issued (the cloud's own known-resource check):
|
|
1224
1308
|
*
|
|
1225
1309
|
* - `<base>/mcp-stub/resource` — the global OAuth resource stub, matched
|
|
1226
1310
|
* byte-for-byte.
|
|
@@ -1230,21 +1314,21 @@ interface BeginHostedLoginOptions {
|
|
|
1230
1314
|
* be the one this `clientId` is registered to, so a client on app A can
|
|
1231
1315
|
* never be minted a token whose `aud` names app B.
|
|
1232
1316
|
*
|
|
1233
|
-
* Both are served and validated by
|
|
1234
|
-
* `
|
|
1235
|
-
*
|
|
1317
|
+
* Both are served and validated by the platform's resource stub, whose
|
|
1318
|
+
* validator refuses a token with **no** `aud` exactly as hard as one with
|
|
1319
|
+
* the wrong `aud` —
|
|
1236
1320
|
* "unscoped" must never read as "for me" — comparing by equality, never
|
|
1237
1321
|
* by prefix.
|
|
1238
1322
|
*
|
|
1239
1323
|
* **`<base>/mcp/<app_identifier>` is not a resource any more.** There is
|
|
1240
1324
|
* no per-app MCP endpoint: the cloud registers one central, non-parametric
|
|
1241
|
-
* `POST /mcp` (
|
|
1242
|
-
*
|
|
1325
|
+
* `POST /mcp` (the contracts' `MCP_ENDPOINT_PATH`), and the
|
|
1326
|
+
* `/oauth/authorize` branch that used to
|
|
1243
1327
|
* accept a `/mcp/<app>` resource was deleted along with the app-level
|
|
1244
1328
|
* `mcp_enabled` flag. Asking for one now yields `invalid_target` for
|
|
1245
1329
|
* every app, **including your own**. The central MCP endpoint has its own
|
|
1246
|
-
* OAuth flow
|
|
1247
|
-
*
|
|
1330
|
+
* OAuth flow, which this SDK's hosted login does not drive —
|
|
1331
|
+
* `beginHostedLogin` always targets
|
|
1248
1332
|
* `OAUTH_PATHS.authorize`.
|
|
1249
1333
|
*
|
|
1250
1334
|
* Whatever you name here is re-checked at the token exchange: it must
|
|
@@ -1262,8 +1346,8 @@ interface HostedLoginRequest {
|
|
|
1262
1346
|
* both back into `completeHostedLogin`. **This SDK does not persist them
|
|
1263
1347
|
* for you.** The redirect back to `redirectUri` is a fresh page load for a
|
|
1264
1348
|
* browser app — nothing kept in this SDK's own memory survives it (the
|
|
1265
|
-
* same reasoning `
|
|
1266
|
-
*
|
|
1349
|
+
* same reasoning `TokenStore` states: the SDK itself never assumes a
|
|
1350
|
+
* browser, or any storage, exists). An in-memory default
|
|
1267
1351
|
* here would not be merely suboptimal, it would be broken for the primary
|
|
1268
1352
|
* use case while looking like it worked for anything that never actually
|
|
1269
1353
|
* navigates away. `sessionStorage`, a signed cookie, or a plain variable
|
|
@@ -1271,6 +1355,11 @@ interface HostedLoginRequest {
|
|
|
1271
1355
|
* the caller's.
|
|
1272
1356
|
*/
|
|
1273
1357
|
state: string;
|
|
1358
|
+
/**
|
|
1359
|
+
* The PKCE code verifier for this attempt. Persist it exactly as
|
|
1360
|
+
* `state` above and pass it back to `completeHostedLogin` — it is what
|
|
1361
|
+
* proves the code exchange comes from the client that started the flow.
|
|
1362
|
+
*/
|
|
1274
1363
|
codeVerifier: string;
|
|
1275
1364
|
}
|
|
1276
1365
|
/** `completeHostedLogin()`'s input — the redirect back, plus what `beginHostedLogin` returned for this same attempt. */
|
|
@@ -1281,13 +1370,14 @@ interface CompleteHostedLoginOptions {
|
|
|
1281
1370
|
state: string;
|
|
1282
1371
|
/**
|
|
1283
1372
|
* The `state` this attempt's `beginHostedLogin` returned. Checked against
|
|
1284
|
-
* `state` above **before any network call** —
|
|
1285
|
-
*
|
|
1286
|
-
* did not itself request.
|
|
1373
|
+
* `state` above **before any network call** — the whole point of RFC
|
|
1374
|
+
* 6749 section 10.12 is that a caller must not complete an authorization
|
|
1375
|
+
* response it did not itself request.
|
|
1287
1376
|
*/
|
|
1288
1377
|
expectedState: string;
|
|
1289
1378
|
/** The `codeVerifier` this attempt's `beginHostedLogin` returned. */
|
|
1290
1379
|
codeVerifier: string;
|
|
1380
|
+
/** Must be the exact same `client_id` passed to `beginHostedLogin`. */
|
|
1291
1381
|
clientId: string;
|
|
1292
1382
|
/** Must be the exact same string passed to `beginHostedLogin`. */
|
|
1293
1383
|
redirectUri: string;
|
|
@@ -1296,22 +1386,31 @@ interface CompleteHostedLoginOptions {
|
|
|
1296
1386
|
* Resending it here is not what binds the audience — the server already
|
|
1297
1387
|
* bound `resource` to the authorization code at `/oauth/authorize` and
|
|
1298
1388
|
* mints `aud` from that stored value regardless of what this call sends —
|
|
1299
|
-
* but RFC 8707
|
|
1389
|
+
* but RFC 8707 section 2 expects a client to name the resource at both steps,
|
|
1300
1390
|
* and the cloud rejects a *mismatched* resend outright (`invalid_target`).
|
|
1301
1391
|
* Omit it here exactly when it was omitted at `beginHostedLogin`.
|
|
1302
1392
|
*/
|
|
1303
1393
|
resource?: string;
|
|
1304
1394
|
}
|
|
1305
1395
|
/**
|
|
1306
|
-
* What `logout()` resolves with
|
|
1307
|
-
*
|
|
1396
|
+
* What `logout()` resolves with — five separable facts, not one nullable
|
|
1397
|
+
* URL, mirroring the wire's `clientLogoutResponse`.
|
|
1398
|
+
*
|
|
1399
|
+
* **`idp_logout` is `null` whenever the server never answered**, which
|
|
1400
|
+
* happens for two different reasons: there was no local session to ask
|
|
1401
|
+
* about, or the request itself failed. Either way there is nothing to
|
|
1402
|
+
* report — not even "not federated", because this client never learned that
|
|
1403
|
+
* either.
|
|
1308
1404
|
*
|
|
1309
|
-
* `
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
* one
|
|
1405
|
+
* `revoked` does not identify which of the two you got. It is `false` only
|
|
1406
|
+
* for the failed request; **a logout with no local session at all resolves
|
|
1407
|
+
* `{ revoked: true, idp_logout: null }`**, since nothing lingers
|
|
1408
|
+
* server-side and that counts as revoked. So `revoked: false` does imply
|
|
1409
|
+
* `idp_logout: null`, and the converse does not hold. Do not read the two
|
|
1410
|
+
* fields as one bit.
|
|
1411
|
+
*
|
|
1412
|
+
* When a real response did come back, `revoked` is `true` and `idp_logout`
|
|
1413
|
+
* is one of:
|
|
1315
1414
|
*
|
|
1316
1415
|
* - `{ status: 'redirect', url }` — send the browser here to end the
|
|
1317
1416
|
* session at the IdP too. Nothing else in this SDK does that navigation
|
|
@@ -1334,27 +1433,61 @@ interface CompleteHostedLoginOptions {
|
|
|
1334
1433
|
* isn't.
|
|
1335
1434
|
*
|
|
1336
1435
|
* **`session_unknown` is a different kind of nothing, and the reason it
|
|
1337
|
-
* exists is a second logout
|
|
1436
|
+
* exists is a second logout.** A double click, a repeated POST, an
|
|
1338
1437
|
* app that logs out on unmount *and* on a route change: the last call wins,
|
|
1339
1438
|
* and before this outcome existed it answered `not_federated` — a positive
|
|
1340
1439
|
* claim about an IdP the server had never looked up. An app that treats it as
|
|
1341
1440
|
* *"fully logged out"* skips a redirect the **first** call may well have
|
|
1342
1441
|
* returned, and the user stays signed in at the IdP after clicking log out.
|
|
1343
1442
|
*
|
|
1344
|
-
* Note that `revoked` cannot help you here: it reports whether the
|
|
1345
|
-
* succeeded, not whether a session was found — so this case
|
|
1346
|
-
* `{ revoked: true, idp_logout: { status: 'session_unknown' } }`,
|
|
1347
|
-
* `null
|
|
1443
|
+
* Note that `revoked` cannot help you here either: it reports whether the
|
|
1444
|
+
* HTTP call succeeded, not whether a session was found — so this case
|
|
1445
|
+
* arrives as `{ revoked: true, idp_logout: { status: 'session_unknown' } }`,
|
|
1446
|
+
* never as `null`.
|
|
1348
1447
|
*/
|
|
1349
1448
|
interface LogoutResult {
|
|
1449
|
+
/**
|
|
1450
|
+
* Whether the server-side revoke actually happened. It reports the fate
|
|
1451
|
+
* of the HTTP call, not whether a session was found — `false` means the
|
|
1452
|
+
* refresh family may still be alive even though this client has
|
|
1453
|
+
* forgotten it.
|
|
1454
|
+
*/
|
|
1350
1455
|
revoked: boolean;
|
|
1456
|
+
/**
|
|
1457
|
+
* What the server could say about the identity provider behind this
|
|
1458
|
+
* session, or `null` when there was no answer to report at all — either
|
|
1459
|
+
* because there was no local session to ask about, or because the request
|
|
1460
|
+
* failed. `null` is **not** the same as `not_federated`, which is a real
|
|
1461
|
+
* finding about a real session.
|
|
1462
|
+
*
|
|
1463
|
+
* When it is not `null` it is one of five statuses: `redirect` (with a
|
|
1464
|
+
* `url` to send the browser to, to end the session at the identity
|
|
1465
|
+
* provider too), `not_federated` (this session never came from one),
|
|
1466
|
+
* `unsupported_by_idp` (it did, and the provider offers no
|
|
1467
|
+
* RP-initiated logout), `hint_unavailable` (it did, the provider can end
|
|
1468
|
+
* the session, and Fleetless has nothing to ask it with), or
|
|
1469
|
+
* `session_unknown` (the server did not find this session, so it can say
|
|
1470
|
+
* nothing about a provider).
|
|
1471
|
+
*
|
|
1472
|
+
* **`unsupported_by_idp` and `hint_unavailable` both mean the identity
|
|
1473
|
+
* provider's session survives and this platform cannot end it.** Render
|
|
1474
|
+
* either one like `not_federated` and you report a session as fully ended
|
|
1475
|
+
* when it is not.
|
|
1476
|
+
*/
|
|
1351
1477
|
idp_logout: ClientLogoutResponse['idp_logout'] | null;
|
|
1352
1478
|
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Who the caller is, reachable as `client.auth`. A client built with a
|
|
1481
|
+
* `tokenStore` uses the full surface. One built with a `serverKey` already
|
|
1482
|
+
* has an identity and no user session, so **`me()` is the only method it
|
|
1483
|
+
* can call** — `login`, `beginHostedLogin`, `completeHostedLogin`,
|
|
1484
|
+
* `logout`, `changePassword` and `passwordResetUrl` all throw on one.
|
|
1485
|
+
*/
|
|
1353
1486
|
interface AuthApi {
|
|
1354
1487
|
/** Exchanges email + password, and the client's configured app identifier, for a session. */
|
|
1355
1488
|
login(email: string, password: string): Promise<void>;
|
|
1356
1489
|
/**
|
|
1357
|
-
* Starts the hosted login flow
|
|
1490
|
+
* Starts the hosted login flow: a Fleetless-served
|
|
1358
1491
|
* login page an app's end user is redirected to, with optional
|
|
1359
1492
|
* per-app IdP federation. Builds the `/oauth/authorize` URL (Authorization
|
|
1360
1493
|
* Code + PKCE, S256 only — OAuth 2.1 removes `plain`) and generates the
|
|
@@ -1386,12 +1519,12 @@ interface AuthApi {
|
|
|
1386
1519
|
*
|
|
1387
1520
|
* Once past that check and the exchange completes, `me()`, `logout()`,
|
|
1388
1521
|
* `changePassword()` and silent refresh all behave identically afterwards,
|
|
1389
|
-
* regardless of which flow the session started from.
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1522
|
+
* regardless of which flow the session started from. Both routes into a
|
|
1523
|
+
* session end at the same Fleetless token: not merely that the bytes
|
|
1524
|
+
* match, but that every existing code path treats the result the same
|
|
1525
|
+
* way.
|
|
1393
1526
|
*
|
|
1394
|
-
* The wire response is RFC 6749
|
|
1527
|
+
* The wire response is the envelope of RFC 6749 section 5.1 (`token_type`, optional
|
|
1395
1528
|
* `scope`), not `sessionTokens` — this method normalizes one into the
|
|
1396
1529
|
* other before storing. **Refresh needs no separate handling**: the cloud
|
|
1397
1530
|
* mints these tokens through the same session mechanism `/api/client/login`
|
|
@@ -1403,7 +1536,7 @@ interface AuthApi {
|
|
|
1403
1536
|
* Throws with the OAuth error code as `.code` (e.g. `invalid_grant` for an
|
|
1404
1537
|
* expired or already-used `code`) if the exchange itself fails — a
|
|
1405
1538
|
* different vocabulary from every other method on this interface, because
|
|
1406
|
-
* `/oauth/token` answers in RFC 6749
|
|
1539
|
+
* `/oauth/token` answers in the shape of RFC 6749 section 5.2, not `apiError`.
|
|
1407
1540
|
*
|
|
1408
1541
|
* **Makes exactly one request to `/oauth/token` — never retried, no
|
|
1409
1542
|
* timeout-and-resend, no internal concurrency of its own.** Stated
|
|
@@ -1412,16 +1545,16 @@ interface AuthApi {
|
|
|
1412
1545
|
* while a first call is still in flight** (a plain "the first attempt
|
|
1413
1546
|
* looked like it timed out, so retry" is exactly the shape this warns
|
|
1414
1547
|
* against — it is not a defect in this method, since this method itself
|
|
1415
|
-
* has nothing that could ever cause that).
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
*
|
|
1548
|
+
* has nothing that could ever cause that). The platform treats a second
|
|
1549
|
+
* presentation of an authorization code as theft and revokes the whole
|
|
1550
|
+
* token family it belongs to, deliberately, even though a plain
|
|
1551
|
+
* double-submission looks identical on the wire —
|
|
1419
1552
|
* because the blast radius is bounded (only a caller already holding the
|
|
1420
1553
|
* correct `code_verifier` and `client_id` can trigger it, so a merely
|
|
1421
1554
|
* *sniffed* code cannot lock anyone out) and the alternative is a
|
|
1422
1555
|
* narrower defence against a real theft.
|
|
1423
1556
|
*
|
|
1424
|
-
* **What actually happens if two requests race
|
|
1557
|
+
* **What actually happens if two requests race, measured:**
|
|
1425
1558
|
* one of the two receives `200` with a refresh token that the server has
|
|
1426
1559
|
* already revoked. The access token in that same response keeps working
|
|
1427
1560
|
* normally for the rest of its short TTL — nothing about the race is
|
|
@@ -1459,8 +1592,8 @@ interface AuthApi {
|
|
|
1459
1592
|
* request without a fresh token), not a bug — but a kiosk or shared
|
|
1460
1593
|
* workstation needs to know that number.
|
|
1461
1594
|
*
|
|
1462
|
-
* **Nor does it end a federated session at the identity provider
|
|
1463
|
-
*
|
|
1595
|
+
* **Nor does it end a federated session at the identity provider.**
|
|
1596
|
+
* Ending the Fleetless session and ending the IdP session are
|
|
1464
1597
|
* two different things — see `LogoutResult.idp_logout`. This method does
|
|
1465
1598
|
* not act on that information itself (no redirect, no fetch to the IdP);
|
|
1466
1599
|
* it only reports what the server found, the same "this SDK stays thin"
|
|
@@ -1470,30 +1603,22 @@ interface AuthApi {
|
|
|
1470
1603
|
* **`idp_logout` is `null` when there was no server answer to report** —
|
|
1471
1604
|
* and that is *not* the same as `revoked === false`.
|
|
1472
1605
|
*
|
|
1473
|
-
*
|
|
1474
|
-
* `revoked
|
|
1475
|
-
*
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
* so this counts as revoked — but there is no IdP fact to report either"* —
|
|
1479
|
-
* so the public docblock and the code contradicted each other in one file.
|
|
1480
|
-
*
|
|
1481
|
-
* That mattered more than a wrong sentence usually does, because **this is
|
|
1482
|
-
* the rule the text tells a caller to branch on**. Someone who reads
|
|
1483
|
-
* `revoked === true` and trusts the promise does not expect `null`.
|
|
1484
|
-
* TypeScript catches it (the type is `| null`); a JavaScript caller does
|
|
1485
|
-
* not.
|
|
1606
|
+
* Calling `logout()` with **no local session** returns
|
|
1607
|
+
* `{ revoked: true, idp_logout: null }`, so `null` is not the same as
|
|
1608
|
+
* `revoked === false` and the two fields are not one bit. TypeScript
|
|
1609
|
+
* catches a caller who forgets (the type is `| null`); a JavaScript caller
|
|
1610
|
+
* does not.
|
|
1486
1611
|
*
|
|
1487
1612
|
* So: **`null` means this client had nothing to send or the request never
|
|
1488
1613
|
* answered.** It is not `not_federated`, which is a real finding about a
|
|
1489
1614
|
* real session — conflating the two is the ambiguity this shape exists to
|
|
1490
|
-
* remove, and it is
|
|
1615
|
+
* remove, and it is the reason to read both fields rather than one.
|
|
1491
1616
|
*/
|
|
1492
1617
|
logout(): Promise<LogoutResult>;
|
|
1493
1618
|
/** Who the caller turned out to be, without decoding a token client-side. */
|
|
1494
1619
|
me(): Promise<ClientIdentity>;
|
|
1495
1620
|
/**
|
|
1496
|
-
* Changes the current end user's password
|
|
1621
|
+
* Changes the current end user's password.
|
|
1497
1622
|
*
|
|
1498
1623
|
* `currentPassword` is required by the server even though the session
|
|
1499
1624
|
* already proves identity — it is what stops a stolen *session* from
|
|
@@ -1531,7 +1656,7 @@ interface AuthApi {
|
|
|
1531
1656
|
}
|
|
1532
1657
|
|
|
1533
1658
|
/**
|
|
1534
|
-
* The snapshot's metadata alone, without the bytes
|
|
1659
|
+
* The snapshot's metadata alone, without the bytes. All fields
|
|
1535
1660
|
* are `null` together when nothing has been captured yet for this camera —
|
|
1536
1661
|
* a fresh configuration before the first `snapshot_interval_ms` elapses, say
|
|
1537
1662
|
* — which is a state, not a failure: the wire answers it with
|
|
@@ -1546,36 +1671,49 @@ interface AuthApi {
|
|
|
1546
1671
|
* `SNAPSHOT_HEADERS`'s doc comment in `@fleetless/contracts`).
|
|
1547
1672
|
*/
|
|
1548
1673
|
interface CameraSnapshotMeta {
|
|
1674
|
+
/** The image's media type, e.g. `image/jpeg`. */
|
|
1549
1675
|
mime: string | null;
|
|
1676
|
+
/** The image's width in pixels. */
|
|
1550
1677
|
width: number | null;
|
|
1678
|
+
/** The image's height in pixels. */
|
|
1551
1679
|
height: number | null;
|
|
1552
|
-
/** The bridge's capture time
|
|
1680
|
+
/** The bridge's capture time, in unix milliseconds — when the frame was taken, not when it was served. */
|
|
1553
1681
|
timestamp_ms: number | null;
|
|
1682
|
+
/** How long the cloud has held this frame, in milliseconds. The one figure to read for freshness. */
|
|
1554
1683
|
age_ms: number | null;
|
|
1555
1684
|
}
|
|
1556
|
-
/** One snapshot read: the image bytes plus everything needed to state how old they are
|
|
1685
|
+
/** One snapshot read: the image bytes plus everything needed to state how old they are. */
|
|
1557
1686
|
interface CameraSnapshot extends CameraSnapshotMeta {
|
|
1687
|
+
/** The encoded image, or `null` when nothing has been captured yet. */
|
|
1558
1688
|
image: Uint8Array | null;
|
|
1559
1689
|
}
|
|
1560
1690
|
/**
|
|
1561
|
-
* What a LiveKit client needs to join, plus the means to leave
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1691
|
+
* What a LiveKit client needs to join, plus the means to leave. Returned by
|
|
1692
|
+
* `cameras.live`. Hand `url` and `token` straight to a LiveKit client SDK
|
|
1693
|
+
* (e.g. `Room.connect(url, token)`) — this SDK stops there on purpose: no
|
|
1694
|
+
* video widget and no teleop-style helper, so that the app owns how the
|
|
1695
|
+
* video is presented.
|
|
1566
1696
|
*/
|
|
1567
1697
|
interface CameraLiveSession {
|
|
1568
1698
|
/**
|
|
1569
|
-
* This viewer's own hold
|
|
1699
|
+
* This viewer's own hold — what `release()` releases, and the only
|
|
1570
1700
|
* thing distinguishing this session from every other tab of the same
|
|
1571
|
-
* identity watching the same camera.
|
|
1572
|
-
* with no id released **all** of this identity's holds on the slug, so one
|
|
1573
|
-
* tab closing stopped the robot for every other tab too. See `release()`'s
|
|
1574
|
-
* doc comment for what changed and what did not.
|
|
1701
|
+
* identity watching the same camera.
|
|
1575
1702
|
*/
|
|
1576
1703
|
session_id: string;
|
|
1704
|
+
/** The LiveKit server URL to connect to. */
|
|
1577
1705
|
url: string;
|
|
1706
|
+
/** The LiveKit room this session joins. */
|
|
1578
1707
|
room: string;
|
|
1708
|
+
/**
|
|
1709
|
+
* The LiveKit access token for this session. It is checked when the
|
|
1710
|
+
* participant connects and not again afterwards, so it bounds *joining*,
|
|
1711
|
+
* not the session: a viewer who has already joined keeps receiving video
|
|
1712
|
+
* past `expires_at`. What ends a joined session is `release()` together
|
|
1713
|
+
* with disconnecting the room, the cloud reconciling the hold away
|
|
1714
|
+
* against LiveKit's real participants, or a revocation — a membership,
|
|
1715
|
+
* role or key change — kicking the participant out.
|
|
1716
|
+
*/
|
|
1579
1717
|
token: string;
|
|
1580
1718
|
/**
|
|
1581
1719
|
* When this token can no longer be used to **join** — not when an
|
|
@@ -1594,14 +1732,10 @@ interface CameraLiveSession {
|
|
|
1594
1732
|
expires_at: string;
|
|
1595
1733
|
/**
|
|
1596
1734
|
* Tells the cloud this viewer no longer wants to hold the camera live —
|
|
1597
|
-
* **this** hold, addressed by `session_id
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1601
|
-
* stopped the robot out from under every other tab of the same logged-in
|
|
1602
|
-
* user, which kept rendering a frozen frame because a LiveKit token is
|
|
1603
|
-
* checked at join and never again. Each `CameraLiveSession` now releases
|
|
1604
|
-
* only the hold it itself took.
|
|
1735
|
+
* **this** hold, addressed by `session_id`, and no other tab's. Each
|
|
1736
|
+
* `CameraLiveSession` releases only the hold it itself took, so one tab's
|
|
1737
|
+
* cleanup never stops the robot out from under another tab of the same
|
|
1738
|
+
* logged-in user.
|
|
1605
1739
|
*
|
|
1606
1740
|
* **This alone does not stop the stream.** The cloud makes LiveKit room
|
|
1607
1741
|
* participation the authoritative refcount, not this call — precisely
|
|
@@ -1622,7 +1756,7 @@ interface CameraLiveSession {
|
|
|
1622
1756
|
*
|
|
1623
1757
|
* **A failed DELETE here is not observable anywhere** — not as a
|
|
1624
1758
|
* rejection, not as a realtime event, not as a field on this object. This
|
|
1625
|
-
* is a deliberate decision, not an oversight
|
|
1759
|
+
* is a deliberate decision, not an oversight: the only
|
|
1626
1760
|
* consumer of that information would be code deciding whether to retry,
|
|
1627
1761
|
* and the backstop this comment already describes — the cloud's own
|
|
1628
1762
|
* LiveKit-participation reconciliation — makes a retry unnecessary for
|
|
@@ -1632,13 +1766,18 @@ interface CameraLiveSession {
|
|
|
1632
1766
|
*/
|
|
1633
1767
|
release(): Promise<void>;
|
|
1634
1768
|
}
|
|
1769
|
+
/**
|
|
1770
|
+
* A robot's cameras, reachable as `client.cameras`: what exists, the latest
|
|
1771
|
+
* still frame, and a live video session. All of it is REST — no realtime
|
|
1772
|
+
* channel is involved.
|
|
1773
|
+
*/
|
|
1635
1774
|
interface CamerasApi {
|
|
1636
|
-
/** Every camera exposed on this robot
|
|
1775
|
+
/** Every camera exposed on this robot, as descriptors — the same per-robot list the other kinds use. */
|
|
1637
1776
|
list(robotId: string): Promise<CameraDescriptor[]>;
|
|
1638
1777
|
/**
|
|
1639
1778
|
* The current snapshot: image bytes plus its age. Independent of `live` —
|
|
1640
1779
|
* a snapshot keeps updating on `snapshot_interval_ms` whether or not
|
|
1641
|
-
* anyone is watching live
|
|
1780
|
+
* anyone is watching live, and keeps being served, with a growing
|
|
1642
1781
|
* age, even while the bridge is offline.
|
|
1643
1782
|
*/
|
|
1644
1783
|
snapshot(robotId: string, slug: string): Promise<CameraSnapshot>;
|
|
@@ -1650,30 +1789,44 @@ interface CamerasApi {
|
|
|
1650
1789
|
*/
|
|
1651
1790
|
snapshotMeta(robotId: string, slug: string): Promise<CameraSnapshotMeta>;
|
|
1652
1791
|
/**
|
|
1653
|
-
* Takes a refcounted hold on this camera's live stream
|
|
1792
|
+
* Takes a refcounted hold on this camera's live stream: the
|
|
1654
1793
|
* first `live()` on a slug starts the robot publishing, the last viewer
|
|
1655
1794
|
* leaving stops it. Deliberately not deduplicated locally across multiple
|
|
1656
1795
|
* `live()` calls for the same `(robotId, slug)` — unlike a datapoint
|
|
1657
1796
|
* subscription, each call needs its own distinct LiveKit participant, so
|
|
1658
|
-
* a local counter here would just be the same shared-count bug
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1797
|
+
* a local counter here would just be the same shared-count bug the
|
|
1798
|
+
* subscription layer already fixed, self-inflicted on a resource the
|
|
1799
|
+
* cloud counts correctly on its own.
|
|
1661
1800
|
*/
|
|
1662
1801
|
live(robotId: string, slug: string): Promise<CameraLiveSession>;
|
|
1663
1802
|
}
|
|
1664
1803
|
|
|
1804
|
+
/**
|
|
1805
|
+
* The callbacks `datapoints.subscribe` reports through: one for values, one
|
|
1806
|
+
* for a refusal. `onEvent` fires immediately with the current value and
|
|
1807
|
+
* again on every change.
|
|
1808
|
+
*/
|
|
1665
1809
|
interface DatapointSubscriptionHandlers {
|
|
1666
1810
|
/** Called with the current value on subscribe, then again on every change. */
|
|
1667
1811
|
onEvent(event: DatapointEvent): void;
|
|
1668
|
-
/** Called once if the subscription is refused
|
|
1812
|
+
/** Called once if the subscription is refused, e.g. `forbidden` or `unknown_datapoint`. */
|
|
1669
1813
|
onError?(error: FleetlessError): void;
|
|
1670
1814
|
}
|
|
1815
|
+
/** A live datapoint subscription, returned by `datapoints.subscribe`. */
|
|
1671
1816
|
interface DatapointSubscription {
|
|
1672
|
-
/**
|
|
1817
|
+
/**
|
|
1818
|
+
* Stops this subscription. The `unsubscribe` frame reaches the server only
|
|
1819
|
+
* when this was the **last** holder of the robot and slug pair, and only if
|
|
1820
|
+
* the channel is connected — subscriptions are reference-counted across
|
|
1821
|
+
* kinds, so releasing this one while a second `datapoints.subscribe`, an
|
|
1822
|
+
* `actions.subscribe` or an in-flight `services.call` still holds the same
|
|
1823
|
+
* pair leaves that one's stream running untouched. Safe to call more than
|
|
1824
|
+
* once.
|
|
1825
|
+
*/
|
|
1673
1826
|
unsubscribe(): void;
|
|
1674
1827
|
}
|
|
1675
1828
|
/**
|
|
1676
|
-
* Window aggregation for `history
|
|
1829
|
+
* Window aggregation for `datapoints.history`. `window` and `agg` always
|
|
1677
1830
|
* travel together on the wire — the cloud refuses one without the other
|
|
1678
1831
|
* rather than defaulting either, since a silently chosen aggregation is a
|
|
1679
1832
|
* chart that lies quietly — so they live in one object here instead of two
|
|
@@ -1685,10 +1838,16 @@ interface DatapointSubscription {
|
|
|
1685
1838
|
interface HistoryAggregation {
|
|
1686
1839
|
/** Bucket width, e.g. `10s`, `1m`. */
|
|
1687
1840
|
window: string;
|
|
1841
|
+
/** How to reduce each bucket's samples to one number. */
|
|
1688
1842
|
agg: 'min' | 'max' | 'avg';
|
|
1689
|
-
/** A numeric field inside an object value, e.g. `pose.x
|
|
1843
|
+
/** A numeric field inside an object value, e.g. `pose.x`. Only meaningful when the datapoint's own value is not itself a number. */
|
|
1690
1844
|
field?: string;
|
|
1691
1845
|
}
|
|
1846
|
+
/**
|
|
1847
|
+
* The window `datapoints.history` reads, and whether it comes back as raw
|
|
1848
|
+
* samples or as aggregated buckets. `aggregate` is what decides which of
|
|
1849
|
+
* the two responses you get.
|
|
1850
|
+
*/
|
|
1692
1851
|
interface HistoryOptions {
|
|
1693
1852
|
/**
|
|
1694
1853
|
* `now-30s` / `now-5m` / `now-1h`, or absolute unix milliseconds — as a
|
|
@@ -1701,11 +1860,22 @@ interface HistoryOptions {
|
|
|
1701
1860
|
from: string;
|
|
1702
1861
|
/** Same two forms as `from`. Defaults to now. */
|
|
1703
1862
|
to?: string;
|
|
1863
|
+
/** The most rows to return. The platform applies its own ceiling regardless. */
|
|
1704
1864
|
limit?: number;
|
|
1705
1865
|
/** Present: the result is aggregated buckets. Absent: raw samples. */
|
|
1706
1866
|
aggregate?: HistoryAggregation;
|
|
1707
1867
|
}
|
|
1868
|
+
/**
|
|
1869
|
+
* A robot's exposed values, reachable as `client.datapoints`: the latest
|
|
1870
|
+
* one, a live subscription to it, and — for a datapoint configured with
|
|
1871
|
+
* retention — its recorded history.
|
|
1872
|
+
*/
|
|
1708
1873
|
interface DatapointsApi {
|
|
1874
|
+
/**
|
|
1875
|
+
* Reads the datapoint's latest value over REST, once. It carries the
|
|
1876
|
+
* bridge's own capture time, so a caller can tell a fresh value from a
|
|
1877
|
+
* stale one without a subscription.
|
|
1878
|
+
*/
|
|
1709
1879
|
get(robotId: string, slug: string): Promise<DatapointValue>;
|
|
1710
1880
|
/**
|
|
1711
1881
|
* Subscribes over the realtime channel. Reconnect and re-authentication
|
|
@@ -1718,25 +1888,27 @@ interface DatapointsApi {
|
|
|
1718
1888
|
* other — the `unsubscribe` frame is sent only when the last subscriber
|
|
1719
1889
|
* on that pair goes away. This matters in practice: two widgets showing
|
|
1720
1890
|
* the same battery value, or a component mounted twice under React
|
|
1721
|
-
* StrictMode, both subscribe to the same key.
|
|
1722
|
-
* `
|
|
1723
|
-
*
|
|
1891
|
+
* StrictMode, both subscribe to the same key. That count is shared with
|
|
1892
|
+
* `actions.subscribe` and `services.call` — a slug is one namespace across
|
|
1893
|
+
* kinds, and so is its subscription.
|
|
1724
1894
|
*/
|
|
1725
1895
|
subscribe(robotId: string, slug: string, handlers: DatapointSubscriptionHandlers): DatapointSubscription;
|
|
1726
1896
|
/**
|
|
1727
|
-
* Reads recorded history for a `retention: true` datapoint
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1732
|
-
*
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1897
|
+
* Reads recorded history for a `retention: true` datapoint over REST — no
|
|
1898
|
+
* realtime channel involved, the same way `cameras.snapshot` isn't. **This
|
|
1899
|
+
* overload is the aggregated one:** `aggregate` is given, so it resolves
|
|
1900
|
+
* with `HistoryBucketsResponse` (`kind: 'buckets'`) — one row per window,
|
|
1901
|
+
* reduced by `aggregate.agg`. Leave `aggregate` out and the other overload
|
|
1902
|
+
* gives you raw samples instead.
|
|
1903
|
+
*
|
|
1904
|
+
* Two overloads rather than one union so a caller who already knows which
|
|
1905
|
+
* they asked for isn't forced to narrow something they determined
|
|
1906
|
+
* themselves. `kind` still carries the same information on both, so code
|
|
1907
|
+
* holding the result dynamically can still branch on it.
|
|
1736
1908
|
*
|
|
1737
1909
|
* **Rejects, does not silently empty out, two specific refusals** —
|
|
1738
1910
|
* unlike `cameras.snapshot`'s absorption of `no_snapshot_yet` into a null
|
|
1739
|
-
* read, these two must reach the caller as
|
|
1911
|
+
* read, these two must reach the caller as a rejected `FleetlessError`:
|
|
1740
1912
|
* - `not_recorded` — the slug exists and is granted, but is configured
|
|
1741
1913
|
* live-only. An empty result here would look exactly like "recorded,
|
|
1742
1914
|
* but nothing in this window", and the two need opposite fixes: turn
|
|
@@ -1747,20 +1919,34 @@ interface DatapointsApi {
|
|
|
1747
1919
|
history(robotId: string, slug: string, options: HistoryOptions & {
|
|
1748
1920
|
aggregate: HistoryAggregation;
|
|
1749
1921
|
}): Promise<HistoryBucketsResponse>;
|
|
1922
|
+
/**
|
|
1923
|
+
* The same read without `aggregate`: resolves with
|
|
1924
|
+
* `HistorySamplesResponse` (`kind: 'samples'`), every recorded sample in
|
|
1925
|
+
* the window as a `timestamp_ms` and a `value`. `timestamp_ms` is the
|
|
1926
|
+
* bridge's own capture time, the same instant the live value carried, so a
|
|
1927
|
+
* recorded point and a live one sit on one axis without apology.
|
|
1928
|
+
*
|
|
1929
|
+
* **Read `truncated`.** The platform caps how much one read returns, by
|
|
1930
|
+
* row count or by bytes, and `truncated_by` says which. A short array that
|
|
1931
|
+
* does not admit it is indistinguishable from a quiet period, and the two
|
|
1932
|
+
* lead to opposite conclusions. Refuses `not_recorded` the same way the
|
|
1933
|
+
* aggregated overload does.
|
|
1934
|
+
*/
|
|
1750
1935
|
history(robotId: string, slug: string, options: HistoryOptions & {
|
|
1751
1936
|
aggregate?: undefined;
|
|
1752
1937
|
}): Promise<HistorySamplesResponse>;
|
|
1753
1938
|
}
|
|
1754
1939
|
|
|
1755
1940
|
/**
|
|
1756
|
-
* An end user's own consent grants
|
|
1941
|
+
* An end user's own consent grants, reachable as `client.grants` — every
|
|
1757
1942
|
* client this identity has ever authorized, and the means to take one back
|
|
1758
1943
|
* without touching any of the others.
|
|
1759
1944
|
*
|
|
1760
1945
|
* **End-user only.** A server key acts with the app's own full rights and
|
|
1761
1946
|
* never went through a consent screen itself — there is no "self" here for
|
|
1762
|
-
* it to list or revoke, same reasoning as `auth.login
|
|
1763
|
-
*
|
|
1947
|
+
* it to list or revoke, same reasoning as `auth.login` on a `serverKey`
|
|
1948
|
+
* client. An end user reaches a group by invitation and signs in through
|
|
1949
|
+
* `auth.login` or the hosted login; a server key never does either.
|
|
1764
1950
|
*/
|
|
1765
1951
|
interface GrantsApi {
|
|
1766
1952
|
/**
|
|
@@ -1769,9 +1955,9 @@ interface GrantsApi {
|
|
|
1769
1955
|
* `role_name`) — the id is what `revoke()` addresses, but a person
|
|
1770
1956
|
* deciding whether to revoke something needs to *recognise* it first, and
|
|
1771
1957
|
* an id is not recognisable. **`client_name` is not trusted** — a
|
|
1772
|
-
* self-registered client chooses its own display name
|
|
1773
|
-
*
|
|
1774
|
-
*
|
|
1958
|
+
* self-registered client chooses its own display name, and one has
|
|
1959
|
+
* already been measured calling itself "Fleetless Official Helper" — do
|
|
1960
|
+
* not render it as if Fleetless vouched for it.
|
|
1775
1961
|
*/
|
|
1776
1962
|
list(): Promise<ConsentGrantSummary[]>;
|
|
1777
1963
|
/**
|
|
@@ -1786,9 +1972,8 @@ interface GrantsApi {
|
|
|
1786
1972
|
* about access tokens, which are stateless and short-lived by design.
|
|
1787
1973
|
*
|
|
1788
1974
|
* **What actually happens to an already-issued access token is stronger
|
|
1789
|
-
* than that count implies
|
|
1790
|
-
*
|
|
1791
|
-
* cloud).** It does not simply expire on its own: the cloud re-checks
|
|
1975
|
+
* than that count implies.** It does not simply expire on its own: the
|
|
1976
|
+
* cloud re-checks
|
|
1792
1977
|
* every request for a revoked grant, so **any** currently-valid access
|
|
1793
1978
|
* token minted through this same client's OAuth flow for this end user
|
|
1794
1979
|
* — not only the one used to call `revoke()` — answers `401
|
|
@@ -1804,18 +1989,22 @@ interface GrantsApi {
|
|
|
1804
1989
|
revoke(clientId: string): Promise<ConsentRevokeResponse>;
|
|
1805
1990
|
}
|
|
1806
1991
|
|
|
1992
|
+
/**
|
|
1993
|
+
* Robot-wide job reads, reachable as `client.jobs`. Everything here is
|
|
1994
|
+
* addressed by robot rather than by slug, which is what `actions` and
|
|
1995
|
+
* `services` cannot do.
|
|
1996
|
+
*/
|
|
1807
1997
|
interface JobsApi {
|
|
1808
1998
|
/**
|
|
1809
|
-
* Every job the platform currently believes this robot has
|
|
1810
|
-
* /api/robots/:id/jobs` (W6b, contracts `robotJobsResponse` doc comment).
|
|
1999
|
+
* Every job the platform currently believes this robot has.
|
|
1811
2000
|
*
|
|
1812
2001
|
* `actions.subscribe`/`services.call` and `GET /jobs/:slug` (the per-slug
|
|
1813
2002
|
* route those build on) all require already knowing the slug. That is not
|
|
1814
2003
|
* always true: a reconnecting bridge can name a job the cloud only
|
|
1815
2004
|
* *adopted*, and a configuration change can leave a job on a slug the
|
|
1816
2005
|
* published document no longer contains. Both are jobs no slug can name,
|
|
1817
|
-
* which is exactly what this method is for
|
|
1818
|
-
*
|
|
2006
|
+
* which is exactly what this method is for — an app developer has no
|
|
2007
|
+
* other way to reach them.
|
|
1819
2008
|
*
|
|
1820
2009
|
* At most one entry per slug: the current job there, exactly what a
|
|
1821
2010
|
* per-slug read would answer for that slug. Not a history endpoint.
|
|
@@ -1832,18 +2021,22 @@ interface JobsApi {
|
|
|
1832
2021
|
* `hello`, having never minted it, and has no other honest value to put
|
|
1833
2022
|
* there. So this is newest-*known*-first: a job the robot has been
|
|
1834
2023
|
* running for an hour can sit above one started a minute ago, if the
|
|
1835
|
-
* hour-long one was only just adopted
|
|
1836
|
-
* comment).
|
|
2024
|
+
* hour-long one was only just adopted.
|
|
1837
2025
|
*/
|
|
1838
2026
|
list(robotId: string): Promise<Job[]>;
|
|
1839
2027
|
}
|
|
1840
2028
|
|
|
2029
|
+
/**
|
|
2030
|
+
* One-way messages to a robot's publishers, reachable as
|
|
2031
|
+
* `client.publishers` — a velocity command, a goal pose, anything the
|
|
2032
|
+
* developer exposed as a publisher.
|
|
2033
|
+
*/
|
|
1841
2034
|
interface PublishersApi {
|
|
1842
2035
|
/**
|
|
1843
|
-
* Publishes one message to a publisher
|
|
2036
|
+
* Publishes one message to a publisher.
|
|
1844
2037
|
*
|
|
1845
2038
|
* This is a plain method call — there is deliberately no deadman switch,
|
|
1846
|
-
* rate governor or "takt" helper here
|
|
2039
|
+
* rate governor or "takt" helper here. The bridge's own
|
|
1847
2040
|
* `timeout_ms` failsafe is the platform's safety primitive: if messages
|
|
1848
2041
|
* stop arriving — including because this process crashed — the bridge
|
|
1849
2042
|
* publishes the configured failsafe message itself. That does **not**
|
|
@@ -1854,45 +2047,54 @@ interface PublishersApi {
|
|
|
1854
2047
|
* building a publisher-driven control loop.
|
|
1855
2048
|
*
|
|
1856
2049
|
* Rejects `publisher_busy` while a different user is publishing and has
|
|
1857
|
-
* not been quiet for
|
|
2050
|
+
* not been quiet for its configured quiet timeout yet — whoever publishes
|
|
1858
2051
|
* holds the publisher implicitly exclusive.
|
|
1859
2052
|
*/
|
|
1860
2053
|
publish(robotId: string, slug: string, message: Record<string, unknown>, options?: SendCommandOptions): Promise<void>;
|
|
1861
2054
|
}
|
|
1862
2055
|
|
|
2056
|
+
/**
|
|
2057
|
+
* Request/response calls to a robot, reachable as `client.services`. A
|
|
2058
|
+
* service answers once and is done, which is why this namespace has a
|
|
2059
|
+
* single method and nothing to subscribe to.
|
|
2060
|
+
*/
|
|
1863
2061
|
interface ServicesApi {
|
|
1864
2062
|
/**
|
|
1865
|
-
* Calls a service and resolves with its result
|
|
1866
|
-
*
|
|
1867
|
-
*
|
|
1868
|
-
* call") — but that is deliberately invisible here: the caller gets a
|
|
2063
|
+
* Calls a service and resolves with its result. A service call is a job
|
|
2064
|
+
* underneath — the same `job_id` exchange and disconnect survival as an
|
|
2065
|
+
* action — but that is deliberately invisible here: the caller gets a
|
|
1869
2066
|
* plain `Promise<result>`, matching the REST `serviceCallResponse` shape's
|
|
1870
2067
|
* developer experience. There is nothing to subscribe to for a service —
|
|
1871
2068
|
* no feedback, no progress, no cancel — so this call already waits for
|
|
1872
2069
|
* the terminal state internally.
|
|
1873
2070
|
*
|
|
1874
|
-
* `options.patienceMs` bounds the **whole wait** for a service call
|
|
1875
|
-
*
|
|
2071
|
+
* `options.patienceMs` bounds the **whole wait** for a service call —
|
|
2072
|
+
* unlike an action, where it bounds acceptance only — because a service
|
|
1876
2073
|
* has no further state to observe once it settles; the platform gives up
|
|
1877
2074
|
* on the ROS call itself after this long.
|
|
1878
2075
|
*
|
|
1879
2076
|
* `options.timeoutMs` bounds this SDK's own local wait for the WHOLE
|
|
1880
2077
|
* call — the ack that a job was created, plus however much of the
|
|
1881
|
-
* budget is left for it to then reach a terminal state
|
|
2078
|
+
* budget is left for it to then reach a terminal state — not two
|
|
1882
2079
|
* separate `timeoutMs`-length windows back to back. A caller who sets
|
|
1883
2080
|
* `timeoutMs: 5000` is bounding total latency at ~5s, not ~10s; the
|
|
1884
2081
|
* number means what it says, once, for the whole call.
|
|
1885
2082
|
*
|
|
1886
|
-
* It is also **not independent** of `patienceMs
|
|
2083
|
+
* It is also **not independent** of `patienceMs`: left unset, it
|
|
1887
2084
|
* is derived from `patienceMs` so this SDK's local clock cannot fire
|
|
1888
2085
|
* before the platform's own deadline has even been reached. Setting
|
|
1889
|
-
* both, with `timeoutMs` shorter than `patienceMs`,
|
|
1890
|
-
* `invalid_option`
|
|
1891
|
-
*
|
|
2086
|
+
* both, with `timeoutMs` shorter than `patienceMs`, rejects with
|
|
2087
|
+
* `invalid_option` before any request is sent rather than letting the two
|
|
2088
|
+
* race — see `InvokeOptions.patienceMs` for the full reasoning.
|
|
1892
2089
|
*/
|
|
1893
2090
|
call(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<unknown>;
|
|
1894
2091
|
}
|
|
1895
2092
|
|
|
2093
|
+
/**
|
|
2094
|
+
* Everything `createClient` accepts. `apiUrl` and `appIdentifier` are
|
|
2095
|
+
* required; the rest either select the kind of caller (`tokenStore` versus
|
|
2096
|
+
* `serverKey`) or replace a global the SDK would otherwise reach for.
|
|
2097
|
+
*/
|
|
1896
2098
|
interface FleetlessClientOptions {
|
|
1897
2099
|
/** Base URL of the Fleetless REST API, e.g. `https://api.fleetless.dev`. */
|
|
1898
2100
|
apiUrl: string;
|
|
@@ -1918,27 +2120,48 @@ interface FleetlessClientOptions {
|
|
|
1918
2120
|
/** Defaults to `apiUrl` with http(s) swapped for ws(s) and `/realtime` appended. */
|
|
1919
2121
|
realtimeUrl?: string;
|
|
1920
2122
|
}
|
|
2123
|
+
/**
|
|
2124
|
+
* The settled configuration of a client, reachable as `client.config`. It
|
|
2125
|
+
* is frozen and reflects the defaults `createClient` filled in, which is
|
|
2126
|
+
* what makes it worth reading: `realtimeUrl` is usually derived rather than
|
|
2127
|
+
* passed.
|
|
2128
|
+
*/
|
|
1921
2129
|
interface FleetlessClientConfig {
|
|
2130
|
+
/** The REST base URL this client calls, exactly as passed to `createClient`. */
|
|
1922
2131
|
readonly apiUrl: string;
|
|
2132
|
+
/** The app this client acts as, exactly as passed to `createClient`. */
|
|
1923
2133
|
readonly appIdentifier: string;
|
|
2134
|
+
/** The realtime WebSocket URL in use, derived from `apiUrl` unless one was passed. */
|
|
1924
2135
|
readonly realtimeUrl: string;
|
|
1925
2136
|
}
|
|
2137
|
+
/**
|
|
2138
|
+
* One app's client, returned by `createClient`. Every API the SDK offers is
|
|
2139
|
+
* a property on it, and all of them share this client's identity, its
|
|
2140
|
+
* single realtime channel and its token refresh.
|
|
2141
|
+
*/
|
|
1926
2142
|
interface FleetlessClient {
|
|
2143
|
+
/** The settled configuration, including the defaults `createClient` filled in. */
|
|
1927
2144
|
readonly config: FleetlessClientConfig;
|
|
2145
|
+
/** Logging in, logging out, and reading who the caller currently is. */
|
|
1928
2146
|
readonly auth: AuthApi;
|
|
1929
|
-
/** An end user's own consent grants
|
|
2147
|
+
/** An end user's own consent grants — not available on a `serverKey` client, same reasoning as `auth`'s session-only methods. */
|
|
1930
2148
|
readonly grants: GrantsApi;
|
|
2149
|
+
/** A topic's latest value, a live subscription to it, and its recorded history. */
|
|
1931
2150
|
readonly datapoints: DatapointsApi;
|
|
2151
|
+
/** Long-running work on the robot: invoke, cancel, and watch a job as it runs. */
|
|
1932
2152
|
readonly actions: ActionsApi;
|
|
2153
|
+
/** Request/response calls to the robot that answer once and are done. */
|
|
1933
2154
|
readonly services: ServicesApi;
|
|
2155
|
+
/** One-way messages to a robot's publisher, such as a velocity command. */
|
|
1934
2156
|
readonly publishers: PublishersApi;
|
|
2157
|
+
/** Camera snapshots, their age, and live video sessions. */
|
|
1935
2158
|
readonly cameras: CamerasApi;
|
|
1936
2159
|
/**
|
|
1937
2160
|
* Robot-wide job reads that do not fit under `actions`/`services` because
|
|
1938
2161
|
* they are not addressed by slug — see `JobsApi.list`.
|
|
1939
2162
|
*/
|
|
1940
2163
|
readonly jobs: JobsApi;
|
|
1941
|
-
/** URDF
|
|
2164
|
+
/** URDF and mesh reads — list/get/urdf, plus the `urdf-loader` mesh callback. */
|
|
1942
2165
|
readonly assets: AssetsApi;
|
|
1943
2166
|
/**
|
|
1944
2167
|
* Closes the realtime channel and stops it from reconnecting. Safe to
|
|
@@ -1951,6 +2174,16 @@ interface FleetlessClient {
|
|
|
1951
2174
|
*/
|
|
1952
2175
|
close(): void;
|
|
1953
2176
|
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Builds a client for one app. Pass `tokenStore` (or nothing — the default
|
|
2179
|
+
* keeps the session in memory) for an end-user client that logs in with
|
|
2180
|
+
* `auth.login` or the hosted login; pass `serverKey` for a server-side
|
|
2181
|
+
* caller that never holds a user session. Passing both throws, because the
|
|
2182
|
+
* two are different identities and a client acts as exactly one.
|
|
2183
|
+
*
|
|
2184
|
+
* Nothing is fetched here: the realtime channel opens on the first
|
|
2185
|
+
* subscription and closes on `close()` or `auth.logout()`.
|
|
2186
|
+
*/
|
|
1954
2187
|
declare function createClient(options: FleetlessClientOptions): FleetlessClient;
|
|
1955
2188
|
|
|
1956
|
-
export { type ActionsApi, type Asset, type AssetBytes, type AssetListResponse, type AssetsApi, type AuthApi, type BeginHostedLoginOptions, type BusyDetails, type CameraDescriptor, type CameraLiveSession, type CameraSnapshot, type CameraSnapshotMeta, type CamerasApi, type ClientIdentity, type CompleteHostedLoginOptions, type ConsentGrantSummary, type ConsentRevokeResponse, type CreateMeshLoaderOptions, type DatapointEvent, type DatapointSubscription, type DatapointSubscriptionHandlers, type DatapointValue, type DatapointsApi, type FleetlessClient, type FleetlessClientConfig, type FleetlessClientOptions, FleetlessError, type FleetlessErrorCode, type FleetlessErrorOptions, type GrantsApi, type HistoryAggregation, type HistoryBucketsResponse, type HistoryOptions, type HistorySamplesResponse, type HostedLoginRequest, InMemoryTokenStore, type Job, type JobEvent, type JobState, type JobSubscription, type JobSubscriptionHandlers, type JobsApi, type LogoutResult, type MeshLoaderDelegate, type ParameterInvalidDetails, type ParameterViolation, type PrepareUrdfSceneOptions, type PublishersApi, type RateLimitDetails, SDK_ERROR_CODES, type SdkErrorCode, type SendCommandOptions, type ServicesApi, type StoredSession, type TokenStore, type UrdfCompleteness, type UrdfSceneManager, type UrdfSceneResources, createClient, parameterInvalidDetails };
|
|
2189
|
+
export { type ActionsApi, type Asset, type AssetBytes, type AssetListResponse, type AssetsApi, type AuthApi, type BeginHostedLoginOptions, type BusyDetails, type CameraDescriptor, type CameraLiveSession, type CameraSnapshot, type CameraSnapshotMeta, type CamerasApi, type ClientIdentity, type CompleteHostedLoginOptions, type ConsentGrantSummary, type ConsentRevokeResponse, type CreateMeshLoaderOptions, type DatapointEvent, type DatapointSubscription, type DatapointSubscriptionHandlers, type DatapointValue, type DatapointsApi, type FleetlessClient, type FleetlessClientConfig, type FleetlessClientOptions, FleetlessError, type FleetlessErrorCode, type FleetlessErrorOptions, type GrantsApi, type HistoryAggregation, type HistoryBucketsResponse, type HistoryOptions, type HistorySamplesResponse, type HostedLoginRequest, InMemoryTokenStore, type InvokeOptions, type Job, type JobEvent, type JobState, type JobSubscription, type JobSubscriptionHandlers, type JobsApi, type LogoutResult, type MeshLoaderDelegate, type ParameterInvalidDetails, type ParameterViolation, type PrepareUrdfSceneOptions, type PublishersApi, type RateLimitDetails, SDK_ERROR_CODES, type SdkErrorCode, type SendCommandOptions, type ServicesApi, type StoredSession, type TokenStore, type UrdfCompleteness, type UrdfSceneManager, type UrdfSceneResources, createClient, parameterInvalidDetails };
|