@couch-kit/host 1.7.13 → 1.7.14
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/lib/action-authorization.d.ts +1 -27
- package/lib/action-authorization.d.ts.map +1 -1
- package/lib/broadcast-scheduler.d.ts +1 -29
- package/lib/broadcast-scheduler.d.ts.map +1 -1
- package/lib/message-validation.d.ts +1 -34
- package/lib/message-validation.d.ts.map +1 -1
- package/lib/provider.d.ts +9 -38
- package/lib/provider.d.ts.map +1 -1
- package/lib/rate-limiter.d.ts +1 -31
- package/lib/rate-limiter.d.ts.map +1 -1
- package/lib/session-manager.d.ts +1 -61
- package/lib/session-manager.d.ts.map +1 -1
- package/package.json +11 -6
- package/src/action-authorization.ts +5 -55
- package/src/broadcast-scheduler.ts +8 -84
- package/src/message-validation.ts +6 -94
- package/src/provider.tsx +92 -371
- package/src/rate-limiter.ts +8 -66
- package/src/session-manager.ts +8 -192
|
@@ -1,28 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export type ActionRejectionCode = "FORBIDDEN_ACTION" | "NOT_JOINED";
|
|
3
|
-
/** Outcome of authorizing an inbound client ACTION message. */
|
|
4
|
-
export type ActionAuthorization = {
|
|
5
|
-
kind: "allow";
|
|
6
|
-
playerId: string;
|
|
7
|
-
} | {
|
|
8
|
-
kind: "reject";
|
|
9
|
-
code: ActionRejectionCode;
|
|
10
|
-
message: string;
|
|
11
|
-
};
|
|
12
|
-
/**
|
|
13
|
-
* Decide whether an inbound client ACTION may be dispatched.
|
|
14
|
-
*
|
|
15
|
-
* Rejects, in order:
|
|
16
|
-
* - **Internal action types** — clients must not be able to inject framework
|
|
17
|
-
* actions (`__HYDRATE__`, `__PLAYER_JOINED__`, etc.) to forge state.
|
|
18
|
-
* - **Un-joined sockets** — a socket with no resolved player ID never completed
|
|
19
|
-
* a JOIN, so its actions have no owner and must not mutate state.
|
|
20
|
-
*
|
|
21
|
-
* The decision is pure so it can be unit-tested without a WebSocket or React.
|
|
22
|
-
*
|
|
23
|
-
* @param actionType - The `type` field of the client's action payload.
|
|
24
|
-
* @param resolvedPlayerId - The player ID cached for the socket at JOIN time, or
|
|
25
|
-
* `undefined` if the socket has not joined.
|
|
26
|
-
*/
|
|
27
|
-
export declare function authorizeClientAction(actionType: string, resolvedPlayerId: string | undefined): ActionAuthorization;
|
|
1
|
+
export { authorizeClientAction, type ActionAuthorization, type ActionRejectionCode, } from "@couch-kit/runtime";
|
|
28
2
|
//# sourceMappingURL=action-authorization.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-authorization.d.ts","sourceRoot":"","sources":["../src/action-authorization.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"action-authorization.d.ts","sourceRoot":"","sources":["../src/action-authorization.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC"}
|
|
@@ -1,30 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/** Default state broadcast throttle (~30fps). */
|
|
3
|
-
export declare const DEFAULT_STATE_THROTTLE_MS = 33;
|
|
4
|
-
export type StateUpdateMessage = Extract<HostMessage, {
|
|
5
|
-
type: typeof MessageTypes.STATE_UPDATE;
|
|
6
|
-
}>;
|
|
7
|
-
export interface TimerScheduler<TTimer> {
|
|
8
|
-
setTimeout(callback: () => void, delayMs: number): TTimer;
|
|
9
|
-
clearTimeout(timer: TTimer): void;
|
|
10
|
-
}
|
|
11
|
-
export interface BroadcastSchedulerOptions<TTimer> {
|
|
12
|
-
stateThrottleMs?: number;
|
|
13
|
-
scheduler?: TimerScheduler<TTimer>;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Debounced state-broadcast scheduler used by the host provider.
|
|
17
|
-
* Rapid state changes are coalesced into one broadcast after the latest change.
|
|
18
|
-
*/
|
|
19
|
-
export declare class BroadcastScheduler<TTimer = ReturnType<typeof setTimeout>> {
|
|
20
|
-
private stateThrottleMs;
|
|
21
|
-
private readonly scheduler;
|
|
22
|
-
private timer;
|
|
23
|
-
constructor(options?: BroadcastSchedulerOptions<TTimer>);
|
|
24
|
-
schedule(callback: () => void): void;
|
|
25
|
-
cancel(): void;
|
|
26
|
-
setStateThrottleMs(stateThrottleMs: number): void;
|
|
27
|
-
hasPendingBroadcast(): boolean;
|
|
28
|
-
}
|
|
29
|
-
export declare function createStateUpdateMessage(newState: unknown, actions: readonly unknown[], timestamp?: number): StateUpdateMessage;
|
|
1
|
+
export { BroadcastScheduler, DEFAULT_STATE_THROTTLE_MS, createStateUpdateMessage, type BroadcastSchedulerOptions, type StateUpdateMessage, type TimerScheduler, } from "@couch-kit/runtime";
|
|
30
2
|
//# sourceMappingURL=broadcast-scheduler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"broadcast-scheduler.d.ts","sourceRoot":"","sources":["../src/broadcast-scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"broadcast-scheduler.d.ts","sourceRoot":"","sources":["../src/broadcast-scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACvB,KAAK,cAAc,GACpB,MAAM,oBAAoB,CAAC"}
|
|
@@ -1,35 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Default maximum size (in bytes) of an inbound client message. Frames larger
|
|
4
|
-
* than this are discarded before `JSON.parse`, bounding the memory a single
|
|
5
|
-
* malicious LAN client can force the host to allocate. Generous enough for
|
|
6
|
-
* data-URI avatars in JOIN payloads while blocking multi-megabyte abuse.
|
|
7
|
-
*/
|
|
8
|
-
export declare const DEFAULT_MAX_MESSAGE_BYTES: number;
|
|
9
|
-
/**
|
|
10
|
-
* Compute the UTF-8 byte length of a raw WebSocket frame payload.
|
|
11
|
-
*
|
|
12
|
-
* For binary frames the `ArrayBuffer.byteLength` is exact. For text frames the
|
|
13
|
-
* UTF-8 size is counted directly from the JS string without allocating an
|
|
14
|
-
* encoder buffer, so an oversized frame can be rejected cheaply.
|
|
15
|
-
*/
|
|
16
|
-
export declare function frameByteLength(data: string | ArrayBuffer): number;
|
|
17
|
-
type ClientMessageOf<TType extends ClientMessage["type"]> = Extract<ClientMessage, {
|
|
18
|
-
type: TType;
|
|
19
|
-
}>;
|
|
20
|
-
export type ValidatedClientMessage = {
|
|
21
|
-
type: typeof MessageTypes.JOIN;
|
|
22
|
-
payload: {
|
|
23
|
-
name: string;
|
|
24
|
-
secret?: unknown;
|
|
25
|
-
avatar?: unknown;
|
|
26
|
-
[key: string]: unknown;
|
|
27
|
-
};
|
|
28
|
-
} | ClientMessageOf<"ACTION"> | ClientMessageOf<"PING"> | ClientMessageOf<"ASSETS_LOADED">;
|
|
29
|
-
/**
|
|
30
|
-
* Validates that an incoming message has the expected shape.
|
|
31
|
-
* Returns true if the message has a processable client-message shape.
|
|
32
|
-
*/
|
|
33
|
-
export declare function isValidClientMessage(msg: unknown): msg is ValidatedClientMessage;
|
|
34
|
-
export {};
|
|
1
|
+
export { DEFAULT_MAX_MESSAGE_BYTES, frameByteLength, isValidClientMessage, type ValidatedClientMessage, } from "@couch-kit/runtime";
|
|
35
2
|
//# sourceMappingURL=message-validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message-validation.d.ts","sourceRoot":"","sources":["../src/message-validation.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"message-validation.d.ts","sourceRoot":"","sources":["../src/message-validation.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EACzB,eAAe,EACf,oBAAoB,EACpB,KAAK,sBAAsB,GAC5B,MAAM,oBAAoB,CAAC"}
|
package/lib/provider.d.ts
CHANGED
|
@@ -1,29 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
reducer: (state: S, action: A) => S;
|
|
2
|
+
import { type IAction, type IGameState } from "@couch-kit/core";
|
|
3
|
+
import { type GameHostRuntimeConfig } from "@couch-kit/runtime";
|
|
4
|
+
export interface GameHostConfig<S extends IGameState, A extends IAction> extends GameHostRuntimeConfig<S, A> {
|
|
6
5
|
port?: number;
|
|
7
6
|
wsPort?: number;
|
|
8
7
|
devMode?: boolean;
|
|
9
8
|
devServerUrl?: string;
|
|
10
9
|
staticDir?: string;
|
|
11
|
-
debug?: boolean;
|
|
12
|
-
/** Timeout (ms) before a disconnected player is permanently removed (default: 5 minutes). */
|
|
13
|
-
disconnectTimeout?: number;
|
|
14
|
-
/** State broadcast throttle interval in milliseconds (default: 33ms, ~30fps). */
|
|
15
|
-
stateThrottleMs?: number;
|
|
16
10
|
/**
|
|
17
|
-
* Maximum size
|
|
18
|
-
*
|
|
11
|
+
* Maximum size of an inbound client message before parsing.
|
|
12
|
+
* Defaults to 256 KiB.
|
|
19
13
|
*/
|
|
20
14
|
maxMessageBytes?: number;
|
|
21
|
-
/** Called when a player successfully joins. */
|
|
22
|
-
onPlayerJoined?: (playerId: string, name: string) => void;
|
|
23
|
-
/** Called when a player disconnects. */
|
|
24
|
-
onPlayerLeft?: (playerId: string) => void;
|
|
25
|
-
/** Called when a server error occurs. */
|
|
26
|
-
onError?: (error: Error) => void;
|
|
27
15
|
}
|
|
28
16
|
interface GameHostContextValue<S extends IGameState, A extends IAction> {
|
|
29
17
|
state: S;
|
|
@@ -32,34 +20,17 @@ interface GameHostContextValue<S extends IGameState, A extends IAction> {
|
|
|
32
20
|
serverError: Error | null;
|
|
33
21
|
}
|
|
34
22
|
/**
|
|
35
|
-
* React
|
|
23
|
+
* React Native adapter for the transport-neutral authoritative game runtime.
|
|
36
24
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* reducer and broadcasts state updates to all connected clients.
|
|
40
|
-
*
|
|
41
|
-
* @param config - Host configuration including reducer, initial state, ports, and callbacks.
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* ```tsx
|
|
45
|
-
* <GameHostProvider config={{ reducer: gameReducer, initialState }}>
|
|
46
|
-
* <GameScreen />
|
|
47
|
-
* </GameHostProvider>
|
|
48
|
-
* ```
|
|
25
|
+
* The runtime owns canonical state and protocol behavior. This provider starts
|
|
26
|
+
* the native static/WebSocket servers and exposes runtime state through React.
|
|
49
27
|
*/
|
|
50
28
|
export declare function GameHostProvider<S extends IGameState, A extends IAction>({ children, config, }: {
|
|
51
29
|
children: React.ReactNode;
|
|
52
30
|
config: GameHostConfig<S, A>;
|
|
53
31
|
}): React.JSX.Element;
|
|
54
32
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* Must be used within a `<GameHostProvider>`. Returns the canonical game state,
|
|
58
|
-
* a dispatch function for actions, the server URL (for QR codes), and any
|
|
59
|
-
* server startup errors.
|
|
60
|
-
*
|
|
61
|
-
* @returns An object with `state`, `dispatch`, `serverUrl`, and `serverError`.
|
|
62
|
-
* @throws If used outside of a `<GameHostProvider>`.
|
|
33
|
+
* Accesses canonical host state, trusted dispatch, and controller server URL.
|
|
63
34
|
*/
|
|
64
35
|
export declare function useGameHost<S extends IGameState, A extends IAction>(): GameHostContextValue<S, A>;
|
|
65
36
|
export {};
|
package/lib/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AACf,OAAO,EAGL,KAAK,OAAO,EACZ,KAAK,UAAU,EAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,oBAAoB,CAAC;AAI5B,MAAM,WAAW,cAAc,CAC7B,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,OAAO,CACjB,SAAQ,qBAAqB,CAAC,CAAC,EAAE,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,oBAAoB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO;IACpE,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,KAAK,GAAG,IAAI,CAAC;CAC3B;AAOD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,EAAE,EACxE,QAAQ,EACR,MAAM,GACP,EAAE;IACD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;CAC9B,qBA4HA;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,UAAU,EAAE,CAAC,SAAS,OAAO,KAK/C,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC,CAC7C"}
|
package/lib/rate-limiter.d.ts
CHANGED
|
@@ -1,32 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export declare const RATE_LIMIT_MAX = 60;
|
|
3
|
-
/** Rate-limit window duration (ms). */
|
|
4
|
-
export declare const RATE_LIMIT_WINDOW = 1000;
|
|
5
|
-
export interface RateLimitInfo {
|
|
6
|
-
count: number;
|
|
7
|
-
windowStart: number;
|
|
8
|
-
}
|
|
9
|
-
export interface RateLimitResult extends RateLimitInfo {
|
|
10
|
-
allowed: boolean;
|
|
11
|
-
}
|
|
12
|
-
export interface ActionRateLimiterOptions {
|
|
13
|
-
maxActions?: number;
|
|
14
|
-
windowMs?: number;
|
|
15
|
-
now?: () => number;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Per-socket action limiter that preserves the host provider's original
|
|
19
|
-
* fixed-window algorithm.
|
|
20
|
-
*/
|
|
21
|
-
export declare class ActionRateLimiter {
|
|
22
|
-
private readonly maxActions;
|
|
23
|
-
private readonly windowMs;
|
|
24
|
-
private readonly now;
|
|
25
|
-
private readonly limits;
|
|
26
|
-
constructor(options?: ActionRateLimiterOptions);
|
|
27
|
-
record(socketId: string): RateLimitResult;
|
|
28
|
-
reset(socketId: string): void;
|
|
29
|
-
clear(): void;
|
|
30
|
-
get(socketId: string): RateLimitInfo | undefined;
|
|
31
|
-
}
|
|
1
|
+
export { ActionRateLimiter, RATE_LIMIT_MAX, RATE_LIMIT_WINDOW, type ActionRateLimiterOptions, type RateLimitInfo, type RateLimitResult, } from "@couch-kit/runtime";
|
|
32
2
|
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../src/rate-limiter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,eAAe,GACrB,MAAM,oBAAoB,CAAC"}
|
package/lib/session-manager.d.ts
CHANGED
|
@@ -1,62 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export interface SessionTimerScheduler<TTimer> {
|
|
3
|
-
setTimeout(callback: () => void, delayMs: number): TTimer;
|
|
4
|
-
clearTimeout(timer: TTimer): void;
|
|
5
|
-
}
|
|
6
|
-
export interface JoinSessionPayload {
|
|
7
|
-
name: string;
|
|
8
|
-
avatar?: string;
|
|
9
|
-
secret: string;
|
|
10
|
-
}
|
|
11
|
-
export interface HostSessionManagerOptions<TTimer> {
|
|
12
|
-
disconnectTimeout?: number;
|
|
13
|
-
getDisconnectTimeout?: () => number;
|
|
14
|
-
scheduler?: SessionTimerScheduler<TTimer>;
|
|
15
|
-
derivePlayerId?: (secret: string) => Promise<string>;
|
|
16
|
-
derivePlayerIdLegacy?: (secret: string) => string;
|
|
17
|
-
}
|
|
18
|
-
export type JoinSessionResult<S extends IGameState> = {
|
|
19
|
-
playerId: string;
|
|
20
|
-
socketId: string;
|
|
21
|
-
secret: string;
|
|
22
|
-
isReconnect: boolean;
|
|
23
|
-
action: InternalAction<S>;
|
|
24
|
-
};
|
|
25
|
-
export type DisconnectSessionResult<S extends IGameState> = {
|
|
26
|
-
kind: "unknown";
|
|
27
|
-
} | {
|
|
28
|
-
kind: "stale";
|
|
29
|
-
playerId: string;
|
|
30
|
-
secret: string;
|
|
31
|
-
} | {
|
|
32
|
-
kind: "left";
|
|
33
|
-
playerId: string;
|
|
34
|
-
secret: string;
|
|
35
|
-
action: InternalAction<S>;
|
|
36
|
-
};
|
|
37
|
-
type PlayersSource<S extends IGameState> = S["players"] | (() => S["players"]);
|
|
38
|
-
/**
|
|
39
|
-
* Tracks host player sessions by secret and socket ID, including reconnects and
|
|
40
|
-
* delayed cleanup of disconnected players.
|
|
41
|
-
*/
|
|
42
|
-
export declare class HostSessionManager<TTimer = ReturnType<typeof setTimeout>> {
|
|
43
|
-
private readonly sessions;
|
|
44
|
-
private readonly reverseMap;
|
|
45
|
-
private readonly cleanupTimers;
|
|
46
|
-
private readonly socketIdToPlayerId;
|
|
47
|
-
private readonly scheduler;
|
|
48
|
-
private readonly getDisconnectTimeout;
|
|
49
|
-
private readonly derivePlayerIdFn;
|
|
50
|
-
private readonly derivePlayerIdLegacyFn;
|
|
51
|
-
constructor(options?: HostSessionManagerOptions<TTimer>);
|
|
52
|
-
handleJoin<S extends IGameState>(socketId: string, payload: JoinSessionPayload, playersSource: PlayersSource<S>): Promise<JoinSessionResult<S>>;
|
|
53
|
-
handleDisconnect<S extends IGameState>(socketId: string): DisconnectSessionResult<S>;
|
|
54
|
-
scheduleRemoval(playerId: string, secret: string, onRemove: (playerId: string) => void): void;
|
|
55
|
-
cancelRemoval(playerId: string): void;
|
|
56
|
-
clearRemovalTimers(): void;
|
|
57
|
-
getPlayerIdForSocket(socketId: string): string | undefined;
|
|
58
|
-
getSocketIdForSecret(secret: string): string | undefined;
|
|
59
|
-
hasPendingRemoval(playerId: string): boolean;
|
|
60
|
-
}
|
|
61
|
-
export {};
|
|
1
|
+
export { HostSessionManager, type DisconnectSessionResult, type HostSessionManagerOptions, type JoinSessionPayload, type JoinSessionResult, type SessionTimerScheduler, } from "@couch-kit/runtime";
|
|
62
2
|
//# sourceMappingURL=session-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../src/session-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@couch-kit/host",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.14",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"provenance": true
|
|
@@ -58,17 +58,22 @@
|
|
|
58
58
|
"prepublishOnly": "bun run build"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@couch-kit/core": "0.9.
|
|
61
|
+
"@couch-kit/core": "0.9.3",
|
|
62
|
+
"@couch-kit/runtime": "0.1.0",
|
|
62
63
|
"buffer": "^6.0.3",
|
|
63
64
|
"js-sha1": "^0.7.0",
|
|
64
65
|
"react-native-nitro-http-server": "^1.6.1"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
68
|
+
"@happy-dom/global-registrator": "^20.10.6",
|
|
69
|
+
"@testing-library/react": "^16.3.2",
|
|
67
70
|
"@types/react": "^19.2.17",
|
|
68
|
-
"react": "19
|
|
69
|
-
"react
|
|
70
|
-
"
|
|
71
|
-
"
|
|
71
|
+
"@types/react-dom": "^19",
|
|
72
|
+
"react": "19.2.7",
|
|
73
|
+
"react-dom": "19.2.7",
|
|
74
|
+
"react-native": "0.86.0",
|
|
75
|
+
"del-cli": "^7.0.0",
|
|
76
|
+
"jest": "^30.0.0"
|
|
72
77
|
},
|
|
73
78
|
"peerDependencies": {
|
|
74
79
|
"react": ">=18.2.0",
|
|
@@ -1,55 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/** Outcome of authorizing an inbound client ACTION message. */
|
|
7
|
-
export type ActionAuthorization =
|
|
8
|
-
| { kind: "allow"; playerId: string }
|
|
9
|
-
| { kind: "reject"; code: ActionRejectionCode; message: string };
|
|
10
|
-
|
|
11
|
-
const INTERNAL_ACTION_TYPES = new Set<string>([
|
|
12
|
-
InternalActionTypes.HYDRATE,
|
|
13
|
-
InternalActionTypes.PLAYER_JOINED,
|
|
14
|
-
InternalActionTypes.PLAYER_LEFT,
|
|
15
|
-
InternalActionTypes.PLAYER_RECONNECTED,
|
|
16
|
-
InternalActionTypes.PLAYER_REMOVED,
|
|
17
|
-
]);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Decide whether an inbound client ACTION may be dispatched.
|
|
21
|
-
*
|
|
22
|
-
* Rejects, in order:
|
|
23
|
-
* - **Internal action types** — clients must not be able to inject framework
|
|
24
|
-
* actions (`__HYDRATE__`, `__PLAYER_JOINED__`, etc.) to forge state.
|
|
25
|
-
* - **Un-joined sockets** — a socket with no resolved player ID never completed
|
|
26
|
-
* a JOIN, so its actions have no owner and must not mutate state.
|
|
27
|
-
*
|
|
28
|
-
* The decision is pure so it can be unit-tested without a WebSocket or React.
|
|
29
|
-
*
|
|
30
|
-
* @param actionType - The `type` field of the client's action payload.
|
|
31
|
-
* @param resolvedPlayerId - The player ID cached for the socket at JOIN time, or
|
|
32
|
-
* `undefined` if the socket has not joined.
|
|
33
|
-
*/
|
|
34
|
-
export function authorizeClientAction(
|
|
35
|
-
actionType: string,
|
|
36
|
-
resolvedPlayerId: string | undefined,
|
|
37
|
-
): ActionAuthorization {
|
|
38
|
-
if (INTERNAL_ACTION_TYPES.has(actionType)) {
|
|
39
|
-
return {
|
|
40
|
-
kind: "reject",
|
|
41
|
-
code: "FORBIDDEN_ACTION",
|
|
42
|
-
message: "Internal action types cannot be dispatched by clients",
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!resolvedPlayerId) {
|
|
47
|
-
return {
|
|
48
|
-
kind: "reject",
|
|
49
|
-
code: "NOT_JOINED",
|
|
50
|
-
message: "You must JOIN before dispatching actions",
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return { kind: "allow", playerId: resolvedPlayerId };
|
|
55
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
authorizeClientAction,
|
|
3
|
+
type ActionAuthorization,
|
|
4
|
+
type ActionRejectionCode,
|
|
5
|
+
} from "@couch-kit/runtime";
|
|
@@ -1,84 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
>;
|
|
10
|
-
|
|
11
|
-
export interface TimerScheduler<TTimer> {
|
|
12
|
-
setTimeout(callback: () => void, delayMs: number): TTimer;
|
|
13
|
-
clearTimeout(timer: TTimer): void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const defaultTimerScheduler: TimerScheduler<ReturnType<typeof setTimeout>> = {
|
|
17
|
-
setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
18
|
-
clearTimeout: (timer) => clearTimeout(timer),
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export interface BroadcastSchedulerOptions<TTimer> {
|
|
22
|
-
stateThrottleMs?: number;
|
|
23
|
-
scheduler?: TimerScheduler<TTimer>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Debounced state-broadcast scheduler used by the host provider.
|
|
28
|
-
* Rapid state changes are coalesced into one broadcast after the latest change.
|
|
29
|
-
*/
|
|
30
|
-
export class BroadcastScheduler<TTimer = ReturnType<typeof setTimeout>> {
|
|
31
|
-
private stateThrottleMs: number;
|
|
32
|
-
private readonly scheduler: TimerScheduler<TTimer>;
|
|
33
|
-
private timer: TTimer | null = null;
|
|
34
|
-
|
|
35
|
-
constructor(options: BroadcastSchedulerOptions<TTimer> = {}) {
|
|
36
|
-
this.stateThrottleMs =
|
|
37
|
-
options.stateThrottleMs ?? DEFAULT_STATE_THROTTLE_MS;
|
|
38
|
-
this.scheduler =
|
|
39
|
-
options.scheduler ??
|
|
40
|
-
(defaultTimerScheduler as unknown as TimerScheduler<TTimer>);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
schedule(callback: () => void): void {
|
|
44
|
-
this.cancel();
|
|
45
|
-
this.timer = this.scheduler.setTimeout(() => {
|
|
46
|
-
this.timer = null;
|
|
47
|
-
callback();
|
|
48
|
-
}, this.stateThrottleMs);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
cancel(): void {
|
|
52
|
-
if (this.timer) {
|
|
53
|
-
this.scheduler.clearTimeout(this.timer);
|
|
54
|
-
this.timer = null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
setStateThrottleMs(stateThrottleMs: number): void {
|
|
59
|
-
this.stateThrottleMs = stateThrottleMs;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
hasPendingBroadcast(): boolean {
|
|
63
|
-
return this.timer !== null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function createStateUpdateMessage(
|
|
68
|
-
newState: unknown,
|
|
69
|
-
actions: readonly unknown[],
|
|
70
|
-
timestamp: number = Date.now(),
|
|
71
|
-
): StateUpdateMessage {
|
|
72
|
-
return {
|
|
73
|
-
type: MessageTypes.STATE_UPDATE,
|
|
74
|
-
payload: {
|
|
75
|
-
newState,
|
|
76
|
-
timestamp,
|
|
77
|
-
...(actions.length === 1
|
|
78
|
-
? { action: actions[0] }
|
|
79
|
-
: actions.length > 1
|
|
80
|
-
? { action: actions }
|
|
81
|
-
: {}),
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
BroadcastScheduler,
|
|
3
|
+
DEFAULT_STATE_THROTTLE_MS,
|
|
4
|
+
createStateUpdateMessage,
|
|
5
|
+
type BroadcastSchedulerOptions,
|
|
6
|
+
type StateUpdateMessage,
|
|
7
|
+
type TimerScheduler,
|
|
8
|
+
} from "@couch-kit/runtime";
|
|
@@ -1,94 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* data-URI avatars in JOIN payloads while blocking multi-megabyte abuse.
|
|
8
|
-
*/
|
|
9
|
-
export const DEFAULT_MAX_MESSAGE_BYTES = 256 * 1024;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Compute the UTF-8 byte length of a raw WebSocket frame payload.
|
|
13
|
-
*
|
|
14
|
-
* For binary frames the `ArrayBuffer.byteLength` is exact. For text frames the
|
|
15
|
-
* UTF-8 size is counted directly from the JS string without allocating an
|
|
16
|
-
* encoder buffer, so an oversized frame can be rejected cheaply.
|
|
17
|
-
*/
|
|
18
|
-
export function frameByteLength(data: string | ArrayBuffer): number {
|
|
19
|
-
if (typeof data !== "string") return data.byteLength;
|
|
20
|
-
|
|
21
|
-
let bytes = 0;
|
|
22
|
-
for (let i = 0; i < data.length; i++) {
|
|
23
|
-
const code = data.charCodeAt(i);
|
|
24
|
-
if (code < 0x80) {
|
|
25
|
-
bytes += 1;
|
|
26
|
-
} else if (code < 0x800) {
|
|
27
|
-
bytes += 2;
|
|
28
|
-
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
29
|
-
// High surrogate: a full pair encodes to 4 UTF-8 bytes.
|
|
30
|
-
bytes += 4;
|
|
31
|
-
i++; // Skip the paired low surrogate.
|
|
32
|
-
} else {
|
|
33
|
-
bytes += 3;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return bytes;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type ClientMessageOf<TType extends ClientMessage["type"]> = Extract<
|
|
40
|
-
ClientMessage,
|
|
41
|
-
{ type: TType }
|
|
42
|
-
>;
|
|
43
|
-
|
|
44
|
-
export type ValidatedClientMessage =
|
|
45
|
-
| {
|
|
46
|
-
type: typeof MessageTypes.JOIN;
|
|
47
|
-
payload: {
|
|
48
|
-
name: string;
|
|
49
|
-
secret?: unknown;
|
|
50
|
-
avatar?: unknown;
|
|
51
|
-
[key: string]: unknown;
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
| ClientMessageOf<"ACTION">
|
|
55
|
-
| ClientMessageOf<"PING">
|
|
56
|
-
| ClientMessageOf<"ASSETS_LOADED">;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Validates that an incoming message has the expected shape.
|
|
60
|
-
* Returns true if the message has a processable client-message shape.
|
|
61
|
-
*/
|
|
62
|
-
export function isValidClientMessage(
|
|
63
|
-
msg: unknown,
|
|
64
|
-
): msg is ValidatedClientMessage {
|
|
65
|
-
if (typeof msg !== "object" || msg === null) return false;
|
|
66
|
-
const m = msg as Record<string, unknown>;
|
|
67
|
-
if (typeof m.type !== "string") return false;
|
|
68
|
-
|
|
69
|
-
switch (m.type) {
|
|
70
|
-
case MessageTypes.JOIN:
|
|
71
|
-
return (
|
|
72
|
-
typeof m.payload === "object" &&
|
|
73
|
-
m.payload !== null &&
|
|
74
|
-
typeof (m.payload as Record<string, unknown>).name === "string"
|
|
75
|
-
);
|
|
76
|
-
case MessageTypes.ACTION:
|
|
77
|
-
return (
|
|
78
|
-
typeof m.payload === "object" &&
|
|
79
|
-
m.payload !== null &&
|
|
80
|
-
typeof (m.payload as Record<string, unknown>).type === "string"
|
|
81
|
-
);
|
|
82
|
-
case MessageTypes.PING:
|
|
83
|
-
return (
|
|
84
|
-
typeof m.payload === "object" &&
|
|
85
|
-
m.payload !== null &&
|
|
86
|
-
typeof (m.payload as Record<string, unknown>).id === "string" &&
|
|
87
|
-
typeof (m.payload as Record<string, unknown>).timestamp === "number"
|
|
88
|
-
);
|
|
89
|
-
case MessageTypes.ASSETS_LOADED:
|
|
90
|
-
return m.payload === true;
|
|
91
|
-
default:
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
DEFAULT_MAX_MESSAGE_BYTES,
|
|
3
|
+
frameByteLength,
|
|
4
|
+
isValidClientMessage,
|
|
5
|
+
type ValidatedClientMessage,
|
|
6
|
+
} from "@couch-kit/runtime";
|