@irtio/client 0.7.0 → 0.9.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/dist/{chunk-XWVXZRBS.js → chunk-5OHONPVG.js} +142 -0
- package/dist/{chunk-DABQDR3S.js → chunk-VZFJXPVF.js} +7 -8
- package/dist/index.d.ts +439 -11
- package/dist/index.js +291 -8
- package/dist/{physics-RT5T36P5.js → physics-D65WIHPR.js} +2 -2
- package/dist/physics-rapier2d-O53E6BHO.js +186 -0
- package/dist/{physics2d-HOFMWPZV.js → physics2d-GIL6YYAY.js} +2 -2
- package/package.json +8 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AnySchema, PlainState, CollectionDesc, Delta, EntityCollection, PhysicsBodyChannel,
|
|
1
|
+
import { AnySchema, PlainState, CollectionDesc, Delta, EntityCollection, PhysicsBodyChannel, ClientImplementations, ClientRpcs, SchemaRpc, RoleOf, VisibleKeys, SchemaDefs, EntityDef, ReadonlyCollection, DeepReadonly, InferFields, Owned, SingletonDef, ClientCallProxy, MessageChannels } from '@irtio/schema';
|
|
2
2
|
import * as _irtio_protocol from '@irtio/protocol';
|
|
3
3
|
import { PresenceRecord, ProfileLedger, ProfileSnapshot } from '@irtio/protocol';
|
|
4
|
+
import RAPIER2D from '@dimforge/rapier2d-compat';
|
|
4
5
|
import * as MATTER from 'matter-js';
|
|
5
6
|
import RAPIER from '@dimforge/rapier3d-compat';
|
|
6
7
|
|
|
@@ -51,6 +52,8 @@ interface CorrectionOp {
|
|
|
51
52
|
/** `true` when the correction outran the resim window and everything it named snapped. */
|
|
52
53
|
readonly snapped: boolean;
|
|
53
54
|
}
|
|
55
|
+
/** A row appeared (`onAdd`) or disappeared (`onRemove`) in one entity collection. */
|
|
56
|
+
type EntityCallback = (id: string, row: unknown) => void;
|
|
54
57
|
declare class ClientStore {
|
|
55
58
|
/** D50: not `readonly` — a schema swap replaces it in place (`swapSchema`). */
|
|
56
59
|
ext: AnySchema;
|
|
@@ -60,6 +63,13 @@ declare class ClientStore {
|
|
|
60
63
|
private tracked;
|
|
61
64
|
private readonly descs;
|
|
62
65
|
private readonly frozen;
|
|
66
|
+
/**
|
|
67
|
+
* Per-collection add/remove subscribers (`room.onAdd` / `room.onRemove`). Kept here rather than
|
|
68
|
+
* in the session because this is the only place that sees a frame's ops against the state the
|
|
69
|
+
* frame is about to change: a removed row's last values exist for exactly as long as it takes
|
|
70
|
+
* `applyDelta` to run.
|
|
71
|
+
*/
|
|
72
|
+
private readonly entityListeners;
|
|
63
73
|
/** Live collection facades by name, so a D50 swap can retarget rather than replace them. */
|
|
64
74
|
private readonly facades;
|
|
65
75
|
/** The object handed out as `room.state`; identity survives a resync. */
|
|
@@ -123,6 +133,28 @@ declare class ClientStore {
|
|
|
123
133
|
private freeze;
|
|
124
134
|
private entityHint;
|
|
125
135
|
private singletonHint;
|
|
136
|
+
private listenersFor;
|
|
137
|
+
/**
|
|
138
|
+
* Subscribes to rows appearing in one entity collection. Rows that are **already present** are
|
|
139
|
+
* announced synchronously as the subscription is made — the join snapshot has usually landed
|
|
140
|
+
* before user code runs, and a listener that had to reconcile the initial set by hand would
|
|
141
|
+
* make the event useless for exactly the case (a spawn effect) it exists for.
|
|
142
|
+
*/
|
|
143
|
+
onEntityAdd(name: string, cb: EntityCallback): () => void;
|
|
144
|
+
/** Subscribes to rows disappearing from one entity collection (never fires for past removals). */
|
|
145
|
+
onEntityRemove(name: string, cb: EntityCallback): () => void;
|
|
146
|
+
private emitAdd;
|
|
147
|
+
private emitRemove;
|
|
148
|
+
/**
|
|
149
|
+
* The adds and removes a delta is about to make, captured against the state as it stands now:
|
|
150
|
+
* a remove's last values are only readable before `applyDelta` drops the row.
|
|
151
|
+
*/
|
|
152
|
+
private captureOps;
|
|
153
|
+
private flushOps;
|
|
154
|
+
/** Fires adds and removes for the difference a wholesale snapshot load made. */
|
|
155
|
+
private emitSnapshotDiff;
|
|
156
|
+
/** The rows every subscribed collection holds right now, for the snapshot diff. */
|
|
157
|
+
private captureRows;
|
|
126
158
|
/**
|
|
127
159
|
* Applies a server `DELTA`. Update ops for instances this client owns are dropped field-wise
|
|
128
160
|
* (server wins only through `CORRECT`); adds, removes and owner changes always apply.
|
|
@@ -345,8 +377,21 @@ interface PredictionStats {
|
|
|
345
377
|
resimSteps: number;
|
|
346
378
|
/** Rebase passes (one per authoritative arrival batch). */
|
|
347
379
|
rebases: number;
|
|
348
|
-
/**
|
|
380
|
+
/**
|
|
381
|
+
* Rebases that rendered every body at authority. Since bugs.md #71 an outrun lead clamps
|
|
382
|
+
* instead of snapping (`leadClamped`), so nothing increments this today; the field stays
|
|
383
|
+
* because a zero here is a fence several suites hold, and a future hard-snap path would count
|
|
384
|
+
* here again.
|
|
385
|
+
*/
|
|
349
386
|
snaps: number;
|
|
387
|
+
/**
|
|
388
|
+
* Rebases whose lead outran `MAX_LEAD` and were re-stepped that many ticks instead (bugs.md
|
|
389
|
+
* #71). The local character still answers input; the anchor sits closer to authority than the
|
|
390
|
+
* round trip wants, so releases overshoot by the difference. A value that climbs and keeps
|
|
391
|
+
* climbing means the measured rtt is past the cap's horizon (667 ms at 60 Hz) — degraded
|
|
392
|
+
* prediction, where a snap would have been none.
|
|
393
|
+
*/
|
|
394
|
+
leadClamped: number;
|
|
350
395
|
/** Corrections whose values matched the local prediction within epsilon. */
|
|
351
396
|
suppressed: number;
|
|
352
397
|
/**
|
|
@@ -788,8 +833,8 @@ declare class Predictor {
|
|
|
788
833
|
* written, so plain state holds exactly what the server said), then re-step the world by the
|
|
789
834
|
* client's lead, applying to each re-stepped tick the intent that was in force *at that tick*:
|
|
790
835
|
* the newest buffered unjudged write stamped at or before it, or the baseline (the newest
|
|
791
|
-
* judged write) before the first of them. Bounded by `MAX_LEAD`: an outrun lead
|
|
792
|
-
*
|
|
836
|
+
* judged write) before the first of them. Bounded by `MAX_LEAD`: an outrun lead re-steps that
|
|
837
|
+
* many and counts (`stats.leadClamped`, bugs.md #71).
|
|
793
838
|
*/
|
|
794
839
|
private rebase;
|
|
795
840
|
/**
|
|
@@ -826,6 +871,126 @@ declare class Predictor {
|
|
|
826
871
|
private isF32;
|
|
827
872
|
}
|
|
828
873
|
|
|
874
|
+
/**
|
|
875
|
+
* The rapier2d half of client-side physics prediction: `joinRoom({ physics2d: { engine:
|
|
876
|
+
* 'rapier2d', … } })`'s option type, the lazy `@dimforge/rapier2d-compat` load, and the adapter
|
|
877
|
+
* that is the only place in the client where a rapier2d type is touched.
|
|
878
|
+
*
|
|
879
|
+
* The loop is `predictor.ts`, shared byte for byte with the other two engines. This file is the
|
|
880
|
+
* seam list, and it is transcribed from `physics.ts` — the Rapier one — rather than from
|
|
881
|
+
* `physics2d.ts`, for the same reason `core/rapier2d.ts` is transcribed from `core/physics.ts`:
|
|
882
|
+
*
|
|
883
|
+
* - **The world applies gravity**, so there is no `settle` hook here and there must not be one.
|
|
884
|
+
* `EngineAdapter.settle` exists for matter2d, whose engine gravity is usually zero and whose
|
|
885
|
+
* rooms apply it per body; a rapier2d world that gained one would fall twice as fast on the
|
|
886
|
+
* client and be corrected every tick.
|
|
887
|
+
* - **Velocities are per second**, so `velocityTolerance` is `epsilon / dt`, the 3D answer, not
|
|
888
|
+
* matter's per-step one.
|
|
889
|
+
* - **The step is `world.step()`** against a `world.timestep` fixed once at start.
|
|
890
|
+
*
|
|
891
|
+
* What it takes from the 2D side is only the plane: `{ x, y }` gravity, a scalar rotation and a
|
|
892
|
+
* scalar spin, mapped onto the thirteen 3D pose numbers through `@irtio/schema`'s `channelOf2d` /
|
|
893
|
+
* `applyChannel2d` / `angleFrom2d`. That mapping is written once, in the schema package, and this
|
|
894
|
+
* file reads it rather than restating it — a second copy is a way for the two sides to disagree
|
|
895
|
+
* by a sign.
|
|
896
|
+
*
|
|
897
|
+
* There is no planar-lock warning here (bug 6's friction trap needs a third axis to lock, and this
|
|
898
|
+
* engine has none), and the engine is loaded lazily, so a game predicting with matter2d or rapier3d
|
|
899
|
+
* never pulls the 2D WASM in.
|
|
900
|
+
*/
|
|
901
|
+
|
|
902
|
+
type ClientRapier2dModule = typeof RAPIER2D;
|
|
903
|
+
type ClientRapier2dWorld = RAPIER2D.World;
|
|
904
|
+
type ClientRapier2dBody = RAPIER2D.RigidBody;
|
|
905
|
+
interface ClientVector2d {
|
|
906
|
+
readonly x: number;
|
|
907
|
+
readonly y: number;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* What a client-side rapier2d body factory returns — the same shape the room config's factories
|
|
911
|
+
* use (`Rapier2dBodySpec` in `@irtio/server`). Rapier bodies *have* colliders rather than being
|
|
912
|
+
* their geometry, which is the one structural difference from the matter2d spec.
|
|
913
|
+
*/
|
|
914
|
+
interface ClientRapier2dBodySpec {
|
|
915
|
+
readonly body: RAPIER2D.RigidBodyDesc;
|
|
916
|
+
readonly colliders?: readonly RAPIER2D.ColliderDesc[];
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Method-syntax members check bivariantly, so a builder's factory or intent hook written against
|
|
920
|
+
* its own instance type (`(body, ball: Ball) => …`) is accepted — the values really passed are
|
|
921
|
+
* the schema's records for that collection.
|
|
922
|
+
*/
|
|
923
|
+
type ClientRapier2dBodyFactory = {
|
|
924
|
+
factory(rapier: ClientRapier2dModule, instance: AnyRecord$2, id: string): ClientRapier2dBodySpec;
|
|
925
|
+
}['factory'];
|
|
926
|
+
/**
|
|
927
|
+
* The client's half of `Rapier2dIntentHook`. Same five parameters in the same order as the server
|
|
928
|
+
* type, so one hook exported from a shared world module typechecks against both sides without a
|
|
929
|
+
* cast.
|
|
930
|
+
*/
|
|
931
|
+
type ClientRapier2dIntentHook = {
|
|
932
|
+
hook(body: ClientRapier2dBody, instance: AnyRecord$2, rapier: ClientRapier2dModule, world: ClientRapier2dWorld, timestep: number): void;
|
|
933
|
+
}['hook'];
|
|
934
|
+
/**
|
|
935
|
+
* `joinRoom({ physics2d: { engine: 'rapier2d', … } })` — the client half of the shared
|
|
936
|
+
* world-builder contract for a rapier2d room.
|
|
937
|
+
*
|
|
938
|
+
* It rides inside the `physics2d` option rather than beside it because it is the same *option*:
|
|
939
|
+
* a planar world, a planar gravity, one 2D engine. `engine` is the discriminant, and it is
|
|
940
|
+
* required here and optional (defaulting to `'matter2d'`) on the matter2d member, so every
|
|
941
|
+
* matter2d room written before this engine existed still compiles unchanged.
|
|
942
|
+
*
|
|
943
|
+
* There is no `settle`. See the module docblock.
|
|
944
|
+
*/
|
|
945
|
+
interface ClientRapier2dOptions {
|
|
946
|
+
/** The discriminant. Required: `physics2d` without one is a matter2d world. */
|
|
947
|
+
readonly engine: 'rapier2d';
|
|
948
|
+
/** Must equal the room config's gravity. Rapier's convention is y-up; nothing flips it. */
|
|
949
|
+
readonly gravity: ClientVector2d;
|
|
950
|
+
/** Seconds per step. Defaults to the room's tick interval (from `WELCOME`). */
|
|
951
|
+
readonly timestep?: number;
|
|
952
|
+
/** The shared static-geometry builder (the room's `physics.setup`). */
|
|
953
|
+
readonly setup?: (world: ClientRapier2dWorld, rapier: ClientRapier2dModule) => void;
|
|
954
|
+
/** Shape factories for the collections this client predicts (the room's `physics.bodies`). */
|
|
955
|
+
readonly bodies?: Readonly<Record<string, ClientRapier2dBodyFactory>>;
|
|
956
|
+
/**
|
|
957
|
+
* Intent → force, applied before every predicted step for bodies this client owns — the same
|
|
958
|
+
* function the room's `tick()` calls per instance, shared so both simulations agree.
|
|
959
|
+
*/
|
|
960
|
+
readonly intents?: Readonly<Record<string, ClientRapier2dIntentHook>>;
|
|
961
|
+
/**
|
|
962
|
+
* D21 cap: how many **non-owned** predicted bodies this client simulates ahead. Default 64.
|
|
963
|
+
* Over-cap instances get a kinematic proxy instead (see `maxProxyBodies`), so a predicted body
|
|
964
|
+
* still stands on them. Counted as `stats.overCap`.
|
|
965
|
+
*/
|
|
966
|
+
readonly maxPredictedBodies?: number;
|
|
967
|
+
/**
|
|
968
|
+
* D71 cap: how many **kinematic proxies** this client keeps. Default `MAX_PROXY_BODIES`. Past
|
|
969
|
+
* it, instances are absent from the local world and predicted bodies pass through them (warned,
|
|
970
|
+
* counted as `stats.absent`). See `ClientPhysicsOptions.maxProxyBodies` for why it is its own
|
|
971
|
+
* number.
|
|
972
|
+
*/
|
|
973
|
+
readonly maxProxyBodies?: number;
|
|
974
|
+
/**
|
|
975
|
+
* A body-field correction whose every value is within this tolerance of the local prediction is
|
|
976
|
+
* *suppressed*: authority still applies, but it is not a misprediction. Positions compare
|
|
977
|
+
* against `epsilon` world units; velocity channels against `epsilon / timestep`, because a
|
|
978
|
+
* rapier2d velocity is per second. Default 0.05, which is sized for a metre-scale world.
|
|
979
|
+
*/
|
|
980
|
+
readonly epsilon?: number;
|
|
981
|
+
/**
|
|
982
|
+
* How fast the drawn position eases back onto the simulation after a re-simulation moved it, as
|
|
983
|
+
* a half-life in milliseconds. `0` turns the smoothing off. Default 70. See
|
|
984
|
+
* `ClientPhysicsOptions.smoothingHalfLifeMs` for why this smooths the error and not the motion.
|
|
985
|
+
*/
|
|
986
|
+
readonly smoothingHalfLifeMs?: number;
|
|
987
|
+
/**
|
|
988
|
+
* How far the drawn position may be held from the simulation while an offset eases away, in
|
|
989
|
+
* world units. Past it the offset is dropped and the body appears where it is. Default 4.
|
|
990
|
+
*/
|
|
991
|
+
readonly smoothingSnapUnits?: number;
|
|
992
|
+
}
|
|
993
|
+
|
|
829
994
|
/**
|
|
830
995
|
* The matter2d half of client-side physics prediction (D45, D57): `joinRoom({ physics2d })`'s
|
|
831
996
|
* option type, the lazy `matter-js` load, and the adapter that is the only place in the client
|
|
@@ -900,6 +1065,11 @@ type ClientIntent2dHook = {
|
|
|
900
1065
|
* Every function here should be the very export the room config imports.
|
|
901
1066
|
*/
|
|
902
1067
|
interface ClientPhysics2dOptions {
|
|
1068
|
+
/**
|
|
1069
|
+
* The `physics2d` discriminant. Optional and defaulting to `'matter2d'`, so every matter2d
|
|
1070
|
+
* client written before rapier2d existed keeps compiling and keeps predicting with matter.
|
|
1071
|
+
*/
|
|
1072
|
+
readonly engine?: 'matter2d';
|
|
903
1073
|
/** Must equal the room config's gravity, in matter's own convention (y is down). */
|
|
904
1074
|
readonly gravity: ClientVector2;
|
|
905
1075
|
/** Seconds per step. Defaults to the room's tick interval (from `WELCOME`). */
|
|
@@ -1314,6 +1484,12 @@ type ClientState<S, Role extends string = RoleOf<S> & string> = {
|
|
|
1314
1484
|
serverOwned: true;
|
|
1315
1485
|
} ? ClientCollection<DeepReadonly<InferFields<F>>> : ClientCollection<Owned<InferFields<F>>> : SchemaDefs<S>[K] extends SingletonDef<infer F, any> ? DeepReadonly<InferFields<F>> : never;
|
|
1316
1486
|
};
|
|
1487
|
+
/** The entity-collection names visible to `Role` — the keys `room.onAdd`/`room.onRemove` take. */
|
|
1488
|
+
type EntityKeys<S, Role extends string = RoleOf<S> & string> = {
|
|
1489
|
+
[K in keyof ClientState<S, Role>]: ClientState<S, Role>[K] extends ReadonlyCollection<any> ? K : never;
|
|
1490
|
+
}[keyof ClientState<S, Role>];
|
|
1491
|
+
/** The row type of one entity collection, as `room.state` hands it out. */
|
|
1492
|
+
type RowOf<S, Role extends string, K extends keyof ClientState<S, Role>> = ClientState<S, Role>[K] extends ReadonlyCollection<infer T> ? T : never;
|
|
1317
1493
|
/** `connecting` → `starting`? → `connected` ⇄ `reconnecting` → `closed`. */
|
|
1318
1494
|
type Status = 'connecting' | 'starting' | 'connected' | 'reconnecting' | 'closed';
|
|
1319
1495
|
/**
|
|
@@ -1505,7 +1681,7 @@ interface JoinOptions<S, Role extends string = string> {
|
|
|
1505
1681
|
* take different functions; passing both is an error at join. The wire carries no engine
|
|
1506
1682
|
* name, so passing the one that does not match the room is a game bug the client cannot see.
|
|
1507
1683
|
*/
|
|
1508
|
-
readonly physics2d?: ClientPhysics2dOptions;
|
|
1684
|
+
readonly physics2d?: ClientPhysics2dOptions | ClientRapier2dOptions;
|
|
1509
1685
|
/** @internal */
|
|
1510
1686
|
readonly transport?: Transport;
|
|
1511
1687
|
/** @internal */
|
|
@@ -1734,9 +1910,29 @@ interface Room<S, Role extends string = RoleOf<S> & string> {
|
|
|
1734
1910
|
/** D70: this socket's message counters, including typed frames it could not read. */
|
|
1735
1911
|
readonly stats: RoomStats;
|
|
1736
1912
|
on<K extends keyof RoomEvents>(event: K, cb: (value: RoomEvents[K]) => void): Unsubscribe;
|
|
1913
|
+
/**
|
|
1914
|
+
* Fires when a row appears in one entity collection — a server-owned spawn, another client's
|
|
1915
|
+
* entity, a resync that brought a row this client had not seen.
|
|
1916
|
+
*
|
|
1917
|
+
* Rows that are **already present** when you subscribe are announced synchronously, inside the
|
|
1918
|
+
* `onAdd` call itself: the join snapshot has landed by the time `joinRoom` resolves, so a
|
|
1919
|
+
* listener registered right after the join sees the whole initial set and then every later
|
|
1920
|
+
* arrival, with no separate pass over `room.state`.
|
|
1921
|
+
*/
|
|
1922
|
+
onAdd<K extends EntityKeys<S, Role>>(collection: K, cb: (id: string, row: RowOf<S, Role, K>) => void): Unsubscribe;
|
|
1923
|
+
/**
|
|
1924
|
+
* Fires when a row disappears from one entity collection, carrying its **last values** — the
|
|
1925
|
+
* row itself is the death event, so a corpse stain or a score tick needs no parallel RPC. The
|
|
1926
|
+
* row handed to the callback is read-only and is not in `room.state` any more.
|
|
1927
|
+
*/
|
|
1928
|
+
onRemove<K extends EntityKeys<S, Role>>(collection: K, cb: (id: string, lastRow: RowOf<S, Role, K>) => void): Unsubscribe;
|
|
1737
1929
|
/** Sends any pending owned writes immediately instead of at the next flush window. */
|
|
1738
1930
|
flush(): void;
|
|
1739
|
-
|
|
1931
|
+
/**
|
|
1932
|
+
* Leaves for good. The returned promise resolves once the socket has closed (immediately if it
|
|
1933
|
+
* was already closed), so a script can `await room.leave()` before exiting.
|
|
1934
|
+
*/
|
|
1935
|
+
leave(): Promise<void>;
|
|
1740
1936
|
}
|
|
1741
1937
|
/**
|
|
1742
1938
|
* What `joinRelay` returns: presence and the message channel, nothing else.
|
|
@@ -1766,7 +1962,8 @@ interface RelayRoom<S = never> {
|
|
|
1766
1962
|
/** D70: this socket's message counters, including typed frames it could not read. */
|
|
1767
1963
|
readonly stats: RoomStats;
|
|
1768
1964
|
on<K extends keyof RoomEvents>(event: K, cb: (value: RoomEvents[K]) => void): Unsubscribe;
|
|
1769
|
-
|
|
1965
|
+
/** Leaves for good; resolves once the socket has closed (immediately if it already had). */
|
|
1966
|
+
leave(): Promise<void>;
|
|
1770
1967
|
}
|
|
1771
1968
|
|
|
1772
1969
|
/**
|
|
@@ -1934,6 +2131,230 @@ declare function matchRoom<S extends AnySchema, Role extends string = RoleOf<S>
|
|
|
1934
2131
|
} & {
|
|
1935
2132
|
readonly identity?: boolean;
|
|
1936
2133
|
}): Promise<Room<S, Role>>;
|
|
2134
|
+
/** What a `mode: "public"` answer carries. */
|
|
2135
|
+
interface PublicTicket {
|
|
2136
|
+
readonly room: string;
|
|
2137
|
+
readonly queue: string;
|
|
2138
|
+
/** How many players the queue this lobby belongs to seats. */
|
|
2139
|
+
readonly size: number;
|
|
2140
|
+
/** True when this caller minted the room rather than joining one somebody else was waiting in. */
|
|
2141
|
+
readonly created: boolean;
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Asks the control plane for a public room of `queue`: the oldest open one, or a fresh code.
|
|
2145
|
+
*
|
|
2146
|
+
* The lower half of {@link joinPublic}, exported because the element's chooser is driven by events
|
|
2147
|
+
* rather than by a call, and an app that wires its own **Join a game** button may want the code
|
|
2148
|
+
* without the join.
|
|
2149
|
+
*
|
|
2150
|
+
* `exclude` names a room this caller already failed to join, so the second answer is a different
|
|
2151
|
+
* one. See `joinPublic` for when that happens.
|
|
2152
|
+
*/
|
|
2153
|
+
declare function findPublic(project: string, options?: Pick<MatchOptions, 'queue' | 'controlUrl' | 'fetch'> & {
|
|
2154
|
+
readonly identity?: string;
|
|
2155
|
+
readonly exclude?: string;
|
|
2156
|
+
}): Promise<PublicTicket>;
|
|
2157
|
+
/**
|
|
2158
|
+
* Join a game: land in the next open public lobby, or open one and wait in it.
|
|
2159
|
+
*
|
|
2160
|
+
* ```ts
|
|
2161
|
+
* const room = await joinPublic(schema); // the default queue
|
|
2162
|
+
* const room = await joinPublic(schema, { queue: '4p', identity: true });
|
|
2163
|
+
* ```
|
|
2164
|
+
*
|
|
2165
|
+
* This is the other shape of matchmaking and it is deliberately not `matchRoom`. `matchRoom`
|
|
2166
|
+
* holds a long poll until a whole party is there and answers `E_NO_MATCH` if one never is;
|
|
2167
|
+
* `joinPublic` resolves on the first round trip and puts the player in a room that says "1/4
|
|
2168
|
+
* players". Waiting in a lobby you can see is a different experience from waiting on a spinner,
|
|
2169
|
+
* and it is the one a **Join a game** button should give.
|
|
2170
|
+
*
|
|
2171
|
+
* Nothing about the join differs from a friend sharing a link — the room neither knows nor cares
|
|
2172
|
+
* that the registry sent you — which is the property every part of this feature is built on.
|
|
2173
|
+
*
|
|
2174
|
+
* **The one retry.** The registry answers from an occupancy reading a few seconds old, so the room
|
|
2175
|
+
* may have filled on the way there. That is refused with `E_ROOM_FULL`, and the honest recovery is
|
|
2176
|
+
* to ask once more, naming the room that did not work so the second answer cannot be the same one.
|
|
2177
|
+
* Once, and never a loop: a retry that kept going would turn a busy game into a client that
|
|
2178
|
+
* hammers control, and a second failure means something a third attempt will not fix.
|
|
2179
|
+
*/
|
|
2180
|
+
declare function joinPublic<S extends AnySchema, Role extends string = RoleOf<S> & string>(schema: S, options?: Pick<MatchOptions, 'queue' | 'controlUrl' | 'fetch'> & JoinOptions<S, Role> & {
|
|
2181
|
+
role?: RoleOf<S> & string;
|
|
2182
|
+
} & {
|
|
2183
|
+
readonly identity?: boolean;
|
|
2184
|
+
}): Promise<Room<S, Role>>;
|
|
2185
|
+
|
|
2186
|
+
/**
|
|
2187
|
+
* D75 (M6 lane H): the client's half of the lobby, and the glue that wires `<irt-lobby>` up.
|
|
2188
|
+
*
|
|
2189
|
+
* Two exports and a clean division between them.
|
|
2190
|
+
*
|
|
2191
|
+
* `lobbyOf(room)` is the **view**: it reads the lobby fragment out of ordinary room state and
|
|
2192
|
+
* answers the structural shape `@irtio/lobby`'s `AttachableRoom.lobby` asks for. It is a reader
|
|
2193
|
+
* plus one owner write (the ready flag), and it holds nothing the room does not already say.
|
|
2194
|
+
*
|
|
2195
|
+
* `attachLobby(element, schema, options)` is the **glue**: it listens for the panel's two chooser
|
|
2196
|
+
* events, makes the calls the panel deliberately cannot make itself, and attaches the room it gets
|
|
2197
|
+
* back. Sugar, not capability — the same rule `matchRoom` is held to. An app that wants to answer
|
|
2198
|
+
* the events itself keeps every part of this and needs none of it.
|
|
2199
|
+
*
|
|
2200
|
+
* ## Why the view polls
|
|
2201
|
+
*
|
|
2202
|
+
* `room.on(...)` has no "state changed" event: the client SDK publishes `status`, `clients`, `rtt`,
|
|
2203
|
+
* `error` and `correct`, and lobby state is ordinary entity state that arrives in a `DELTA` like
|
|
2204
|
+
* everything else. So `on('change')` samples — on every `clients` event, and on a cheap timer in
|
|
2205
|
+
* between — and fires only when a small signature actually differs.
|
|
2206
|
+
*
|
|
2207
|
+
* That is a real cost and it is written here rather than hidden: one shallow string build per
|
|
2208
|
+
* sample, over at most `maxClients` records, for as long as somebody is watching. It is the honest
|
|
2209
|
+
* price of the element importing nothing from this package. A `state` event on `Room` would replace
|
|
2210
|
+
* the timer with a subscription and is recorded as a debt in the lane report rather than smuggled
|
|
2211
|
+
* into this lane's protocol-free budget.
|
|
2212
|
+
*/
|
|
2213
|
+
|
|
2214
|
+
/**
|
|
2215
|
+
* The two collection names the lobby fragment declares.
|
|
2216
|
+
*
|
|
2217
|
+
* Literals here rather than an import from `@irtio/server`, which is where they are defined. The
|
|
2218
|
+
* client SDK ships to browsers and depends on `@irtio/schema` and `@irtio/protocol` alone; taking
|
|
2219
|
+
* a dependency on the room-file API to read two strings would put the whole of it in every game's
|
|
2220
|
+
* bundle. This is the same fence `@irtio/server`'s own `MAX_AWAKE_MAX` sits behind, and it is
|
|
2221
|
+
* pinned the same way: `test/lobby-view.test.ts` imports both and asserts the pairs equal, from a
|
|
2222
|
+
* package that legitimately depends on both, so a rename is a failing test rather than a lobby
|
|
2223
|
+
* that silently never appears.
|
|
2224
|
+
*/
|
|
2225
|
+
declare const LOBBY_STATE = "irtLobby";
|
|
2226
|
+
declare const LOBBY_MEMBERS = "irtLobbyMembers";
|
|
2227
|
+
/** How often the view samples for a change between `clients` events. */
|
|
2228
|
+
declare const LOBBY_POLL_MS = 120;
|
|
2229
|
+
/** One row of the lobby roster, in join order. */
|
|
2230
|
+
interface LobbyPlayer {
|
|
2231
|
+
readonly clientId: string;
|
|
2232
|
+
readonly ready: boolean;
|
|
2233
|
+
readonly connected: boolean;
|
|
2234
|
+
/** True for the row belonging to the client holding this view. */
|
|
2235
|
+
readonly me: boolean;
|
|
2236
|
+
}
|
|
2237
|
+
/** What `lobbyOf` answers: `@irtio/lobby`'s structural `lobby` member, with names. */
|
|
2238
|
+
interface LobbyView {
|
|
2239
|
+
readonly phase: 'lobby' | 'started';
|
|
2240
|
+
readonly players: readonly LobbyPlayer[];
|
|
2241
|
+
readonly public: boolean;
|
|
2242
|
+
readonly readyUi: boolean;
|
|
2243
|
+
/** How many the room starts at under `'when-full'`, or 0 when the room has not said. */
|
|
2244
|
+
readonly capacity: number;
|
|
2245
|
+
ready(value: boolean): void;
|
|
2246
|
+
setPublic?(value: boolean): void;
|
|
2247
|
+
on(event: 'change', cb: () => void): () => void;
|
|
2248
|
+
}
|
|
2249
|
+
interface StateLike {
|
|
2250
|
+
[collection: string]: unknown;
|
|
2251
|
+
}
|
|
2252
|
+
interface RoomLike {
|
|
2253
|
+
readonly me: string;
|
|
2254
|
+
readonly state: StateLike;
|
|
2255
|
+
readonly clients: readonly {
|
|
2256
|
+
clientId: string;
|
|
2257
|
+
connected: boolean;
|
|
2258
|
+
}[];
|
|
2259
|
+
on(event: 'clients', cb: (clients: readonly {
|
|
2260
|
+
clientId: string;
|
|
2261
|
+
connected: boolean;
|
|
2262
|
+
}[]) => void): () => void;
|
|
2263
|
+
flush(): void;
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Does this room carry a lobby?
|
|
2267
|
+
*
|
|
2268
|
+
* A schema that did not spread `lobbyCollections` has neither collection, and the honest answer for
|
|
2269
|
+
* such a room is that it has no lobby rather than an empty one — `<irt-lobby>` renders no lobby UI
|
|
2270
|
+
* at all in that case, which is what keeps every existing consumer unchanged.
|
|
2271
|
+
*/
|
|
2272
|
+
declare function hasLobby(room: unknown): boolean;
|
|
2273
|
+
/**
|
|
2274
|
+
* Reads a joined room's lobby.
|
|
2275
|
+
*
|
|
2276
|
+
* ```ts
|
|
2277
|
+
* const room = await joinPublic(schema);
|
|
2278
|
+
* lobby.attach(room, { lobby: lobbyOf(room) });
|
|
2279
|
+
* ```
|
|
2280
|
+
*
|
|
2281
|
+
* `setPublic` is present only when the caller supplies one, and that is a consequence of there
|
|
2282
|
+
* being no built-in RPC for it. The platform never takes a public toggle from a client — that
|
|
2283
|
+
* would be a protocol change — so the room's own game code owns the door. A game that wants the
|
|
2284
|
+
* panel's toggle to work declares its own RPC and passes it here:
|
|
2285
|
+
*
|
|
2286
|
+
* ```ts
|
|
2287
|
+
* lobbyOf(room, { setPublic: (value) => void room.call.setPublic({ value }) })
|
|
2288
|
+
* ```
|
|
2289
|
+
*
|
|
2290
|
+
* Without one, the panel renders the room's public state and no toggle, which is the truthful
|
|
2291
|
+
* rendering of a room whose code has not offered the control.
|
|
2292
|
+
*/
|
|
2293
|
+
declare function lobbyOf(room: unknown, options?: {
|
|
2294
|
+
readonly setPublic?: (value: boolean) => void;
|
|
2295
|
+
}): LobbyView | undefined;
|
|
2296
|
+
/** What the panel's `quickmatch` event carries. */
|
|
2297
|
+
interface QuickMatchDetail {
|
|
2298
|
+
readonly queue?: string;
|
|
2299
|
+
}
|
|
2300
|
+
/** The element surface `attachLobby` needs. Structural, so a test can pass a stand-in. */
|
|
2301
|
+
interface LobbyElementLike {
|
|
2302
|
+
addEventListener(type: string, cb: (event: Event) => void): void;
|
|
2303
|
+
removeEventListener(type: string, cb: (event: Event) => void): void;
|
|
2304
|
+
setAttribute(name: string, value: string): void;
|
|
2305
|
+
removeAttribute(name: string): void;
|
|
2306
|
+
attach(room: unknown): () => void;
|
|
2307
|
+
}
|
|
2308
|
+
/**
|
|
2309
|
+
* The room, as `<irt-lobby>`'s structural `AttachableRoom` wants it: everything the panel already
|
|
2310
|
+
* read, plus the optional `lobby` member the design's §6 adds.
|
|
2311
|
+
*
|
|
2312
|
+
* A wrapper of getters rather than a spread, because a `Room`'s fields are accessors on a class:
|
|
2313
|
+
* `{ ...room }` would freeze `status`, `id` and `clients` at the moment of the copy, and the panel
|
|
2314
|
+
* re-reads all three on every render precisely so a reconnect that changes the code is picked up.
|
|
2315
|
+
* Mutating the room to hang a `lobby` on it was the other option and is worse — the SDK's own
|
|
2316
|
+
* object would then carry a field its type does not declare.
|
|
2317
|
+
*/
|
|
2318
|
+
declare function attachable(room: RoomLike & {
|
|
2319
|
+
id: string;
|
|
2320
|
+
link: string;
|
|
2321
|
+
status: string;
|
|
2322
|
+
maxClients?: number;
|
|
2323
|
+
rtt?: number;
|
|
2324
|
+
}, view: LobbyView | undefined): unknown;
|
|
2325
|
+
interface AttachLobbyOptions<S extends AnySchema, Role extends string> {
|
|
2326
|
+
/** Passed through to `joinRoom` / `joinPublic`. */
|
|
2327
|
+
readonly join?: JoinOptions<S, Role> & {
|
|
2328
|
+
role?: RoleOf<S> & string;
|
|
2329
|
+
};
|
|
2330
|
+
/** Passed through to the control-plane call. */
|
|
2331
|
+
readonly match?: Pick<MatchOptions, 'controlUrl' | 'fetch'> & {
|
|
2332
|
+
readonly identity?: boolean;
|
|
2333
|
+
};
|
|
2334
|
+
/** The room's own public-toggle RPC, if it has one. See `lobbyOf`. */
|
|
2335
|
+
readonly setPublic?: (room: Room<S, Role>, value: boolean) => void;
|
|
2336
|
+
/** Called with the room once it is joined, so game code can start drawing. */
|
|
2337
|
+
readonly onRoom?: (room: Room<S, Role>) => void;
|
|
2338
|
+
/** Called when a join fails. Default: the panel's status goes back to idle and the error is
|
|
2339
|
+
* rethrown on the microtask queue so it reaches `window.onerror` rather than vanishing. */
|
|
2340
|
+
readonly onError?: (err: unknown) => void;
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Wires `<irt-lobby>`'s chooser to the two calls it deliberately cannot make itself.
|
|
2344
|
+
*
|
|
2345
|
+
* ```ts
|
|
2346
|
+
* attachLobby(document.querySelector('irt-lobby')!, schema, { onRoom: (room) => start(room) });
|
|
2347
|
+
* ```
|
|
2348
|
+
*
|
|
2349
|
+
* The panel emits and this answers: `private` becomes an ordinary `joinRoom` with no code (which
|
|
2350
|
+
* creates one), `quickmatch` becomes `joinPublic`. Both then `attach` the room, exactly as an app
|
|
2351
|
+
* doing it by hand would. Returns an unsubscribe.
|
|
2352
|
+
*
|
|
2353
|
+
* `@irtio/lobby` imports nothing from this package and never will — that is what lets a React or
|
|
2354
|
+
* Svelte app drive the panel with attributes and no SDK. This function is the other side of that
|
|
2355
|
+
* rule rather than an exception to it: the dependency points this way.
|
|
2356
|
+
*/
|
|
2357
|
+
declare function attachLobby<S extends AnySchema, Role extends string = RoleOf<S> & string>(element: LobbyElementLike, schema: S, options?: AttachLobbyOptions<S, Role>): () => void;
|
|
1937
2358
|
|
|
1938
2359
|
/**
|
|
1939
2360
|
* The default `Scheduler`. In a browser the write batcher aligns to `requestAnimationFrame` (one
|
|
@@ -2135,7 +2556,7 @@ interface SessionOptions {
|
|
|
2135
2556
|
/** The shared world-builder half the client predicts with (D22 part 2). */
|
|
2136
2557
|
readonly physics?: ClientPhysicsOptions | undefined;
|
|
2137
2558
|
/** The same, for a matter2d room (D57). Never both; `joinRoom` refuses that. */
|
|
2138
|
-
readonly physics2d?: ClientPhysics2dOptions | undefined;
|
|
2559
|
+
readonly physics2d?: ClientPhysics2dOptions | ClientRapier2dOptions | undefined;
|
|
2139
2560
|
readonly transport?: Transport | undefined;
|
|
2140
2561
|
readonly scheduler?: Scheduler | undefined;
|
|
2141
2562
|
readonly onFrame?: FrameHook | undefined;
|
|
@@ -2272,8 +2693,15 @@ declare class Session {
|
|
|
2272
2693
|
private connect;
|
|
2273
2694
|
private transportFailed;
|
|
2274
2695
|
private sendHello;
|
|
2275
|
-
/**
|
|
2276
|
-
|
|
2696
|
+
/**
|
|
2697
|
+
* Leaves for good: no reconnect, pending calls reject, the socket closes.
|
|
2698
|
+
*
|
|
2699
|
+
* The promise resolves when the socket reports its close (or immediately when there was
|
|
2700
|
+
* nothing open), so `await room.leave()` is a real wait rather than a guessed timeout. A
|
|
2701
|
+
* transport that never reports one is bounded by `LEAVE_CLOSE_TIMEOUT_MS` so the caller cannot
|
|
2702
|
+
* hang on it.
|
|
2703
|
+
*/
|
|
2704
|
+
leave(): Promise<void>;
|
|
2277
2705
|
private fatal;
|
|
2278
2706
|
private onSocketClosed;
|
|
2279
2707
|
private stopTimers;
|
|
@@ -2586,4 +3014,4 @@ declare function joinRoom<S extends AnySchema, Role extends string = RoleOf<S> &
|
|
|
2586
3014
|
*/
|
|
2587
3015
|
declare function joinRelay<S extends AnySchema = never>(options?: JoinRelayOptions<S extends AnySchema ? S : AnySchema>): Promise<RelayRoom<S>>;
|
|
2588
3016
|
|
|
2589
|
-
export { ACCOUNT_STORAGE_KEY, CALL_TIMEOUT_MS, type ClientBody2dFactory, type ClientBody2dSpec, type ClientBodySpec, type ClientCollection, type ClientIntent2dHook, type ClientMatterBody, type ClientMatterConstraint, type ClientMatterEngine, type ClientMatterModule, type ClientPhysics2dOptions, type ClientPhysicsOptions, type ClientRapierBody, type ClientRapierModule, type ClientRapierWorld, type ClientState, ClientStore, type ClientVector2, type ClientVector3, type Correction, DEFAULT_CONTROL_URL, DEFAULT_REGION, DEFAULT_WRITE_INTERVAL_MS, DEV_PORT, E_CONNECT_FAILED, E_IDENTITY_RATE_LIMITED, type FrameHook, INTERNAL_VOICE_TRACKS, Identity, IdentityError, type IdentityOptions, type IdentityStorage, type JoinOptions, type JoinRelayOptions, MAX_IDENTITY_RETRY_WAIT_MS, MAX_PREDICTED_BODIES, MAX_PROXY_BODIES, MatchError, type MatchOptions, type MatchTicket, type MessageTarget, PING_INTERVAL_MS, PREDICTION_EPSILON, type PartyTicket, type PredictionStats, type PredictionStatus, REGION_RE, RESIM_DEPTH, type RelayRoom, type Room, type RoomCallProxy, type RoomError, type RoomEvents, type RoomMessageStats, type RoomMessages, type RoomProfile, type RoomStats, SMOOTHING_HALF_LIFE_MS, SMOOTHING_SNAP_UNITS, type Scheduler, Session, type Status, type Transport, type TransportSocket, type Unsubscribe, type VoiceHandle, type VoiceOptions, type VoicePeerState, type VoiceTrackAccess, createParty, defaultScheduler, findMatch, identityStorageKey, joinRelay, joinRoom, joinVoice, linkForUrl, matchRoom, resolveUrl, roomIdFrom, webSocketTransport };
|
|
3017
|
+
export { ACCOUNT_STORAGE_KEY, type AttachLobbyOptions, CALL_TIMEOUT_MS, type ClientBody2dFactory, type ClientBody2dSpec, type ClientBodySpec, type ClientCollection, type ClientIntent2dHook, type ClientMatterBody, type ClientMatterConstraint, type ClientMatterEngine, type ClientMatterModule, type ClientPhysics2dOptions, type ClientPhysicsOptions, type ClientRapier2dBody, type ClientRapier2dBodyFactory, type ClientRapier2dBodySpec, type ClientRapier2dIntentHook, type ClientRapier2dModule, type ClientRapier2dOptions, type ClientRapier2dWorld, type ClientRapierBody, type ClientRapierModule, type ClientRapierWorld, type ClientState, ClientStore, type ClientVector2, type ClientVector2d, type ClientVector3, type Correction, DEFAULT_CONTROL_URL, DEFAULT_REGION, DEFAULT_WRITE_INTERVAL_MS, DEV_PORT, E_CONNECT_FAILED, E_IDENTITY_RATE_LIMITED, type EntityKeys, type FrameHook, INTERNAL_VOICE_TRACKS, Identity, IdentityError, type IdentityOptions, type IdentityStorage, type JoinOptions, type JoinRelayOptions, LOBBY_MEMBERS, LOBBY_POLL_MS, LOBBY_STATE, type LobbyPlayer, type LobbyView, MAX_IDENTITY_RETRY_WAIT_MS, MAX_PREDICTED_BODIES, MAX_PROXY_BODIES, MatchError, type MatchOptions, type MatchTicket, type MessageTarget, PING_INTERVAL_MS, PREDICTION_EPSILON, type PartyTicket, type PredictionStats, type PredictionStatus, type PublicTicket, type QuickMatchDetail, REGION_RE, RESIM_DEPTH, type RelayRoom, type Room, type RoomCallProxy, type RoomError, type RoomEvents, type RoomMessageStats, type RoomMessages, type RoomProfile, type RoomStats, type RowOf, SMOOTHING_HALF_LIFE_MS, SMOOTHING_SNAP_UNITS, type Scheduler, Session, type Status, type Transport, type TransportSocket, type Unsubscribe, type VoiceHandle, type VoiceOptions, type VoicePeerState, type VoiceTrackAccess, attachLobby, attachable, createParty, defaultScheduler, findMatch, findPublic, hasLobby, identityStorageKey, joinPublic, joinRelay, joinRoom, joinVoice, linkForUrl, lobbyOf, matchRoom, resolveUrl, roomIdFrom, webSocketTransport };
|