@couch-kit/runtime 0.1.0 → 0.3.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 +55 -0
- package/dist/index.cjs +31 -4
- package/dist/index.js +31 -4
- package/lib/broadcast-scheduler.d.ts.map +1 -1
- package/lib/rate-limiter.d.ts.map +1 -1
- package/lib/runtime.d.ts +41 -0
- package/lib/runtime.d.ts.map +1 -1
- package/lib/session-manager.d.ts.map +1 -1
- package/package.json +3 -4
- package/src/runtime.ts +78 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# @couch-kit/runtime
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#164](https://github.com/faluciano/react-native-couch-kit/pull/164) [`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e) Thanks [@faluciano](https://github.com/faluciano)! - Send a projected state update as one relay frame instead of one per player
|
|
8
|
+
|
|
9
|
+
A game with a `project` function sends every player their own view, which meant
|
|
10
|
+
one WebSocket frame per player for every state change. Relays bill and
|
|
11
|
+
rate-limit per inbound frame, so a four-player table paid four messages for one
|
|
12
|
+
update and spent four of the display's 30-per-second budget.
|
|
13
|
+
|
|
14
|
+
`GameRuntimeTransport` gains an optional `sendMany(entries)`. When a transport
|
|
15
|
+
implements it, the runtime hands over the whole projected batch at once;
|
|
16
|
+
transports that do not — the LAN WebSocket path — keep receiving one `send` per
|
|
17
|
+
connection and are unaffected.
|
|
18
|
+
|
|
19
|
+
`RelayDisplayHost` implements it with a new `DATA_MULTI` envelope carrying a
|
|
20
|
+
peer-id-to-payload map, which the relay unpacks into ordinary `DATA` frames.
|
|
21
|
+
Phones need no update — nothing on the client side can tell a batched update
|
|
22
|
+
from a unicast one. If the combined frame would exceed the relay's 256KB
|
|
23
|
+
ceiling, the display falls back to individual frames rather than send something
|
|
24
|
+
the relay would drop.
|
|
25
|
+
|
|
26
|
+
Relays must be updated before displays: both bundled implementations
|
|
27
|
+
(`services/relay`, `services/relay-worker`) understand `DATA_MULTI`, and an
|
|
28
|
+
older relay answers it with `MALFORMED`. The type is host-only — a phone sending
|
|
29
|
+
it is rejected, so it cannot be used to reach another phone directly.
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [[`a157126`](https://github.com/faluciano/react-native-couch-kit/commit/a157126424e4d73dcc7185118d5be0db6719792e)]:
|
|
34
|
+
- @couch-kit/core@0.10.0
|
|
35
|
+
|
|
36
|
+
## 0.2.0
|
|
37
|
+
|
|
38
|
+
### Minor Changes
|
|
39
|
+
|
|
40
|
+
- [#155](https://github.com/faluciano/react-native-couch-kit/pull/155) [`4f297a1`](https://github.com/faluciano/react-native-couch-kit/commit/4f297a19c442541703c3bee7bee26354ae3476a4) Thanks [@faluciano](https://github.com/faluciano)! - Hidden information can now be hidden for real: `GameHostRuntimeConfig` takes an
|
|
41
|
+
optional `project(state, playerId)` that narrows the authoritative state to what
|
|
42
|
+
one player may see.
|
|
43
|
+
|
|
44
|
+
Without it the runtime broadcasts the same state to everyone, so hiding a hand
|
|
45
|
+
depends on the client choosing not to look — any player could read opponents'
|
|
46
|
+
cards from devtools. With it, the data never reaches their device: the runtime
|
|
47
|
+
sends each connection its own projection, in `WELCOME`, `RECONNECTED`, and every
|
|
48
|
+
state update.
|
|
49
|
+
|
|
50
|
+
Games with no hidden information omit `project` and are unchanged — state is
|
|
51
|
+
still broadcast in one frame.
|
|
52
|
+
|
|
53
|
+
Because a projected client holds a _view_ rather than the whole state, it cannot
|
|
54
|
+
run the game reducer, so `ClientConfig.reducer` is now optional. Omit it and the
|
|
55
|
+
client renders what the host sends (no optimistic updates); a round trip is
|
|
56
|
+
imperceptible for turn-based games, and it is what makes the guarantee real.
|
|
57
|
+
|
|
3
58
|
## 0.1.0
|
|
4
59
|
|
|
5
60
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -434,7 +434,7 @@ class GameHostRuntime {
|
|
|
434
434
|
type: import_core5.MessageTypes.RECONNECTED,
|
|
435
435
|
payload: {
|
|
436
436
|
playerId,
|
|
437
|
-
state: this.
|
|
437
|
+
state: this.viewFor(playerId)
|
|
438
438
|
}
|
|
439
439
|
});
|
|
440
440
|
} else {
|
|
@@ -442,7 +442,7 @@ class GameHostRuntime {
|
|
|
442
442
|
type: import_core5.MessageTypes.WELCOME,
|
|
443
443
|
payload: {
|
|
444
444
|
playerId,
|
|
445
|
-
state: this.
|
|
445
|
+
state: this.viewFor(playerId),
|
|
446
446
|
serverTime: Date.now()
|
|
447
447
|
}
|
|
448
448
|
});
|
|
@@ -560,13 +560,40 @@ class GameHostRuntime {
|
|
|
560
560
|
}
|
|
561
561
|
this.broadcastScheduler.schedule(this.broadcastState);
|
|
562
562
|
}
|
|
563
|
+
viewFor(playerId) {
|
|
564
|
+
const project = this.config.project;
|
|
565
|
+
return project ? project(this.state, playerId) : this.state;
|
|
566
|
+
}
|
|
563
567
|
broadcastState = () => {
|
|
564
|
-
|
|
568
|
+
const transport = this.transport;
|
|
569
|
+
if (!transport)
|
|
565
570
|
return;
|
|
566
571
|
const actions = this.actionQueue;
|
|
567
572
|
this.actionQueue = [];
|
|
568
573
|
this.stateDirty = false;
|
|
569
|
-
this.
|
|
574
|
+
if (!this.config.project) {
|
|
575
|
+
transport.broadcast(createStateUpdateMessage(this.state, actions));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const entries = [];
|
|
579
|
+
for (const connectionId of this.joinedConnections) {
|
|
580
|
+
const playerId = this.sessionManager.getPlayerIdForSocket(connectionId);
|
|
581
|
+
if (!playerId)
|
|
582
|
+
continue;
|
|
583
|
+
entries.push({
|
|
584
|
+
connectionId,
|
|
585
|
+
message: createStateUpdateMessage(this.viewFor(playerId), actions)
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
if (entries.length === 0)
|
|
589
|
+
return;
|
|
590
|
+
if (transport.sendMany) {
|
|
591
|
+
transport.sendMany(entries);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
for (const { connectionId, message } of entries) {
|
|
595
|
+
this.send(connectionId, message);
|
|
596
|
+
}
|
|
570
597
|
};
|
|
571
598
|
send(connectionId, message) {
|
|
572
599
|
this.transport?.send(connectionId, message);
|
package/dist/index.js
CHANGED
|
@@ -389,7 +389,7 @@ class GameHostRuntime {
|
|
|
389
389
|
type: MessageTypes3.RECONNECTED,
|
|
390
390
|
payload: {
|
|
391
391
|
playerId,
|
|
392
|
-
state: this.
|
|
392
|
+
state: this.viewFor(playerId)
|
|
393
393
|
}
|
|
394
394
|
});
|
|
395
395
|
} else {
|
|
@@ -397,7 +397,7 @@ class GameHostRuntime {
|
|
|
397
397
|
type: MessageTypes3.WELCOME,
|
|
398
398
|
payload: {
|
|
399
399
|
playerId,
|
|
400
|
-
state: this.
|
|
400
|
+
state: this.viewFor(playerId),
|
|
401
401
|
serverTime: Date.now()
|
|
402
402
|
}
|
|
403
403
|
});
|
|
@@ -515,13 +515,40 @@ class GameHostRuntime {
|
|
|
515
515
|
}
|
|
516
516
|
this.broadcastScheduler.schedule(this.broadcastState);
|
|
517
517
|
}
|
|
518
|
+
viewFor(playerId) {
|
|
519
|
+
const project = this.config.project;
|
|
520
|
+
return project ? project(this.state, playerId) : this.state;
|
|
521
|
+
}
|
|
518
522
|
broadcastState = () => {
|
|
519
|
-
|
|
523
|
+
const transport = this.transport;
|
|
524
|
+
if (!transport)
|
|
520
525
|
return;
|
|
521
526
|
const actions = this.actionQueue;
|
|
522
527
|
this.actionQueue = [];
|
|
523
528
|
this.stateDirty = false;
|
|
524
|
-
this.
|
|
529
|
+
if (!this.config.project) {
|
|
530
|
+
transport.broadcast(createStateUpdateMessage(this.state, actions));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const entries = [];
|
|
534
|
+
for (const connectionId of this.joinedConnections) {
|
|
535
|
+
const playerId = this.sessionManager.getPlayerIdForSocket(connectionId);
|
|
536
|
+
if (!playerId)
|
|
537
|
+
continue;
|
|
538
|
+
entries.push({
|
|
539
|
+
connectionId,
|
|
540
|
+
message: createStateUpdateMessage(this.viewFor(playerId), actions)
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
if (entries.length === 0)
|
|
544
|
+
return;
|
|
545
|
+
if (transport.sendMany) {
|
|
546
|
+
transport.sendMany(entries);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
for (const { connectionId, message } of entries) {
|
|
550
|
+
this.send(connectionId, message);
|
|
551
|
+
}
|
|
525
552
|
};
|
|
526
553
|
send(connectionId, message) {
|
|
527
554
|
this.transport?.send(connectionId, message);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"broadcast-scheduler.d.ts","sourceRoot":"","sources":["../src/broadcast-scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEjE,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,MAAM,MAAM,kBAAkB,GAAG,OAAO,CACtC,WAAW,EACX;IAAE,IAAI,EAAE,OAAO,YAAY,CAAC,YAAY,CAAA;CAAE,CAC3C,CAAC;AAEF,MAAM,WAAW,cAAc,CAAC,MAAM;IACpC,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1D,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAOD,MAAM,WAAW,yBAAyB,CAAC,MAAM;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACpC;AAED;;;GAGG;AACH,qBAAa,kBAAkB,CAAC,MAAM,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC;IACpE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,KAAK,CAAuB;
|
|
1
|
+
{"version":3,"file":"broadcast-scheduler.d.ts","sourceRoot":"","sources":["../src/broadcast-scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEjE,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,MAAM,MAAM,kBAAkB,GAAG,OAAO,CACtC,WAAW,EACX;IAAE,IAAI,EAAE,OAAO,YAAY,CAAC,YAAY,CAAA;CAAE,CAC3C,CAAC;AAEF,MAAM,WAAW,cAAc,CAAC,MAAM;IACpC,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1D,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAOD,MAAM,WAAW,yBAAyB,CAAC,MAAM;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACpC;AAED;;;GAGG;AACH,qBAAa,kBAAkB,CAAC,MAAM,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC;IACpE,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,KAAK,CAAuB;IAEpC,YAAY,OAAO,GAAE,yBAAyB,CAAC,MAAM,CAAM,EAK1D;IAED,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI,CAMnC;IAED,MAAM,IAAI,IAAI,CAKb;IAED,kBAAkB,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAEhD;IAED,mBAAmB,IAAI,OAAO,CAE7B;CACF;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,OAAO,EACjB,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,SAAS,GAAE,MAAmB,GAC7B,kBAAkB,CAapB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,uCAAuC;AACvC,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,uCAAuC;AACvC,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAEtC,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAgB,SAAQ,aAAa;IACpD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoC;IAE3D,YAAY,OAAO,GAAE,wBAA6B,EAIjD;IAED,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,CAexC;IAED,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE5B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CAE/C;CACF"}
|
package/lib/runtime.d.ts
CHANGED
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
import { type GameReducer, type HostMessage, type IAction, type IGameState } from "@couch-kit/core";
|
|
2
|
+
/** One connection's share of a {@link GameRuntimeTransport.sendMany} delivery. */
|
|
3
|
+
export interface AddressedMessage {
|
|
4
|
+
connectionId: string;
|
|
5
|
+
message: HostMessage;
|
|
6
|
+
}
|
|
2
7
|
/** Minimal message-delivery surface required by the authoritative runtime. */
|
|
3
8
|
export interface GameRuntimeTransport {
|
|
4
9
|
send(connectionId: string, message: HostMessage): void;
|
|
5
10
|
broadcast(message: HostMessage): void;
|
|
11
|
+
/**
|
|
12
|
+
* Delivers a batch of per-connection messages, for transports that can carry
|
|
13
|
+
* them in one frame.
|
|
14
|
+
*
|
|
15
|
+
* Optional: the runtime falls back to a {@link GameRuntimeTransport.send} per
|
|
16
|
+
* entry, which is what a plain LAN WebSocket transport wants anyway. It earns
|
|
17
|
+
* its keep on the relay, where each frame the display sends is separately
|
|
18
|
+
* billed and rate-limited, so a projected state update costs one message
|
|
19
|
+
* rather than one per player.
|
|
20
|
+
*/
|
|
21
|
+
sendMany?(entries: readonly AddressedMessage[]): void;
|
|
6
22
|
}
|
|
7
23
|
/** Configuration shared by every authoritative Couch Kit host transport. */
|
|
8
24
|
export interface GameHostRuntimeConfig<S extends IGameState, A extends IAction> {
|
|
9
25
|
initialState: S;
|
|
10
26
|
reducer: GameReducer<S, A>;
|
|
27
|
+
/**
|
|
28
|
+
* Narrows the authoritative state to what one player is allowed to see.
|
|
29
|
+
*
|
|
30
|
+
* Without it, every player receives the whole state and any hiding is left to
|
|
31
|
+
* the client — which makes hidden information a convention rather than a
|
|
32
|
+
* rule. With it, the data a player must not see never reaches their device:
|
|
33
|
+
* the runtime sends each connection its own projection instead of one shared
|
|
34
|
+
* broadcast.
|
|
35
|
+
*
|
|
36
|
+
* Omit for games with no hidden information; state is broadcast as before.
|
|
37
|
+
*
|
|
38
|
+
* The projection is what clients receive, so their state type is the *view*,
|
|
39
|
+
* not `S`. A client cannot run the game reducer over a partial view, so
|
|
40
|
+
* `useGameClient` must be used without a `reducer` (no optimistic updates)
|
|
41
|
+
* when a projection is configured.
|
|
42
|
+
*
|
|
43
|
+
* @param state - Current authoritative state.
|
|
44
|
+
* @param playerId - Player the projection is for.
|
|
45
|
+
*/
|
|
46
|
+
project?: (state: S, playerId: string) => unknown;
|
|
11
47
|
debug?: boolean;
|
|
12
48
|
/** Timeout before a disconnected player is permanently removed. */
|
|
13
49
|
disconnectTimeout?: number;
|
|
@@ -59,6 +95,11 @@ export declare class GameHostRuntime<S extends IGameState, A extends IAction> {
|
|
|
59
95
|
/** Cancels runtime timers and releases transport-specific state. */
|
|
60
96
|
stop(): void;
|
|
61
97
|
private applyAction;
|
|
98
|
+
/**
|
|
99
|
+
* What `playerId` is allowed to see. Identity unless a projection is
|
|
100
|
+
* configured, so games without hidden information are unaffected.
|
|
101
|
+
*/
|
|
102
|
+
private viewFor;
|
|
62
103
|
private readonly broadcastState;
|
|
63
104
|
private send;
|
|
64
105
|
private invokeLifecycleCallback;
|
package/lib/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,UAAU,EAEhB,MAAM,iBAAiB,CAAC;AAczB,8EAA8E;AAC9E,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IACvD,SAAS,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,UAAU,EACf,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,qBAAqB,CAAC,MAAM;IAC3C,UAAU,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1D,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AASD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,yBAAyB,CAAC,MAAM;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,oBAAoB,CAAC,EAAE,MAAM,MAAM,CAAC;IACpC,SAAS,CAAC,EAAE,qBAAqB,CAAC,MAAM,CAAC,CAAC;IAC1C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACrD,oBAAoB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CACnD;AAED,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,UAAU,IAAI;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,SAAS,UAAU,IACpD;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACnD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;CAC3B,CAAC;AAEN,KAAK,aAAa,CAAC,CAAC,SAAS,UAAU,IAAI,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AAE/E;;;GAGG;AACH,qBAAa,kBAAkB,CAAC,MAAM,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC;IACpE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA6B;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAC1D,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAe;IACpD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAsC;IACvE,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAA6B;
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@couch-kit/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"provenance": true
|
|
@@ -52,14 +52,13 @@
|
|
|
52
52
|
"build": "bun build ./src/index.ts --outdir ./dist --target browser --external @couch-kit/core && bun build ./src/index.ts --outfile ./dist/index.cjs --target browser --format cjs --external @couch-kit/core && tsc -p tsconfig.build.json",
|
|
53
53
|
"prepublishOnly": "bun run build",
|
|
54
54
|
"test": "bun test",
|
|
55
|
-
"lint": "eslint src/",
|
|
56
55
|
"typecheck": "tsc --noEmit",
|
|
57
56
|
"clean": "rm -rf dist lib"
|
|
58
57
|
},
|
|
59
58
|
"dependencies": {
|
|
60
|
-
"@couch-kit/core": "0.
|
|
59
|
+
"@couch-kit/core": "0.10.0"
|
|
61
60
|
},
|
|
62
61
|
"devDependencies": {
|
|
63
|
-
"typescript": "^
|
|
62
|
+
"typescript": "^7.0.0"
|
|
64
63
|
}
|
|
65
64
|
}
|
package/src/runtime.ts
CHANGED
|
@@ -23,10 +23,27 @@ import {
|
|
|
23
23
|
type JoinSessionPayload,
|
|
24
24
|
} from "./session-manager.js";
|
|
25
25
|
|
|
26
|
+
/** One connection's share of a {@link GameRuntimeTransport.sendMany} delivery. */
|
|
27
|
+
export interface AddressedMessage {
|
|
28
|
+
connectionId: string;
|
|
29
|
+
message: HostMessage;
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
/** Minimal message-delivery surface required by the authoritative runtime. */
|
|
27
33
|
export interface GameRuntimeTransport {
|
|
28
34
|
send(connectionId: string, message: HostMessage): void;
|
|
29
35
|
broadcast(message: HostMessage): void;
|
|
36
|
+
/**
|
|
37
|
+
* Delivers a batch of per-connection messages, for transports that can carry
|
|
38
|
+
* them in one frame.
|
|
39
|
+
*
|
|
40
|
+
* Optional: the runtime falls back to a {@link GameRuntimeTransport.send} per
|
|
41
|
+
* entry, which is what a plain LAN WebSocket transport wants anyway. It earns
|
|
42
|
+
* its keep on the relay, where each frame the display sends is separately
|
|
43
|
+
* billed and rate-limited, so a projected state update costs one message
|
|
44
|
+
* rather than one per player.
|
|
45
|
+
*/
|
|
46
|
+
sendMany?(entries: readonly AddressedMessage[]): void;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
/** Configuration shared by every authoritative Couch Kit host transport. */
|
|
@@ -36,6 +53,26 @@ export interface GameHostRuntimeConfig<
|
|
|
36
53
|
> {
|
|
37
54
|
initialState: S;
|
|
38
55
|
reducer: GameReducer<S, A>;
|
|
56
|
+
/**
|
|
57
|
+
* Narrows the authoritative state to what one player is allowed to see.
|
|
58
|
+
*
|
|
59
|
+
* Without it, every player receives the whole state and any hiding is left to
|
|
60
|
+
* the client — which makes hidden information a convention rather than a
|
|
61
|
+
* rule. With it, the data a player must not see never reaches their device:
|
|
62
|
+
* the runtime sends each connection its own projection instead of one shared
|
|
63
|
+
* broadcast.
|
|
64
|
+
*
|
|
65
|
+
* Omit for games with no hidden information; state is broadcast as before.
|
|
66
|
+
*
|
|
67
|
+
* The projection is what clients receive, so their state type is the *view*,
|
|
68
|
+
* not `S`. A client cannot run the game reducer over a partial view, so
|
|
69
|
+
* `useGameClient` must be used without a `reducer` (no optimistic updates)
|
|
70
|
+
* when a projection is configured.
|
|
71
|
+
*
|
|
72
|
+
* @param state - Current authoritative state.
|
|
73
|
+
* @param playerId - Player the projection is for.
|
|
74
|
+
*/
|
|
75
|
+
project?: (state: S, playerId: string) => unknown;
|
|
39
76
|
debug?: boolean;
|
|
40
77
|
/** Timeout before a disconnected player is permanently removed. */
|
|
41
78
|
disconnectTimeout?: number;
|
|
@@ -215,7 +252,7 @@ export class GameHostRuntime<S extends IGameState, A extends IAction> {
|
|
|
215
252
|
type: MessageTypes.RECONNECTED,
|
|
216
253
|
payload: {
|
|
217
254
|
playerId,
|
|
218
|
-
state: this.
|
|
255
|
+
state: this.viewFor(playerId),
|
|
219
256
|
},
|
|
220
257
|
});
|
|
221
258
|
} else {
|
|
@@ -223,7 +260,7 @@ export class GameHostRuntime<S extends IGameState, A extends IAction> {
|
|
|
223
260
|
type: MessageTypes.WELCOME,
|
|
224
261
|
payload: {
|
|
225
262
|
playerId,
|
|
226
|
-
state: this.
|
|
263
|
+
state: this.viewFor(playerId),
|
|
227
264
|
serverTime: Date.now(),
|
|
228
265
|
},
|
|
229
266
|
});
|
|
@@ -374,13 +411,50 @@ export class GameHostRuntime<S extends IGameState, A extends IAction> {
|
|
|
374
411
|
this.broadcastScheduler.schedule(this.broadcastState);
|
|
375
412
|
}
|
|
376
413
|
|
|
414
|
+
/**
|
|
415
|
+
* What `playerId` is allowed to see. Identity unless a projection is
|
|
416
|
+
* configured, so games without hidden information are unaffected.
|
|
417
|
+
*/
|
|
418
|
+
private viewFor(playerId: string): unknown {
|
|
419
|
+
const project = this.config.project;
|
|
420
|
+
return project ? project(this.state, playerId) : this.state;
|
|
421
|
+
}
|
|
422
|
+
|
|
377
423
|
private readonly broadcastState = (): void => {
|
|
378
|
-
|
|
424
|
+
const transport = this.transport;
|
|
425
|
+
if (!transport) return;
|
|
379
426
|
|
|
380
427
|
const actions = this.actionQueue;
|
|
381
428
|
this.actionQueue = [];
|
|
382
429
|
this.stateDirty = false;
|
|
383
|
-
|
|
430
|
+
|
|
431
|
+
if (!this.config.project) {
|
|
432
|
+
transport.broadcast(createStateUpdateMessage(this.state, actions));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Projected games get one message per connection rather than a broadcast:
|
|
437
|
+
// a shared frame cannot carry different views. Transports that can batch
|
|
438
|
+
// (see GameRuntimeTransport.sendMany) still put them on the wire as a
|
|
439
|
+
// single frame; the rest fall back to N sends, bounded by players-per-room.
|
|
440
|
+
const entries: AddressedMessage[] = [];
|
|
441
|
+
for (const connectionId of this.joinedConnections) {
|
|
442
|
+
const playerId = this.sessionManager.getPlayerIdForSocket(connectionId);
|
|
443
|
+
if (!playerId) continue;
|
|
444
|
+
entries.push({
|
|
445
|
+
connectionId,
|
|
446
|
+
message: createStateUpdateMessage(this.viewFor(playerId), actions),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (entries.length === 0) return;
|
|
450
|
+
|
|
451
|
+
if (transport.sendMany) {
|
|
452
|
+
transport.sendMany(entries);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
for (const { connectionId, message } of entries) {
|
|
456
|
+
this.send(connectionId, message);
|
|
457
|
+
}
|
|
384
458
|
};
|
|
385
459
|
|
|
386
460
|
private send(connectionId: string, message: HostMessage): void {
|