@diffenderfer-games/hub-contract 2.0.0-next.0 → 2.0.0-next.1

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -21
  3. package/README.md +26 -26
  4. package/dist/index.d.ts +95 -95
  5. package/package.json +53 -53
  6. package/src/api/analytics.ts +81 -81
  7. package/src/api/auth.ts +77 -77
  8. package/src/api/catalog.ts +171 -171
  9. package/src/api/daily.ts +122 -122
  10. package/src/api/economy/bundle.ts +69 -69
  11. package/src/api/index.ts +85 -85
  12. package/src/api/leaderboards.ts +151 -151
  13. package/src/api/messages.ts +142 -142
  14. package/src/api/mp/chat.ts +36 -36
  15. package/src/api/mp/races.ts +211 -211
  16. package/src/api/mp/rooms.ts +90 -90
  17. package/src/api/mp/snapshot.ts +17 -17
  18. package/src/api/mp.ts +110 -110
  19. package/src/api/notify.ts +111 -111
  20. package/src/api/progress/game-data.ts +189 -189
  21. package/src/api/progress.ts +59 -59
  22. package/src/api/race.ts +47 -47
  23. package/src/api/reports.ts +208 -208
  24. package/src/api/social/config.ts +47 -47
  25. package/src/api/social/endpoints.ts +179 -179
  26. package/src/api/social/people.ts +118 -118
  27. package/src/api/social/presence.ts +95 -95
  28. package/src/api/social.ts +9 -9
  29. package/src/api/version.ts +40 -40
  30. package/src/coerce.ts +50 -50
  31. package/src/common.ts +42 -42
  32. package/src/entities.ts +62 -62
  33. package/src/errors/codes/core.ts +10 -10
  34. package/src/errors/codes/multiplayer.ts +43 -43
  35. package/src/errors/codes/play.ts +5 -5
  36. package/src/errors/codes/social.ts +31 -31
  37. package/src/errors/index.ts +88 -88
  38. package/src/index.ts +16 -16
  39. package/src/limits.ts +100 -100
  40. package/src/logic/daily.ts +54 -54
  41. package/src/logic/index.ts +8 -8
  42. package/src/logic/presence.ts +62 -62
  43. package/src/logic/time.ts +81 -81
  44. package/src/manifest/changes.ts +55 -55
  45. package/src/manifest/controls.ts +42 -42
  46. package/src/manifest/fields.ts +89 -89
  47. package/src/manifest/game.ts +132 -132
  48. package/src/manifest/index.ts +12 -12
  49. package/src/manifest/inject.ts +31 -31
  50. package/src/manifest/leaderboards.ts +29 -29
  51. package/src/manifest/multiplayer.ts +71 -71
  52. package/src/manifest/race.ts +93 -93
  53. package/src/middleware.ts +47 -47
  54. package/src/realtime/define.ts +52 -52
  55. package/src/realtime/envelope.ts +67 -67
  56. package/src/realtime/events/multiplayer.ts +8 -8
  57. package/src/realtime/events/parties.ts +31 -31
  58. package/src/realtime/events/races.ts +27 -27
  59. package/src/realtime/events/rooms.ts +48 -48
  60. package/src/realtime/events/social.ts +81 -81
  61. package/src/realtime/index.ts +24 -24
  62. package/src/realtime/ops/empty.ts +8 -8
  63. package/src/realtime/ops/hello.ts +40 -40
  64. package/src/realtime/ops/multiplayer.ts +8 -8
  65. package/src/realtime/ops/parties.ts +13 -13
  66. package/src/realtime/ops/races.ts +99 -99
  67. package/src/realtime/ops/rooms.ts +1 -1
  68. package/src/realtime/ops/social.ts +58 -58
@@ -1,67 +1,67 @@
1
- /**
2
- * The legacy `/_ws` envelope (multiplayer-impl.md §4.2), unchanged since v1
3
- * and kept indefinitely (plan D6):
4
- *
5
- * ```
6
- * client → server {t:'op', id, op, d}
7
- * server → client {t:'ack', id, ok:true, d}
8
- * {t:'ack', id, ok:false, err: ErrorBody}
9
- * {t:'ev', ev, d}
10
- * ```
11
- */
12
- import { z } from 'zod';
13
- import { Json } from '../common';
14
- import { ErrorBody, type ErrorCode } from '../errors';
15
-
16
- /** A client's op frame. `id` is the client's own counter, echoed in the ack. */
17
- export const OpFrame = z.object({
18
- t: z.literal('op'),
19
- id: z.number(),
20
- op: z.string(),
21
- d: Json.optional(),
22
- });
23
-
24
- /** An op frame. */
25
- export type OpFrame = z.infer<typeof OpFrame>;
26
-
27
- /** A successful ack. */
28
- export const OkAckFrame = z.object({ t: z.literal('ack'), id: z.number(), ok: z.literal(true), d: Json });
29
-
30
- /** A failed ack. */
31
- export const ErrAckFrame = z.object({ t: z.literal('ack'), id: z.number(), ok: z.literal(false), err: ErrorBody });
32
-
33
- /** An ack frame: the server's answer to one op. */
34
- export const AckFrame = z.discriminatedUnion('ok', [OkAckFrame, ErrAckFrame]);
35
-
36
- /** An ack frame. */
37
- export type AckFrame = z.infer<typeof AckFrame>;
38
-
39
- /** A server-pushed event frame. */
40
- export const EvFrame = z.object({ t: z.literal('ev'), ev: z.string(), d: Json });
41
-
42
- /** An event frame. */
43
- export type EvFrame = z.infer<typeof EvFrame>;
44
-
45
- /** Any frame the server sends. */
46
- export const ServerFrame = z.union([AckFrame, EvFrame]);
47
-
48
- /** Close codes the client understands. */
49
- export const CloseCode = {
50
- /** Service restart: reconnect quietly. */
51
- restart: 1012,
52
- /** The account is suspended. */
53
- suspended: 4003,
54
- /** Too many sockets for this user or IP. */
55
- tooMany: 4029,
56
- /** A frame that wasn't a valid op. */
57
- badMessage: 4400,
58
- } as const;
59
-
60
- /**
61
- * Codes any op can fail with, raised by the envelope rather than the op:
62
- * the per-socket rate limit, an unknown op name, and ops sent before `hello`.
63
- */
64
- export const ENVELOPE_ERRORS: readonly ErrorCode[] = ['rate_limited', 'unknown_op', 'hello_required'];
65
-
66
- /** Ops the server accepts before `hello`. */
67
- export const PRE_HELLO_OPS: readonly string[] = ['hello', 'ping'];
1
+ /**
2
+ * The legacy `/_ws` envelope (multiplayer-impl.md §4.2), unchanged since v1
3
+ * and kept indefinitely (plan D6):
4
+ *
5
+ * ```
6
+ * client → server {t:'op', id, op, d}
7
+ * server → client {t:'ack', id, ok:true, d}
8
+ * {t:'ack', id, ok:false, err: ErrorBody}
9
+ * {t:'ev', ev, d}
10
+ * ```
11
+ */
12
+ import { z } from 'zod';
13
+ import { Json } from '../common';
14
+ import { ErrorBody, type ErrorCode } from '../errors';
15
+
16
+ /** A client's op frame. `id` is the client's own counter, echoed in the ack. */
17
+ export const OpFrame = z.object({
18
+ t: z.literal('op'),
19
+ id: z.number(),
20
+ op: z.string(),
21
+ d: Json.optional(),
22
+ });
23
+
24
+ /** An op frame. */
25
+ export type OpFrame = z.infer<typeof OpFrame>;
26
+
27
+ /** A successful ack. */
28
+ export const OkAckFrame = z.object({ t: z.literal('ack'), id: z.number(), ok: z.literal(true), d: Json });
29
+
30
+ /** A failed ack. */
31
+ export const ErrAckFrame = z.object({ t: z.literal('ack'), id: z.number(), ok: z.literal(false), err: ErrorBody });
32
+
33
+ /** An ack frame: the server's answer to one op. */
34
+ export const AckFrame = z.discriminatedUnion('ok', [OkAckFrame, ErrAckFrame]);
35
+
36
+ /** An ack frame. */
37
+ export type AckFrame = z.infer<typeof AckFrame>;
38
+
39
+ /** A server-pushed event frame. */
40
+ export const EvFrame = z.object({ t: z.literal('ev'), ev: z.string(), d: Json });
41
+
42
+ /** An event frame. */
43
+ export type EvFrame = z.infer<typeof EvFrame>;
44
+
45
+ /** Any frame the server sends. */
46
+ export const ServerFrame = z.union([AckFrame, EvFrame]);
47
+
48
+ /** Close codes the client understands. */
49
+ export const CloseCode = {
50
+ /** Service restart: reconnect quietly. */
51
+ restart: 1012,
52
+ /** The account is suspended. */
53
+ suspended: 4003,
54
+ /** Too many sockets for this user or IP. */
55
+ tooMany: 4029,
56
+ /** A frame that wasn't a valid op. */
57
+ badMessage: 4400,
58
+ } as const;
59
+
60
+ /**
61
+ * Codes any op can fail with, raised by the envelope rather than the op:
62
+ * the per-socket rate limit, an unknown op name, and ops sent before `hello`.
63
+ */
64
+ export const ENVELOPE_ERRORS: readonly ErrorCode[] = ['rate_limited', 'unknown_op', 'hello_required'];
65
+
66
+ /** Ops the server accepts before `hello`. */
67
+ export const PRE_HELLO_OPS: readonly string[] = ['hello', 'ping'];
@@ -1,8 +1,8 @@
1
- /** Socket events for invites, parties, hub rooms and races. */
2
- import type { EventDef } from '../define';
3
- import { partyEvents } from './parties';
4
- import { raceEvents } from './races';
5
- import { roomEvents } from './rooms';
6
-
7
- /** The multiplayer events, by event name. */
8
- export const multiplayerEvents = { ...partyEvents, ...roomEvents, ...raceEvents } satisfies Record<string, EventDef>;
1
+ /** Socket events for invites, parties, hub rooms and races. */
2
+ import type { EventDef } from '../define';
3
+ import { partyEvents } from './parties';
4
+ import { raceEvents } from './races';
5
+ import { roomEvents } from './rooms';
6
+
7
+ /** The multiplayer events, by event name. */
8
+ export const multiplayerEvents = { ...partyEvents, ...roomEvents, ...raceEvents } satisfies Record<string, EventDef>;
@@ -1,31 +1,31 @@
1
- /** Invite and party events (multiplayer-impl.md §5.6). */
2
- import { z } from 'zod';
3
- import { PlayerLite } from '../../entities';
4
- import { ChatLineEvent } from '../../api/mp/chat';
5
- import { Invite, InviteStatus, OpaqueId, Party } from '../../api/mp/parties';
6
- import { event, type EventDef } from '../define';
7
-
8
- /** The invite and party events, by event name. */
9
- export const partyEvents = {
10
- invite: event({
11
- d: Invite,
12
- doc: 'A friend invited you to a game.',
13
- }),
14
- 'invite.update': event({
15
- d: z.object({
16
- id: OpaqueId,
17
- status: InviteStatus,
18
- /** Who accepted or declined (absent for cancelled and expired). */
19
- by: PlayerLite.optional(),
20
- }),
21
- doc: 'An invite ended; sent to the inviter and the invitee (so its toast closes).',
22
- }),
23
- party: event({
24
- d: Party.nullable(),
25
- doc: "Your party's current state, or null when you left it or it dissolved.",
26
- }),
27
- 'party.chat': event({
28
- d: ChatLineEvent,
29
- doc: 'A party chat line.',
30
- }),
31
- } satisfies Record<string, EventDef>;
1
+ /** Invite and party events (multiplayer-impl.md §5.6). */
2
+ import { z } from 'zod';
3
+ import { PlayerLite } from '../../entities';
4
+ import { ChatLineEvent } from '../../api/mp/chat';
5
+ import { Invite, InviteStatus, OpaqueId, Party } from '../../api/mp/parties';
6
+ import { event, type EventDef } from '../define';
7
+
8
+ /** The invite and party events, by event name. */
9
+ export const partyEvents = {
10
+ invite: event({
11
+ d: Invite,
12
+ doc: 'A friend invited you to a game.',
13
+ }),
14
+ 'invite.update': event({
15
+ d: z.object({
16
+ id: OpaqueId,
17
+ status: InviteStatus,
18
+ /** Who accepted or declined (absent for cancelled and expired). */
19
+ by: PlayerLite.optional(),
20
+ }),
21
+ doc: 'An invite ended; sent to the inviter and the invitee (so its toast closes).',
22
+ }),
23
+ party: event({
24
+ d: Party.nullable(),
25
+ doc: "Your party's current state, or null when you left it or it dissolved.",
26
+ }),
27
+ 'party.chat': event({
28
+ d: ChatLineEvent,
29
+ doc: 'A party chat line.',
30
+ }),
31
+ } satisfies Record<string, EventDef>;
@@ -1,27 +1,27 @@
1
- /** Race events (docs/plans/race.md §4). */
2
- import { z } from 'zod';
3
- import { UserId } from '../../common';
4
- import { RaceProgress, RaceResult, RaceStart, RaceStats } from '../../api/mp/races';
5
- import { event, type EventDef } from '../define';
6
-
7
- /** The race events, by event name. */
8
- export const raceEvents = {
9
- 'race.start': event({
10
- d: RaceStart,
11
- doc: 'The countdown began: the seed is revealed now.',
12
- }),
13
- 'race.status': event({
14
- d: z.object({
15
- userId: UserId,
16
- stats: RaceStats,
17
- progress: RaceProgress.optional(),
18
- /** ms since the go (server clock). */
19
- at: z.number().int().nonnegative(),
20
- }),
21
- doc: "Another racer's live status, for the race HUD.",
22
- }),
23
- 'race.result': event({
24
- d: z.object({ result: RaceResult }),
25
- doc: 'The race was decided.',
26
- }),
27
- } satisfies Record<string, EventDef>;
1
+ /** Race events (docs/plans/race.md §4). */
2
+ import { z } from 'zod';
3
+ import { UserId } from '../../common';
4
+ import { RaceProgress, RaceResult, RaceStart, RaceStats } from '../../api/mp/races';
5
+ import { event, type EventDef } from '../define';
6
+
7
+ /** The race events, by event name. */
8
+ export const raceEvents = {
9
+ 'race.start': event({
10
+ d: RaceStart,
11
+ doc: 'The countdown began: the seed is revealed now.',
12
+ }),
13
+ 'race.status': event({
14
+ d: z.object({
15
+ userId: UserId,
16
+ stats: RaceStats,
17
+ progress: RaceProgress.optional(),
18
+ /** ms since the go (server clock). */
19
+ at: z.number().int().nonnegative(),
20
+ }),
21
+ doc: "Another racer's live status, for the race HUD.",
22
+ }),
23
+ 'race.result': event({
24
+ d: z.object({ result: RaceResult }),
25
+ doc: 'The race was decided.',
26
+ }),
27
+ } satisfies Record<string, EventDef>;
@@ -1,48 +1,48 @@
1
- /** Hub relay room events (multiplayer-impl.md §6). Race rooms send these too. */
2
- import { z } from 'zod';
3
- import { Json, UserId } from '../../common';
4
- import { ChatLineEvent } from '../../api/mp/chat';
5
- import { OpaqueId } from '../../api/mp/parties';
6
- import { RoomClosedReason, RoomInfo, RoomMessage, RoomState, SeatGoneReason } from '../../api/mp/rooms';
7
- import { event, type EventDef } from '../define';
8
-
9
- /** The room events, by event name. */
10
- export const roomEvents = {
11
- room: event({
12
- d: z.object({
13
- room: RoomInfo,
14
- /** What the host passed to `room.end`; present once, on that update. */
15
- results: Json.optional(),
16
- }),
17
- doc: 'The room changed: roster, ready flags, phase, host, race state.',
18
- }),
19
- 'room.msg': event({
20
- d: RoomMessage,
21
- doc: 'A relayed game message.',
22
- }),
23
- 'room.state': event({
24
- d: z.object({
25
- /** Shallow-merged into the state unless `replace`; a `null` value deletes its key. */
26
- patch: RoomState,
27
- replace: z.boolean(),
28
- seq: z.number().int().positive(),
29
- }),
30
- doc: 'The shared state changed.',
31
- }),
32
- 'room.chat': event({
33
- d: ChatLineEvent,
34
- doc: 'A room chat line.',
35
- }),
36
- 'room.kicked': event({
37
- d: z.object({ roomId: OpaqueId }),
38
- doc: 'The host removed you from the room.',
39
- }),
40
- 'room.closed': event({
41
- d: z.object({ reason: RoomClosedReason }),
42
- doc: 'The room is gone for this tab (emptied, ended, host left, moved, another tab took the seat, removed).',
43
- }),
44
- 'room.abandoned': event({
45
- d: z.object({ userId: UserId, reason: SeatGoneReason }),
46
- doc: 'A player left mid-game (or their reconnect grace ran out); the game decides what that means.',
47
- }),
48
- } satisfies Record<string, EventDef>;
1
+ /** Hub relay room events (multiplayer-impl.md §6). Race rooms send these too. */
2
+ import { z } from 'zod';
3
+ import { Json, UserId } from '../../common';
4
+ import { ChatLineEvent } from '../../api/mp/chat';
5
+ import { OpaqueId } from '../../api/mp/parties';
6
+ import { RoomClosedReason, RoomInfo, RoomMessage, RoomState, SeatGoneReason } from '../../api/mp/rooms';
7
+ import { event, type EventDef } from '../define';
8
+
9
+ /** The room events, by event name. */
10
+ export const roomEvents = {
11
+ room: event({
12
+ d: z.object({
13
+ room: RoomInfo,
14
+ /** What the host passed to `room.end`; present once, on that update. */
15
+ results: Json.optional(),
16
+ }),
17
+ doc: 'The room changed: roster, ready flags, phase, host, race state.',
18
+ }),
19
+ 'room.msg': event({
20
+ d: RoomMessage,
21
+ doc: 'A relayed game message.',
22
+ }),
23
+ 'room.state': event({
24
+ d: z.object({
25
+ /** Shallow-merged into the state unless `replace`; a `null` value deletes its key. */
26
+ patch: RoomState,
27
+ replace: z.boolean(),
28
+ seq: z.number().int().positive(),
29
+ }),
30
+ doc: 'The shared state changed.',
31
+ }),
32
+ 'room.chat': event({
33
+ d: ChatLineEvent,
34
+ doc: 'A room chat line.',
35
+ }),
36
+ 'room.kicked': event({
37
+ d: z.object({ roomId: OpaqueId }),
38
+ doc: 'The host removed you from the room.',
39
+ }),
40
+ 'room.closed': event({
41
+ d: z.object({ reason: RoomClosedReason }),
42
+ doc: 'The room is gone for this tab (emptied, ended, host left, moved, another tab took the seat, removed).',
43
+ }),
44
+ 'room.abandoned': event({
45
+ d: z.object({ userId: UserId, reason: SeatGoneReason }),
46
+ doc: 'A player left mid-game (or their reconnect grace ran out); the game decides what that means.',
47
+ }),
48
+ } satisfies Record<string, EventDef>;
@@ -1,81 +1,81 @@
1
- /**
2
- * Socket events for presence, counts, friends, typing, config, DMs, notices
3
- * and suspension (multiplayer-impl.md §4.6). Every event goes to all of the
4
- * recipient's tabs.
5
- */
6
- import { z } from 'zod';
7
- import { Millis, UserId } from '../../common';
8
- import { PlayerLite } from '../../entities';
9
- import { Message, MessageId } from '../../api/messages';
10
- import { SocialConfig } from '../../api/social/config';
11
- import { FriendChange } from '../../api/social/people';
12
- import { Counts, Presence } from '../../api/social/presence';
13
- import { event, type EventDef } from '../define';
14
-
15
- /** What kind of thing a notice is about. `renamed`: an admin changed your username. */
16
- export const NoticeKind = z.enum(['dm', 'friend_request', 'friend_online', 'lobby_open', 'invite', 'suggestion', 'renamed']);
17
-
18
- /** A notice kind. */
19
- export type NoticeKind = z.infer<typeof NoticeKind>;
20
-
21
- /** A toast (`ev:notice`), sent instead of a push when the player has a visible tab. */
22
- export const Notice = z.object({
23
- kind: NoticeKind,
24
- title: z.string(),
25
- body: z.string().optional(),
26
- /** A button: its label and the page it opens. */
27
- action: z.object({ label: z.string(), url: z.string() }).optional(),
28
- });
29
-
30
- /** A toast. */
31
- export type Notice = z.infer<typeof Notice>;
32
-
33
- /** The social events, by event name. */
34
- export const socialEvents = {
35
- presence: event({
36
- d: z.object({
37
- userId: UserId,
38
- /** `null`: offline, or no longer visible to you. */
39
- p: Presence.nullable(),
40
- }),
41
- doc: "A friend's presence changed.",
42
- }),
43
- counts: event({ d: Counts, doc: 'The online counts changed (broadcast to every tab, at most every 15 s).' }),
44
- friends: event({
45
- d: z.object({ change: FriendChange, user: PlayerLite }),
46
- doc: 'Your friends list changed (refetch `GET /social/friends`).',
47
- }),
48
- typing: event({ d: z.object({ from: UserId }), doc: 'A friend is typing to you.' }),
49
- config: event({ d: SocialConfig, doc: 'The site-wide social configuration changed (broadcast).' }),
50
- dm: event({
51
- d: z.object({
52
- message: Message,
53
- /** The other side of the conversation, from your point of view. */
54
- conversationWith: UserId,
55
- }),
56
- doc: 'A DM was delivered: to the recipient, and to the sender\'s other tabs.',
57
- }),
58
- 'dm.read': event({
59
- d: z.object({
60
- /** Who read. */
61
- by: UserId,
62
- /** Their messages up to and including this id are read. */
63
- upTo: MessageId,
64
- }),
65
- doc: 'Messages were read (to the sender, and to the reader\'s own tabs to sync badges).',
66
- }),
67
- 'dm.hidden': event({
68
- d: z.object({ ids: z.array(MessageId) }),
69
- doc: 'A moderator hid these messages: drop them from both sides of the conversation.',
70
- }),
71
- notice: event({ d: Notice, doc: 'A toast to show.' }),
72
- suspended: event({
73
- d: z.object({
74
- /** When the suspension ends (epoch ms). */
75
- until: Millis,
76
- /** The moderator's reason, when given. */
77
- reason: z.string().optional(),
78
- }),
79
- doc: 'The account is suspended; the socket then closes with 4003.',
80
- }),
81
- } satisfies Record<string, EventDef>;
1
+ /**
2
+ * Socket events for presence, counts, friends, typing, config, DMs, notices
3
+ * and suspension (multiplayer-impl.md §4.6). Every event goes to all of the
4
+ * recipient's tabs.
5
+ */
6
+ import { z } from 'zod';
7
+ import { Millis, UserId } from '../../common';
8
+ import { PlayerLite } from '../../entities';
9
+ import { Message, MessageId } from '../../api/messages';
10
+ import { SocialConfig } from '../../api/social/config';
11
+ import { FriendChange } from '../../api/social/people';
12
+ import { Counts, Presence } from '../../api/social/presence';
13
+ import { event, type EventDef } from '../define';
14
+
15
+ /** What kind of thing a notice is about. `renamed`: an admin changed your username. */
16
+ export const NoticeKind = z.enum(['dm', 'friend_request', 'friend_online', 'lobby_open', 'invite', 'suggestion', 'renamed']);
17
+
18
+ /** A notice kind. */
19
+ export type NoticeKind = z.infer<typeof NoticeKind>;
20
+
21
+ /** A toast (`ev:notice`), sent instead of a push when the player has a visible tab. */
22
+ export const Notice = z.object({
23
+ kind: NoticeKind,
24
+ title: z.string(),
25
+ body: z.string().optional(),
26
+ /** A button: its label and the page it opens. */
27
+ action: z.object({ label: z.string(), url: z.string() }).optional(),
28
+ });
29
+
30
+ /** A toast. */
31
+ export type Notice = z.infer<typeof Notice>;
32
+
33
+ /** The social events, by event name. */
34
+ export const socialEvents = {
35
+ presence: event({
36
+ d: z.object({
37
+ userId: UserId,
38
+ /** `null`: offline, or no longer visible to you. */
39
+ p: Presence.nullable(),
40
+ }),
41
+ doc: "A friend's presence changed.",
42
+ }),
43
+ counts: event({ d: Counts, doc: 'The online counts changed (broadcast to every tab, at most every 15 s).' }),
44
+ friends: event({
45
+ d: z.object({ change: FriendChange, user: PlayerLite }),
46
+ doc: 'Your friends list changed (refetch `GET /social/friends`).',
47
+ }),
48
+ typing: event({ d: z.object({ from: UserId }), doc: 'A friend is typing to you.' }),
49
+ config: event({ d: SocialConfig, doc: 'The site-wide social configuration changed (broadcast).' }),
50
+ dm: event({
51
+ d: z.object({
52
+ message: Message,
53
+ /** The other side of the conversation, from your point of view. */
54
+ conversationWith: UserId,
55
+ }),
56
+ doc: 'A DM was delivered: to the recipient, and to the sender\'s other tabs.',
57
+ }),
58
+ 'dm.read': event({
59
+ d: z.object({
60
+ /** Who read. */
61
+ by: UserId,
62
+ /** Their messages up to and including this id are read. */
63
+ upTo: MessageId,
64
+ }),
65
+ doc: 'Messages were read (to the sender, and to the reader\'s own tabs to sync badges).',
66
+ }),
67
+ 'dm.hidden': event({
68
+ d: z.object({ ids: z.array(MessageId) }),
69
+ doc: 'A moderator hid these messages: drop them from both sides of the conversation.',
70
+ }),
71
+ notice: event({ d: Notice, doc: 'A toast to show.' }),
72
+ suspended: event({
73
+ d: z.object({
74
+ /** When the suspension ends (epoch ms). */
75
+ until: Millis,
76
+ /** The moderator's reason, when given. */
77
+ reason: z.string().optional(),
78
+ }),
79
+ doc: 'The account is suspended; the socket then closes with 4003.',
80
+ }),
81
+ } satisfies Record<string, EventDef>;
@@ -1,24 +1,24 @@
1
- /**
2
- * The `/_ws` socket contract: the legacy envelope plus every op and event,
3
- * by name.
4
- */
5
- import { multiplayerEvents } from './events/multiplayer';
6
- import { socialEvents } from './events/social';
7
- import { helloOps } from './ops/hello';
8
- import { multiplayerOps } from './ops/multiplayer';
9
- import { socialOps } from './ops/social';
10
-
11
- export * from './define';
12
- export * from './envelope';
13
-
14
- /** Every socket op, by op name (an intersection type keeps the .d.ts small). */
15
- export const ops: typeof helloOps & typeof socialOps & typeof multiplayerOps = { ...helloOps, ...socialOps, ...multiplayerOps };
16
-
17
- /** Every socket event, by event name (an intersection type keeps the .d.ts small). */
18
- export const events: typeof socialEvents & typeof multiplayerEvents = { ...socialEvents, ...multiplayerEvents };
19
-
20
- /** A socket op name. */
21
- export type OpName = keyof typeof ops;
22
-
23
- /** A socket event name. */
24
- export type EventName = keyof typeof events;
1
+ /**
2
+ * The `/_ws` socket contract: the legacy envelope plus every op and event,
3
+ * by name.
4
+ */
5
+ import { multiplayerEvents } from './events/multiplayer';
6
+ import { socialEvents } from './events/social';
7
+ import { helloOps } from './ops/hello';
8
+ import { multiplayerOps } from './ops/multiplayer';
9
+ import { socialOps } from './ops/social';
10
+
11
+ export * from './define';
12
+ export * from './envelope';
13
+
14
+ /** Every socket op, by op name (an intersection type keeps the .d.ts small). */
15
+ export const ops: typeof helloOps & typeof socialOps & typeof multiplayerOps = { ...helloOps, ...socialOps, ...multiplayerOps };
16
+
17
+ /** Every socket event, by event name (an intersection type keeps the .d.ts small). */
18
+ export const events: typeof socialEvents & typeof multiplayerEvents = { ...socialEvents, ...multiplayerEvents };
19
+
20
+ /** A socket op name. */
21
+ export type OpName = keyof typeof ops;
22
+
23
+ /** A socket event name. */
24
+ export type EventName = keyof typeof events;
@@ -1,8 +1,8 @@
1
- /** The empty input and ack many socket ops share. */
2
- import { z } from 'zod';
3
-
4
- /** An op that takes no input (`d` absent or `{}`). */
5
- export const NoInput = z.object({});
6
-
7
- /** An ack with no payload (`{}`). */
8
- export const EmptyAck = z.object({});
1
+ /** The empty input and ack many socket ops share. */
2
+ import { z } from 'zod';
3
+
4
+ /** An op that takes no input (`d` absent or `{}`). */
5
+ export const NoInput = z.object({});
6
+
7
+ /** An ack with no payload (`{}`). */
8
+ export const EmptyAck = z.object({});