@hadoku/rooms-protocol 0.0.0-stage → 0.43.15
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/LICENSE +21 -0
- package/dist/game-bundle.d.ts +73 -0
- package/dist/game-bundle.d.ts.map +1 -0
- package/dist/game-bundle.js +30 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/participantId.d.ts +159 -0
- package/dist/participantId.d.ts.map +1 -0
- package/dist/participantId.js +209 -0
- package/dist/resync.d.ts +117 -0
- package/dist/resync.d.ts.map +1 -0
- package/dist/resync.js +110 -0
- package/dist/seats.d.ts +269 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +276 -0
- package/package.json +22 -4
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Matthaeus Wolff
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The game-bundle contract: how a room host mounts a game it knows nothing
|
|
3
|
+
* about.
|
|
4
|
+
*
|
|
5
|
+
* A host (watchparty's room) holds a list of `GameEntry` rows as DATA. For a
|
|
6
|
+
* `module` entry it dynamically imports `url`, expects the module to export
|
|
7
|
+
* `mount` (a `GameBundleModule`), and hands it an element plus a context. The
|
|
8
|
+
* bundle renders into that element — inside its own shadow root, so neither
|
|
9
|
+
* side's styles reach the other — and talks to its own server; the host only
|
|
10
|
+
* relays who is looking, what theme they are in, and whether they lead.
|
|
11
|
+
*
|
|
12
|
+
* Adding a game is therefore a data edit on the host, never host code.
|
|
13
|
+
* `iframe` is the one built-in kind: the host's own cross-origin iframe game
|
|
14
|
+
* (Bomberman), kept as it was.
|
|
15
|
+
*/
|
|
16
|
+
/** Who is viewing. The same values the embeddable room chrome receives. */
|
|
17
|
+
export interface GameBundleIdentity {
|
|
18
|
+
/** The viewer's key-registry userId when known, else `public`. A host
|
|
19
|
+
* never hands a bundle the session id: it is a bearer credential. */
|
|
20
|
+
userId: string;
|
|
21
|
+
userType: string;
|
|
22
|
+
/** Display name, or null when the viewer has none yet. */
|
|
23
|
+
name: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface GameBundleContext {
|
|
26
|
+
/** The room the bundle is mounted in: the same value for every
|
|
27
|
+
* participant. It is the room's id, not the key a viewer joined with —
|
|
28
|
+
* a leader joins with a different key, and that key is a credential a
|
|
29
|
+
* bundle must never be handed. */
|
|
30
|
+
roomKey: string;
|
|
31
|
+
identity: GameBundleIdentity;
|
|
32
|
+
/** The current hadoku theme name. */
|
|
33
|
+
theme: string;
|
|
34
|
+
isLeader: boolean;
|
|
35
|
+
/** Ask the host to close the bundle. For the leader this closes it for
|
|
36
|
+
* everyone; for anyone else it closes it for them only. */
|
|
37
|
+
close(): void;
|
|
38
|
+
}
|
|
39
|
+
export interface MountedGameBundle {
|
|
40
|
+
/** Tear down everything `mount` created. Called once. */
|
|
41
|
+
unmount(): void;
|
|
42
|
+
/** The context changed (theme, identity, leadership). */
|
|
43
|
+
update(ctx: GameBundleContext): void;
|
|
44
|
+
}
|
|
45
|
+
/** What a `module` entry's URL must export. */
|
|
46
|
+
export interface GameBundleModule {
|
|
47
|
+
mount(el: HTMLElement, ctx: GameBundleContext): MountedGameBundle | Promise<MountedGameBundle>;
|
|
48
|
+
}
|
|
49
|
+
export type GameEntry =
|
|
50
|
+
/** The host's built-in iframe game. */
|
|
51
|
+
{
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
cover?: string;
|
|
55
|
+
kind: 'iframe';
|
|
56
|
+
}
|
|
57
|
+
/** An ES module exporting `GameBundleModule`, at a same-origin path. */
|
|
58
|
+
| {
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
cover?: string;
|
|
62
|
+
kind: 'module';
|
|
63
|
+
url: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Whether `value` is a module that satisfies `GameBundleModule`. A bundle is
|
|
67
|
+
* loaded from a URL at runtime, so its shape is a claim the host checks rather
|
|
68
|
+
* than a type it can trust.
|
|
69
|
+
*/
|
|
70
|
+
export declare function isGameBundleModule(value: unknown): value is GameBundleModule;
|
|
71
|
+
/** Whether `value` is what a bundle's `mount` must resolve to. */
|
|
72
|
+
export declare function isMountedGameBundle(value: unknown): value is MountedGameBundle;
|
|
73
|
+
//# sourceMappingURL=game-bundle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game-bundle.d.ts","sourceRoot":"","sources":["../src/game-bundle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,2EAA2E;AAC3E,MAAM,WAAW,kBAAkB;IACjC;0EACsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;uCAGmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB;gEAC4D;IAC5D,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,OAAO,IAAI,IAAI,CAAC;IAChB,yDAAyD;IACzD,MAAM,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACtC;AAED,+CAA+C;AAC/C,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAChG;AAED,MAAM,MAAM,SAAS;AACnB,uCAAuC;AACrC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE;AAC9D,wEAAwE;GACtE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAG5E;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAI9E"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The game-bundle contract: how a room host mounts a game it knows nothing
|
|
3
|
+
* about.
|
|
4
|
+
*
|
|
5
|
+
* A host (watchparty's room) holds a list of `GameEntry` rows as DATA. For a
|
|
6
|
+
* `module` entry it dynamically imports `url`, expects the module to export
|
|
7
|
+
* `mount` (a `GameBundleModule`), and hands it an element plus a context. The
|
|
8
|
+
* bundle renders into that element — inside its own shadow root, so neither
|
|
9
|
+
* side's styles reach the other — and talks to its own server; the host only
|
|
10
|
+
* relays who is looking, what theme they are in, and whether they lead.
|
|
11
|
+
*
|
|
12
|
+
* Adding a game is therefore a data edit on the host, never host code.
|
|
13
|
+
* `iframe` is the one built-in kind: the host's own cross-origin iframe game
|
|
14
|
+
* (Bomberman), kept as it was.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Whether `value` is a module that satisfies `GameBundleModule`. A bundle is
|
|
18
|
+
* loaded from a URL at runtime, so its shape is a claim the host checks rather
|
|
19
|
+
* than a type it can trust.
|
|
20
|
+
*/
|
|
21
|
+
export function isGameBundleModule(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null
|
|
23
|
+
&& typeof value.mount === 'function';
|
|
24
|
+
}
|
|
25
|
+
/** Whether `value` is what a bundle's `mount` must resolve to. */
|
|
26
|
+
export function isMountedGameBundle(value) {
|
|
27
|
+
return typeof value === 'object' && value !== null
|
|
28
|
+
&& typeof value.unmount === 'function'
|
|
29
|
+
&& typeof value.update === 'function';
|
|
30
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wolffm/rooms-protocol — the wire contract for rooms.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately dependency-free (not even zod yet) and free of any hosting
|
|
5
|
+
* app's vocabulary: no playhead, media, fragment, playlist, tierlist or
|
|
6
|
+
* sprite. Both `rooms-server` and a client bind to this, so anything that
|
|
7
|
+
* lands here becomes a promise to every consumer.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from watchparty's `@watchparty/shared`, which keeps re-exporting
|
|
10
|
+
* these so the ~40 existing import sites did not all have to move in the same
|
|
11
|
+
* commit as the package boundary.
|
|
12
|
+
*/
|
|
13
|
+
export * from './participantId.js';
|
|
14
|
+
export * from './seats.js';
|
|
15
|
+
export * from './resync.js';
|
|
16
|
+
export * from './game-bundle.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,cAAc,oBAAoB,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wolffm/rooms-protocol — the wire contract for rooms.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately dependency-free (not even zod yet) and free of any hosting
|
|
5
|
+
* app's vocabulary: no playhead, media, fragment, playlist, tierlist or
|
|
6
|
+
* sprite. Both `rooms-server` and a client bind to this, so anything that
|
|
7
|
+
* lands here becomes a promise to every consumer.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from watchparty's `@watchparty/shared`, which keeps re-exporting
|
|
10
|
+
* these so the ~40 existing import sites did not all have to move in the same
|
|
11
|
+
* commit as the package boundary.
|
|
12
|
+
*/
|
|
13
|
+
export * from './participantId.js';
|
|
14
|
+
export * from './seats.js';
|
|
15
|
+
export * from './resync.js';
|
|
16
|
+
export * from './game-bundle.js';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Participant identity — the FORMAT half (pure, isomorphic, zero deps).
|
|
3
|
+
*
|
|
4
|
+
* A `ParticipantId` names a *person in a room*, and it is deliberately NOT the
|
|
5
|
+
* socket id. Until this module existed, `clientId` WAS `socket.id`
|
|
6
|
+
* (`ws/join.ts`), so every identity in the system died on reconnect and the
|
|
7
|
+
* codebase carried three separate workarounds for it: the `fk:`/`cid:` name
|
|
8
|
+
* persistence keys in `rooms/store.ts`, the A/B bucketing that had to abandon
|
|
9
|
+
* `me.clientId` for a persisted localStorage id (`lib/fragmentDelivery.ts`),
|
|
10
|
+
* and the leader-takeover heuristics in `ws/join.ts` that exist to recognise a
|
|
11
|
+
* refreshing presenter whose new socket is a different "client".
|
|
12
|
+
*
|
|
13
|
+
* ## The tier is IN the id, on purpose
|
|
14
|
+
*
|
|
15
|
+
* fk:<digest> account — derived from the platform ACCOUNT id (the key
|
|
16
|
+
* registry's `userId`, resolved server-side through
|
|
17
|
+
* whoami). Survives reconnects, reloads, restarts,
|
|
18
|
+
* devices, networks — and KEY ROTATION. The `fk`
|
|
19
|
+
* spelling predates that and is kept because
|
|
20
|
+
* consumers parse it; read it as "account".
|
|
21
|
+
*
|
|
22
|
+
* ## Derive the account tier from the userId, never from the key
|
|
23
|
+
*
|
|
24
|
+
* The X-User-Key is a CREDENTIAL and it rotates. An id hashed from it changes
|
|
25
|
+
* the moment the key does, and everything keyed on the id — a remembered seat,
|
|
26
|
+
* a restored name, the same-presenter leader takeover — silently stops
|
|
27
|
+
* recognising the person. That is the same failure that detached every
|
|
28
|
+
* tierlist ballot in watchparty (#144), one layer up. `accountId` is the
|
|
29
|
+
* input; `friendKey` remains accepted ONLY so a consumer that has not yet
|
|
30
|
+
* resolved userIds keeps working, and loses rotation-stability while it does.
|
|
31
|
+
* dev:<digest> device — derived from a browser-persisted id. Survives a
|
|
32
|
+
* reload and a reconnect; not a new browser.
|
|
33
|
+
* eph:<socketId> ephemeral— anon with no usable storage. Dies with the socket.
|
|
34
|
+
*
|
|
35
|
+
* Encoding durability in the id means code that REQUIRES durability can assert
|
|
36
|
+
* on it (`requireDurable`) instead of hoping. A 45-minute draft seat can demand
|
|
37
|
+
* `account` or `device`; a demo room can accept `ephemeral`.
|
|
38
|
+
*
|
|
39
|
+
* ## `device` is NOT an authorization boundary
|
|
40
|
+
*
|
|
41
|
+
* The device id is **client-asserted** — it arrives in the join payload and
|
|
42
|
+
* nothing proves it. So a `dev:` identity is good enough to carry a display
|
|
43
|
+
* name or hold a seat among friends, and NOT good enough to gate a capability:
|
|
44
|
+
* anyone who learns another client's device id can present it. Only the
|
|
45
|
+
* `account` tier is server-authenticated (the edge resolves the hadoku_session
|
|
46
|
+
* cookie into X-User-Key; the browser never sees the key). Anything that grants
|
|
47
|
+
* power — leadership, ownership, a moderation action — must require
|
|
48
|
+
* `account`. `isAuthenticatedTier` is the check; do not widen it.
|
|
49
|
+
*
|
|
50
|
+
* ## Why the id can be logged and put on the wire
|
|
51
|
+
*
|
|
52
|
+
* The `fk:` and `dev:` bodies are non-reversible digests, not the source
|
|
53
|
+
* material (see the server's `deriveParticipantId`, which owns the hashing
|
|
54
|
+
* because it needs a synchronous crypto primitive this package cannot assume).
|
|
55
|
+
* The friend key IS the X-User-Key credential, so embedding it raw would hand
|
|
56
|
+
* every viewer everyone else's key while looking entirely normal on screen —
|
|
57
|
+
* the same trap `opaqueVoterId` was written to avoid in `tierlist/store.ts`.
|
|
58
|
+
* Hashing at derivation means there is exactly ONE id per participant rather
|
|
59
|
+
* than an internal id plus a wire handle to keep in sync.
|
|
60
|
+
*/
|
|
61
|
+
/** Durability class of a participant identity. Ordered least → most durable. */
|
|
62
|
+
export type ParticipantIdTier = 'ephemeral' | 'device' | 'account';
|
|
63
|
+
/** `fk:…` | `dev:…` | `eph:…` — see this module's header. */
|
|
64
|
+
export type ParticipantId = string;
|
|
65
|
+
export declare const PARTICIPANT_ID_PREFIX: Readonly<Record<ParticipantIdTier, string>>;
|
|
66
|
+
/**
|
|
67
|
+
* Shortest friend key we treat as real. Matches the existing `>= 5` checks in
|
|
68
|
+
* `ws/handlers.ts` and `rooms/store.ts#hasFriendKey` — keep them in step.
|
|
69
|
+
*/
|
|
70
|
+
export declare const MIN_FRIEND_KEY_LENGTH = 5;
|
|
71
|
+
/**
|
|
72
|
+
* Shortest device id we accept. Longer than the friend-key floor on purpose:
|
|
73
|
+
* a friend key is server-injected and known-good, while a device id is
|
|
74
|
+
* client-asserted, so a short one is far more likely to be junk (or a
|
|
75
|
+
* collision-farming attempt) than a real persisted id. Clients that can't do
|
|
76
|
+
* better fall through to `ephemeral`, which is honest rather than broken.
|
|
77
|
+
*/
|
|
78
|
+
export declare const MIN_DEVICE_ID_LENGTH = 8;
|
|
79
|
+
/** Hex digest length used by the `account` and `device` bodies. */
|
|
80
|
+
export declare const PARTICIPANT_ID_DIGEST_LENGTH = 16;
|
|
81
|
+
/**
|
|
82
|
+
* Tier of an id, or `null` if it isn't one. Returns null rather than throwing
|
|
83
|
+
* or defaulting: an unrecognised id is absence of evidence, and defaulting it
|
|
84
|
+
* to `ephemeral` would silently admit a malformed id to every path that only
|
|
85
|
+
* refuses `ephemeral` — the failure would look like a working downgrade.
|
|
86
|
+
*/
|
|
87
|
+
export declare function participantIdTier(id: string): ParticipantIdTier | null;
|
|
88
|
+
/** True for a well-formed id of any tier. */
|
|
89
|
+
export declare function isParticipantId(id: string): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Survives a reconnect (`account` or `device`). This is the check for "may hold
|
|
92
|
+
* a seat / may have a pending request replayed", NOT for "may do something
|
|
93
|
+
* privileged" — see `isAuthenticatedTier`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function isDurableParticipantId(id: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Server-authenticated identity (`account` only). The gate for anything that
|
|
98
|
+
* grants power. Deliberately narrower than `isDurableParticipantId` because a
|
|
99
|
+
* device id is client-asserted — see this module's header.
|
|
100
|
+
*/
|
|
101
|
+
export declare function isAuthenticatedTier(id: string): boolean;
|
|
102
|
+
/** Rank comparison — `tierAtLeast(id, 'device')` admits device and account. */
|
|
103
|
+
export declare function participantTierAtLeast(id: string, minTier: ParticipantIdTier): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Whether a friend key is long enough to be treated as real. THE one
|
|
106
|
+
* definition — it was hand-copied as `length >= 5` across six files, which is
|
|
107
|
+
* how one of them ends up disagreeing with the rest.
|
|
108
|
+
*
|
|
109
|
+
* A type guard, so callers narrow instead of re-asserting with a cast.
|
|
110
|
+
*/
|
|
111
|
+
export declare function isUsableFriendKey(friendKey: unknown): friendKey is string;
|
|
112
|
+
/**
|
|
113
|
+
* Whether a platform account id (the registry `userId`) is usable. Same floor
|
|
114
|
+
* as a friend key: it is server-resolved, never client-asserted, so the check
|
|
115
|
+
* exists only to reject an empty or junk value, not to judge trust.
|
|
116
|
+
*/
|
|
117
|
+
export declare function isUsableAccountId(accountId: unknown): accountId is string;
|
|
118
|
+
/** Whether a client-supplied device id is long enough to be treated as real. */
|
|
119
|
+
export declare function isUsableDeviceId(deviceId: unknown): deviceId is string;
|
|
120
|
+
/**
|
|
121
|
+
* Which tier a set of inputs WOULD produce, without needing the hash. Lets the
|
|
122
|
+
* join path log and gate on the tier before (or without) deriving the id, and
|
|
123
|
+
* lets tests assert the tier decision independently of the digest.
|
|
124
|
+
*/
|
|
125
|
+
export declare function resolveParticipantTier(input: {
|
|
126
|
+
accountId?: string | null;
|
|
127
|
+
friendKey?: string | null;
|
|
128
|
+
deviceId?: string | null;
|
|
129
|
+
}): ParticipantIdTier;
|
|
130
|
+
/**
|
|
131
|
+
* Assemble an id from an already-computed body. The server's
|
|
132
|
+
* `deriveParticipantId` is the only intended caller for the hashed tiers; it is
|
|
133
|
+
* exported so both halves share one spelling of the prefixes rather than
|
|
134
|
+
* concatenating them by hand at each site.
|
|
135
|
+
*/
|
|
136
|
+
export declare function formatParticipantId(tier: ParticipantIdTier, body: string): ParticipantId;
|
|
137
|
+
/**
|
|
138
|
+
* The body after the scheme, or `null` for a malformed id. Useful for logs
|
|
139
|
+
* that want a short handle without the prefix noise; never for reconstructing
|
|
140
|
+
* the source material, which is not recoverable from a digest.
|
|
141
|
+
*/
|
|
142
|
+
export declare function participantIdBody(id: string): string | null;
|
|
143
|
+
/** How much of a participant's body a handle shows. */
|
|
144
|
+
export declare const PARTICIPANT_HANDLE_LENGTH = 6;
|
|
145
|
+
/**
|
|
146
|
+
* A short, wire-safe handle for a participant — what you may SHOW a room.
|
|
147
|
+
*
|
|
148
|
+
* The one part of an identity that is safe to put in a seat row or a log line:
|
|
149
|
+
* the body is a non-reversible digest (or, at the ephemeral tier, a socket id
|
|
150
|
+
* every client already sees), and the prefix that names the tier is dropped
|
|
151
|
+
* because a roster has no business advertising who is signed in.
|
|
152
|
+
*
|
|
153
|
+
* Here rather than in each consumer so the truncation is ONE number. Two apps
|
|
154
|
+
* each picking their own produce handles that collide at different rates, and
|
|
155
|
+
* the one that picked too short finds out from a room where two people look
|
|
156
|
+
* like the same person. `'unknown'` for a malformed id, never a slice of it.
|
|
157
|
+
*/
|
|
158
|
+
export declare function participantHandle(id: string): string;
|
|
159
|
+
//# sourceMappingURL=participantId.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"participantId.d.ts","sourceRoot":"","sources":["../src/participantId.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AAEH,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEnE,6DAA6D;AAC7D,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC;AAEnC,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAI7E,CAAC;AAaF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,mEAAmE;AACnE,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAS/C;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAItE;AAED,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,+EAA+E;AAC/E,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAItF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,IAAI,MAAM,CAEzE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,OAAO,GAAG,SAAS,IAAI,MAAM,CAEzE;AAED,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,IAAI,MAAM,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,GAAG,iBAAiB,CAKpB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAExF;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG3D;AAED,uDAAuD;AACvD,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGpD"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Participant identity — the FORMAT half (pure, isomorphic, zero deps).
|
|
3
|
+
*
|
|
4
|
+
* A `ParticipantId` names a *person in a room*, and it is deliberately NOT the
|
|
5
|
+
* socket id. Until this module existed, `clientId` WAS `socket.id`
|
|
6
|
+
* (`ws/join.ts`), so every identity in the system died on reconnect and the
|
|
7
|
+
* codebase carried three separate workarounds for it: the `fk:`/`cid:` name
|
|
8
|
+
* persistence keys in `rooms/store.ts`, the A/B bucketing that had to abandon
|
|
9
|
+
* `me.clientId` for a persisted localStorage id (`lib/fragmentDelivery.ts`),
|
|
10
|
+
* and the leader-takeover heuristics in `ws/join.ts` that exist to recognise a
|
|
11
|
+
* refreshing presenter whose new socket is a different "client".
|
|
12
|
+
*
|
|
13
|
+
* ## The tier is IN the id, on purpose
|
|
14
|
+
*
|
|
15
|
+
* fk:<digest> account — derived from the platform ACCOUNT id (the key
|
|
16
|
+
* registry's `userId`, resolved server-side through
|
|
17
|
+
* whoami). Survives reconnects, reloads, restarts,
|
|
18
|
+
* devices, networks — and KEY ROTATION. The `fk`
|
|
19
|
+
* spelling predates that and is kept because
|
|
20
|
+
* consumers parse it; read it as "account".
|
|
21
|
+
*
|
|
22
|
+
* ## Derive the account tier from the userId, never from the key
|
|
23
|
+
*
|
|
24
|
+
* The X-User-Key is a CREDENTIAL and it rotates. An id hashed from it changes
|
|
25
|
+
* the moment the key does, and everything keyed on the id — a remembered seat,
|
|
26
|
+
* a restored name, the same-presenter leader takeover — silently stops
|
|
27
|
+
* recognising the person. That is the same failure that detached every
|
|
28
|
+
* tierlist ballot in watchparty (#144), one layer up. `accountId` is the
|
|
29
|
+
* input; `friendKey` remains accepted ONLY so a consumer that has not yet
|
|
30
|
+
* resolved userIds keeps working, and loses rotation-stability while it does.
|
|
31
|
+
* dev:<digest> device — derived from a browser-persisted id. Survives a
|
|
32
|
+
* reload and a reconnect; not a new browser.
|
|
33
|
+
* eph:<socketId> ephemeral— anon with no usable storage. Dies with the socket.
|
|
34
|
+
*
|
|
35
|
+
* Encoding durability in the id means code that REQUIRES durability can assert
|
|
36
|
+
* on it (`requireDurable`) instead of hoping. A 45-minute draft seat can demand
|
|
37
|
+
* `account` or `device`; a demo room can accept `ephemeral`.
|
|
38
|
+
*
|
|
39
|
+
* ## `device` is NOT an authorization boundary
|
|
40
|
+
*
|
|
41
|
+
* The device id is **client-asserted** — it arrives in the join payload and
|
|
42
|
+
* nothing proves it. So a `dev:` identity is good enough to carry a display
|
|
43
|
+
* name or hold a seat among friends, and NOT good enough to gate a capability:
|
|
44
|
+
* anyone who learns another client's device id can present it. Only the
|
|
45
|
+
* `account` tier is server-authenticated (the edge resolves the hadoku_session
|
|
46
|
+
* cookie into X-User-Key; the browser never sees the key). Anything that grants
|
|
47
|
+
* power — leadership, ownership, a moderation action — must require
|
|
48
|
+
* `account`. `isAuthenticatedTier` is the check; do not widen it.
|
|
49
|
+
*
|
|
50
|
+
* ## Why the id can be logged and put on the wire
|
|
51
|
+
*
|
|
52
|
+
* The `fk:` and `dev:` bodies are non-reversible digests, not the source
|
|
53
|
+
* material (see the server's `deriveParticipantId`, which owns the hashing
|
|
54
|
+
* because it needs a synchronous crypto primitive this package cannot assume).
|
|
55
|
+
* The friend key IS the X-User-Key credential, so embedding it raw would hand
|
|
56
|
+
* every viewer everyone else's key while looking entirely normal on screen —
|
|
57
|
+
* the same trap `opaqueVoterId` was written to avoid in `tierlist/store.ts`.
|
|
58
|
+
* Hashing at derivation means there is exactly ONE id per participant rather
|
|
59
|
+
* than an internal id plus a wire handle to keep in sync.
|
|
60
|
+
*/
|
|
61
|
+
export const PARTICIPANT_ID_PREFIX = {
|
|
62
|
+
account: 'fk:',
|
|
63
|
+
device: 'dev:',
|
|
64
|
+
ephemeral: 'eph:',
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Tier ranking, least → most durable. Derived from one source so the
|
|
68
|
+
* vocabulary and the ordering cannot drift (the same reason `TIER_RANK` in the
|
|
69
|
+
* server's `whoami.ts` is a map rather than a list of comparisons).
|
|
70
|
+
*/
|
|
71
|
+
const TIER_RANK = {
|
|
72
|
+
ephemeral: 0,
|
|
73
|
+
device: 1,
|
|
74
|
+
account: 2,
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Shortest friend key we treat as real. Matches the existing `>= 5` checks in
|
|
78
|
+
* `ws/handlers.ts` and `rooms/store.ts#hasFriendKey` — keep them in step.
|
|
79
|
+
*/
|
|
80
|
+
export const MIN_FRIEND_KEY_LENGTH = 5;
|
|
81
|
+
/**
|
|
82
|
+
* Shortest device id we accept. Longer than the friend-key floor on purpose:
|
|
83
|
+
* a friend key is server-injected and known-good, while a device id is
|
|
84
|
+
* client-asserted, so a short one is far more likely to be junk (or a
|
|
85
|
+
* collision-farming attempt) than a real persisted id. Clients that can't do
|
|
86
|
+
* better fall through to `ephemeral`, which is honest rather than broken.
|
|
87
|
+
*/
|
|
88
|
+
export const MIN_DEVICE_ID_LENGTH = 8;
|
|
89
|
+
/** Hex digest length used by the `account` and `device` bodies. */
|
|
90
|
+
export const PARTICIPANT_ID_DIGEST_LENGTH = 16;
|
|
91
|
+
const PARSE_RE = /^(fk|dev|eph):(.+)$/;
|
|
92
|
+
const TIER_BY_SCHEME = {
|
|
93
|
+
fk: 'account',
|
|
94
|
+
dev: 'device',
|
|
95
|
+
eph: 'ephemeral',
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Tier of an id, or `null` if it isn't one. Returns null rather than throwing
|
|
99
|
+
* or defaulting: an unrecognised id is absence of evidence, and defaulting it
|
|
100
|
+
* to `ephemeral` would silently admit a malformed id to every path that only
|
|
101
|
+
* refuses `ephemeral` — the failure would look like a working downgrade.
|
|
102
|
+
*/
|
|
103
|
+
export function participantIdTier(id) {
|
|
104
|
+
const m = PARSE_RE.exec(id);
|
|
105
|
+
if (!m)
|
|
106
|
+
return null;
|
|
107
|
+
return TIER_BY_SCHEME[m[1]] ?? null;
|
|
108
|
+
}
|
|
109
|
+
/** True for a well-formed id of any tier. */
|
|
110
|
+
export function isParticipantId(id) {
|
|
111
|
+
return participantIdTier(id) !== null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Survives a reconnect (`account` or `device`). This is the check for "may hold
|
|
115
|
+
* a seat / may have a pending request replayed", NOT for "may do something
|
|
116
|
+
* privileged" — see `isAuthenticatedTier`.
|
|
117
|
+
*/
|
|
118
|
+
export function isDurableParticipantId(id) {
|
|
119
|
+
const tier = participantIdTier(id);
|
|
120
|
+
return tier !== null && TIER_RANK[tier] >= TIER_RANK.device;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Server-authenticated identity (`account` only). The gate for anything that
|
|
124
|
+
* grants power. Deliberately narrower than `isDurableParticipantId` because a
|
|
125
|
+
* device id is client-asserted — see this module's header.
|
|
126
|
+
*/
|
|
127
|
+
export function isAuthenticatedTier(id) {
|
|
128
|
+
return participantIdTier(id) === 'account';
|
|
129
|
+
}
|
|
130
|
+
/** Rank comparison — `tierAtLeast(id, 'device')` admits device and account. */
|
|
131
|
+
export function participantTierAtLeast(id, minTier) {
|
|
132
|
+
const tier = participantIdTier(id);
|
|
133
|
+
if (tier === null)
|
|
134
|
+
return false;
|
|
135
|
+
return TIER_RANK[tier] >= TIER_RANK[minTier];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Whether a friend key is long enough to be treated as real. THE one
|
|
139
|
+
* definition — it was hand-copied as `length >= 5` across six files, which is
|
|
140
|
+
* how one of them ends up disagreeing with the rest.
|
|
141
|
+
*
|
|
142
|
+
* A type guard, so callers narrow instead of re-asserting with a cast.
|
|
143
|
+
*/
|
|
144
|
+
export function isUsableFriendKey(friendKey) {
|
|
145
|
+
return typeof friendKey === 'string' && friendKey.length >= MIN_FRIEND_KEY_LENGTH;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Whether a platform account id (the registry `userId`) is usable. Same floor
|
|
149
|
+
* as a friend key: it is server-resolved, never client-asserted, so the check
|
|
150
|
+
* exists only to reject an empty or junk value, not to judge trust.
|
|
151
|
+
*/
|
|
152
|
+
export function isUsableAccountId(accountId) {
|
|
153
|
+
return typeof accountId === 'string' && accountId.length >= MIN_FRIEND_KEY_LENGTH;
|
|
154
|
+
}
|
|
155
|
+
/** Whether a client-supplied device id is long enough to be treated as real. */
|
|
156
|
+
export function isUsableDeviceId(deviceId) {
|
|
157
|
+
return typeof deviceId === 'string' && deviceId.length >= MIN_DEVICE_ID_LENGTH;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Which tier a set of inputs WOULD produce, without needing the hash. Lets the
|
|
161
|
+
* join path log and gate on the tier before (or without) deriving the id, and
|
|
162
|
+
* lets tests assert the tier decision independently of the digest.
|
|
163
|
+
*/
|
|
164
|
+
export function resolveParticipantTier(input) {
|
|
165
|
+
if (isUsableAccountId(input.accountId))
|
|
166
|
+
return 'account';
|
|
167
|
+
if (isUsableFriendKey(input.friendKey))
|
|
168
|
+
return 'account';
|
|
169
|
+
if (isUsableDeviceId(input.deviceId))
|
|
170
|
+
return 'device';
|
|
171
|
+
return 'ephemeral';
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Assemble an id from an already-computed body. The server's
|
|
175
|
+
* `deriveParticipantId` is the only intended caller for the hashed tiers; it is
|
|
176
|
+
* exported so both halves share one spelling of the prefixes rather than
|
|
177
|
+
* concatenating them by hand at each site.
|
|
178
|
+
*/
|
|
179
|
+
export function formatParticipantId(tier, body) {
|
|
180
|
+
return `${PARTICIPANT_ID_PREFIX[tier]}${body}`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The body after the scheme, or `null` for a malformed id. Useful for logs
|
|
184
|
+
* that want a short handle without the prefix noise; never for reconstructing
|
|
185
|
+
* the source material, which is not recoverable from a digest.
|
|
186
|
+
*/
|
|
187
|
+
export function participantIdBody(id) {
|
|
188
|
+
const m = PARSE_RE.exec(id);
|
|
189
|
+
return m ? m[2] : null;
|
|
190
|
+
}
|
|
191
|
+
/** How much of a participant's body a handle shows. */
|
|
192
|
+
export const PARTICIPANT_HANDLE_LENGTH = 6;
|
|
193
|
+
/**
|
|
194
|
+
* A short, wire-safe handle for a participant — what you may SHOW a room.
|
|
195
|
+
*
|
|
196
|
+
* The one part of an identity that is safe to put in a seat row or a log line:
|
|
197
|
+
* the body is a non-reversible digest (or, at the ephemeral tier, a socket id
|
|
198
|
+
* every client already sees), and the prefix that names the tier is dropped
|
|
199
|
+
* because a roster has no business advertising who is signed in.
|
|
200
|
+
*
|
|
201
|
+
* Here rather than in each consumer so the truncation is ONE number. Two apps
|
|
202
|
+
* each picking their own produce handles that collide at different rates, and
|
|
203
|
+
* the one that picked too short finds out from a room where two people look
|
|
204
|
+
* like the same person. `'unknown'` for a malformed id, never a slice of it.
|
|
205
|
+
*/
|
|
206
|
+
export function participantHandle(id) {
|
|
207
|
+
const body = participantIdBody(id);
|
|
208
|
+
return body === null ? 'unknown' : body.slice(0, PARTICIPANT_HANDLE_LENGTH);
|
|
209
|
+
}
|
package/dist/resync.d.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconnect and resync — the CONTRACT, not the transport.
|
|
3
|
+
*
|
|
4
|
+
* A client that drops and returns has three problems, and they are separable:
|
|
5
|
+
* did it miss anything (sequence), is it even talking to the same room
|
|
6
|
+
* incarnation (epoch), and does the server still agree about who it is
|
|
7
|
+
* (identity). The rules for all three live here, pure and dependency-free, so
|
|
8
|
+
* both ends agree on them and either end can test them without a socket.
|
|
9
|
+
*
|
|
10
|
+
* **The implementations deliberately stay where they are.** Watchparty's socket
|
|
11
|
+
* lifecycle is in `apps/ui/src/lib/socket.ts` and is not extracted; a second
|
|
12
|
+
* consumer whose client is a cross-origin iframe relaying over `postMessage`
|
|
13
|
+
* has a different transport entirely. What both need to agree on is the RULES,
|
|
14
|
+
* which is this file.
|
|
15
|
+
*
|
|
16
|
+
* ## What watchparty actually stamps today — read this before binding
|
|
17
|
+
*
|
|
18
|
+
* `CoreRoomState.lastEventSeq` is incremented **only for playback control
|
|
19
|
+
* events** (`ws/control.ts`), and it reaches clients in the `snapshot` and
|
|
20
|
+
* `control_ack` payloads. Chat, presence, health and game envelopes carry no
|
|
21
|
+
* sequence at all. So `classifySeq` is a rule this package *guarantees*, not a
|
|
22
|
+
* behaviour watchparty currently exercises end to end — an app that wants gap
|
|
23
|
+
* detection stamps `SeqCursor` on its own envelopes and runs the classifier.
|
|
24
|
+
*
|
|
25
|
+
* That distinction was stated wrongly once (an earlier version of the
|
|
26
|
+
* extraction reply claimed "every envelope carries seq"), so it is spelled out
|
|
27
|
+
* here rather than left to be inferred.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Where a client is in a room's event stream.
|
|
31
|
+
*
|
|
32
|
+
* `epoch` identifies the room INCARNATION. It exists because a sequence number
|
|
33
|
+
* alone cannot tell "I missed events" from "the server restarted and started
|
|
34
|
+
* counting again" — and those need opposite responses: catch up versus discard
|
|
35
|
+
* everything and refetch. Any value works as long as it changes when the room
|
|
36
|
+
* is rebuilt and not otherwise (a boot id, a restore id, a uuid).
|
|
37
|
+
*/
|
|
38
|
+
export interface SeqCursor {
|
|
39
|
+
epoch: string;
|
|
40
|
+
seq: number;
|
|
41
|
+
}
|
|
42
|
+
export type SeqVerdict =
|
|
43
|
+
/** Exactly the next event. Apply it. */
|
|
44
|
+
'ok'
|
|
45
|
+
/** Already seen. Drop it — do NOT re-apply, and do not resync. */
|
|
46
|
+
| 'duplicate'
|
|
47
|
+
/** Events were missed. Request a resync. */
|
|
48
|
+
| 'gap'
|
|
49
|
+
/** Different room incarnation. Discard local state and refetch in full. */
|
|
50
|
+
| 'epoch-change';
|
|
51
|
+
/**
|
|
52
|
+
* Classify an incoming event against the last one applied.
|
|
53
|
+
*
|
|
54
|
+
* `last === null` means nothing has been applied yet, which is 'ok' rather than
|
|
55
|
+
* a gap: a client joining mid-stream has missed everything by definition, and
|
|
56
|
+
* its snapshot — not a resync request — is what establishes the baseline.
|
|
57
|
+
*
|
|
58
|
+
* **Sequence numbers are NEVER compared across epochs.** A restarted server
|
|
59
|
+
* counting from 1 while the client holds 500 would otherwise read as a flood
|
|
60
|
+
* of duplicates and the client would sit there discarding live events forever.
|
|
61
|
+
* The epoch check therefore comes first, before any arithmetic.
|
|
62
|
+
*/
|
|
63
|
+
export declare function classifySeq(last: SeqCursor | null, incoming: SeqCursor): SeqVerdict;
|
|
64
|
+
/** How many events a gap skipped. 0 when the verdict is not 'gap'. */
|
|
65
|
+
export declare function missedCount(last: SeqCursor | null, incoming: SeqCursor): number;
|
|
66
|
+
/**
|
|
67
|
+
* Does this verdict require the client to ask the server for state?
|
|
68
|
+
*
|
|
69
|
+
* A helper rather than a comparison at each call site, because the answer is
|
|
70
|
+
* NOT "anything that isn't ok": a duplicate must not trigger a resync. Under
|
|
71
|
+
* any retransmit, duplicates arrive in bursts, and resyncing on each one turns
|
|
72
|
+
* a harmless replay into a request storm against the server that is already
|
|
73
|
+
* struggling enough to be retransmitting.
|
|
74
|
+
*/
|
|
75
|
+
export declare function needsResync(verdict: SeqVerdict): boolean;
|
|
76
|
+
/** The server's view of us, from the room roster. */
|
|
77
|
+
export interface RosterIdentity {
|
|
78
|
+
name?: string | null;
|
|
79
|
+
avatarId?: string | null;
|
|
80
|
+
}
|
|
81
|
+
export interface LocalIdentity {
|
|
82
|
+
name?: string | null;
|
|
83
|
+
avatarId?: string | null;
|
|
84
|
+
}
|
|
85
|
+
export interface IdentityReassert {
|
|
86
|
+
name?: string;
|
|
87
|
+
avatarId?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Should the client re-assert its local identity to the server?
|
|
91
|
+
*
|
|
92
|
+
* On each snapshot a client compares its own name/avatar against the server's
|
|
93
|
+
* roster entry FOR ITSELF and emits an identity update only for the fields that
|
|
94
|
+
* actually disagree, so later snapshots are no-ops.
|
|
95
|
+
*
|
|
96
|
+
* **Read `serverEntry` from the ROSTER, not from the snapshot.** Watchparty's
|
|
97
|
+
* snapshot carries seven fields and has never included a roster; a caller that
|
|
98
|
+
* looked itself up there got `undefined` permanently, both guards collapsed to
|
|
99
|
+
* local state alone, and every client with a saved identity re-emitted on
|
|
100
|
+
* every snapshot — including the room-wide ones. That bug survived a long time
|
|
101
|
+
* because broken and correct agree on the FIRST snapshot after join and
|
|
102
|
+
* diverge only afterwards.
|
|
103
|
+
*
|
|
104
|
+
* `serverEntry === undefined` still means "assert": that is the genuine
|
|
105
|
+
* first-snapshot case, where the client cannot know the server's view.
|
|
106
|
+
*
|
|
107
|
+
* Known quirk, preserved deliberately: an empty server name does not trigger a
|
|
108
|
+
* name assert, because an empty string is not a default name under any sane
|
|
109
|
+
* `isDefaultName`. Widening that is a separate decision with a different blast
|
|
110
|
+
* radius.
|
|
111
|
+
*
|
|
112
|
+
* @param isDefaultName recognises an auto-assigned name; a local name that is
|
|
113
|
+
* still a default is not worth asserting.
|
|
114
|
+
* @returns the fields to emit, or null when nothing differs.
|
|
115
|
+
*/
|
|
116
|
+
export declare function resolveIdentityReassert(local: LocalIdentity, serverEntry: RosterIdentity | undefined, isDefaultName: (name: string) => boolean): IdentityReassert | null;
|
|
117
|
+
//# sourceMappingURL=resync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resync.d.ts","sourceRoot":"","sources":["../src/resync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH;;;;;;;;GAQG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,MAAM,UAAU;AACpB,wCAAwC;AACtC,IAAI;AACN,kEAAkE;GAChE,WAAW;AACb,4CAA4C;GAC1C,KAAK;AACP,2EAA2E;GACzE,cAAc,CAAC;AAEnB;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,EAAE,QAAQ,EAAE,SAAS,GAAG,UAAU,CAMnF;AAED,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,EAAE,QAAQ,EAAE,SAAS,GAAG,MAAM,CAG/E;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAExD;AAID,qDAAqD;AACrD,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,aAAa,EACpB,WAAW,EAAE,cAAc,GAAG,SAAS,EACvC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GACvC,gBAAgB,GAAG,IAAI,CAczB"}
|