@diffenderfer-games/hub 2.0.0-next.1 → 2.0.0-next.2

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 (90) hide show
  1. package/CHANGELOG.md +36 -9
  2. package/README.md +25 -10
  3. package/dist/auto.d.ts +6 -6
  4. package/dist/auto.js +2 -2
  5. package/dist/doctor/cli.mjs +14 -7
  6. package/dist/doctor/cli.mjs.map +1 -1
  7. package/dist/index.d.ts +6 -6
  8. package/dist/index.js +2 -2
  9. package/dist/mp.d.ts +3 -3
  10. package/dist/race.d.ts +1 -1
  11. package/dist/rooms.d.ts +3 -3
  12. package/dist/shared/bridge.js +9 -3
  13. package/dist/shared/bridge.js.map +1 -1
  14. package/dist/shared/client-version.gen.js +1 -1
  15. package/dist/shared/daily.d.ts +10 -4
  16. package/dist/shared/daily.d.ts.map +1 -1
  17. package/dist/shared/daily.js +12 -2
  18. package/dist/shared/daily.js.map +1 -1
  19. package/dist/shared/index.d.ts +314 -35
  20. package/dist/shared/index.d.ts.map +1 -1
  21. package/dist/shared/input.js +7 -2
  22. package/dist/shared/input.js.map +1 -1
  23. package/dist/shared/latest-state.js +1 -1
  24. package/dist/shared/mp.js +10 -2
  25. package/dist/shared/mp.js.map +1 -1
  26. package/dist/shared/multiplayer-types.d.ts +15 -2
  27. package/dist/shared/multiplayer-types.d.ts.map +1 -1
  28. package/dist/shared/race-types.d.ts +1 -1
  29. package/dist/shared/race.js +1 -1
  30. package/dist/shared/room-types.d.ts +28 -3
  31. package/dist/shared/room-types.d.ts.map +1 -1
  32. package/dist/shared/rooms.d.ts.map +1 -1
  33. package/dist/shared/rooms.js +10 -2
  34. package/dist/shared/rooms.js.map +1 -1
  35. package/dist/shared/runtime-opener.js +1 -1
  36. package/dist/shared/social.d.ts +23 -8
  37. package/dist/shared/social.d.ts.map +1 -1
  38. package/dist/shared/social.js +6 -1
  39. package/dist/shared/social.js.map +1 -1
  40. package/dist/shared/src.js +1588 -388
  41. package/dist/shared/src.js.map +1 -1
  42. package/dist/shared/wire-types.d.ts +6 -2
  43. package/dist/shared/wire-types.d.ts.map +1 -1
  44. package/dist/social.d.ts +5 -5
  45. package/dist/social.js +1 -1
  46. package/package.json +2 -2
  47. package/src/core/connection.ts +187 -92
  48. package/src/core/game-data.ts +11 -9
  49. package/src/core/hub-economy.ts +47 -10
  50. package/src/core/hub-progress.ts +13 -16
  51. package/src/core/hub.ts +4 -2
  52. package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
  53. package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
  54. package/src/core/offline/hub-kinds/game-fields.ts +2 -0
  55. package/src/core/offline/hub-kinds/index.ts +35 -0
  56. package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
  57. package/src/core/offline/hub-kinds/progress-kinds.ts +148 -0
  58. package/src/core/offline/kind-adapters.ts +48 -0
  59. package/src/core/offline/local-inventory.ts +64 -0
  60. package/src/core/offline/local-store.ts +20 -22
  61. package/src/core/offline/offline-api.ts +65 -0
  62. package/src/core/offline/outbox-entry.ts +59 -0
  63. package/src/core/offline/outbox-kinds.ts +78 -0
  64. package/src/core/offline/outbox-merge.ts +39 -0
  65. package/src/core/offline/outbox-replayer.ts +141 -0
  66. package/src/core/offline/outbox-store.ts +85 -0
  67. package/src/core/offline/outbox.ts +232 -0
  68. package/src/core/offline/queue.ts +4 -49
  69. package/src/core/offline/replay-lease.ts +87 -0
  70. package/src/core/offline/replay-policy.ts +52 -0
  71. package/src/core/rest/rest-client.ts +14 -3
  72. package/src/core/rest/routes.gen.ts +108 -87
  73. package/src/core/storage/json-guards.ts +6 -0
  74. package/src/core/version/client-version.gen.ts +1 -1
  75. package/src/daily/daily-system.ts +8 -1
  76. package/src/daily/types.ts +3 -2
  77. package/src/doctor/checks.ts +10 -2
  78. package/src/index.ts +6 -1
  79. package/src/input/input-registry.ts +9 -2
  80. package/src/mp.ts +1 -1
  81. package/src/realtime/bridge.ts +5 -0
  82. package/src/realtime/multiplayer-forwarder.ts +13 -1
  83. package/src/realtime/multiplayer-types.ts +15 -1
  84. package/src/realtime/room-types.ts +28 -2
  85. package/src/realtime/rooms-forwarder.ts +13 -1
  86. package/src/realtime/runtime-api.ts +19 -8
  87. package/src/realtime/social-forwarder.ts +5 -0
  88. package/src/realtime/social-types.ts +8 -0
  89. package/src/realtime/wire-types.ts +7 -0
  90. package/src/rooms.ts +1 -1
@@ -117,5 +117,10 @@ export async function rethrowAsHubError<Result>(pending: Promise<Result>): Promi
117
117
  }
118
118
  }
119
119
 
120
+ /** The rejection for a feature the page's social runtime is too old to have (version 3 additions). */
121
+ export function outdatedRuntime(feature: string): Error {
122
+ return hubLink().createOfflineError(`${feature} needs the newest hub. Reload the page to update.`);
123
+ }
124
+
120
125
  /** The page's one bridge (per client bundle). */
121
126
  export const pageBridge = new RealtimeBridge();
@@ -3,7 +3,7 @@
3
3
  * forwarded to the social runtime.
4
4
  */
5
5
  import { getUiStack } from '../uistack/page-stack';
6
- import { pageBridge } from './bridge';
6
+ import { outdatedRuntime, pageBridge } from './bridge';
7
7
  import { hubLink } from './hub-link';
8
8
  import { latestState } from './latest-state';
9
9
  import type { HubMpApi } from './multiplayer-types';
@@ -25,4 +25,16 @@ export const multiplayerForwarder: HubMpApi = {
25
25
  ticket: () => hubLink().getGameServerTicket(),
26
26
  setJoinInfo: (partyId, joinInfo) => pageBridge.call((realtime) => realtime.mp.setJoinInfo(partyId, joinInfo)),
27
27
  party: () => pageBridge.current()?.mp.party() ?? null,
28
+ inviteLink: () => pageBridge.call((realtime) => {
29
+ if (realtime.mp.inviteLink === undefined) {
30
+ throw outdatedRuntime('Invite links');
31
+ }
32
+ return realtime.mp.inviteLink();
33
+ }),
34
+ revokeInviteLink: (token) => pageBridge.call((realtime) => {
35
+ if (realtime.mp.revokeInviteLink === undefined) {
36
+ throw outdatedRuntime('Invite links');
37
+ }
38
+ return realtime.mp.revokeInviteLink(token);
39
+ }),
28
40
  };
@@ -3,6 +3,7 @@
3
3
  * launches, the leave guard and own-server tickets.
4
4
  */
5
5
  import type { Invite, JoinInfo, Launch, Party } from '@diffenderfer-games/hub-contract';
6
+ import type { InviteLink } from './wire-types';
6
7
 
7
8
  /** `hub.mp.onJoinInfo` payload: the party leader published where to connect. */
8
9
  export interface JoinInfoEvent {
@@ -30,8 +31,21 @@ export interface BusyOptions {
30
31
  label?: string;
31
32
  }
32
33
 
34
+ /** What feature version 3 added to `hub.mp`: invite links. */
35
+ export interface InviteLinkMethods {
36
+ /**
37
+ * A shareable link to the room I'm in (my live one when it exists): anyone
38
+ * who opens it joins while a seat is free, otherwise watches. It expires
39
+ * after 24 h. Claimed accounts only; rejects with `HubError`
40
+ * (`not_in_room`, `too_many_links`, `rate_limited`) or `OfflineError`.
41
+ */
42
+ inviteLink(): Promise<InviteLink>;
43
+ /** Revoke a link I made, or one into a room I host. */
44
+ revokeInviteLink(token: string): Promise<void>;
45
+ }
46
+
33
47
  /** `hub.mp`: the multiplayer lifecycle (docs/multiplayer.md §7). */
34
- export interface HubMpApi {
48
+ export interface HubMpApi extends InviteLinkMethods {
35
49
  /** Invite, join and watch arrivals. Call at boot; launches that arrived earlier are buffered. */
36
50
  onLaunch(listener: (launch: Launch) => void): () => void;
37
51
  /** The party leader published join info (e.g. a room code). */
@@ -3,7 +3,7 @@
3
3
  * multiplayer for static games, relayed (never refereed) by the hub.
4
4
  */
5
5
  import type { Json, RoomInfo } from '@diffenderfer-games/hub-contract';
6
- import type { RoomChatLine, RoomMessage } from './wire-types';
6
+ import type { RoomChatLine, RoomMessage, RoomSeat, RoomSeatChange } from './wire-types';
7
7
 
8
8
  /** The shared room state (a JSON object, at most 32 KB). */
9
9
  export type RoomState = Record<string, Json>;
@@ -20,6 +20,13 @@ export interface RoomCreateOptions {
20
20
  maxPlayers?: number;
21
21
  /** `host` (default): guests' unaddressed sends reach the host only. `relay`: everyone hears every send. */
22
22
  model?: 'host' | 'relay';
23
+ /**
24
+ * When the host role moves on mid-game: `grace` (default) once the host's
25
+ * seat is freed (left, or 3 minutes away), so a host holding hidden state
26
+ * can come back; `drop` as soon as the host drops, for games whose truth is
27
+ * all in `setState`.
28
+ */
29
+ hostMigration?: 'grace' | 'drop';
23
30
  /** Seat this party in the room. */
24
31
  partyId?: string;
25
32
  }
@@ -52,6 +59,13 @@ export interface RoomEvents {
52
59
  ended: { room: RoomInfo; results?: Json };
53
60
  /** A player left mid-game (or their reconnect grace ran out). */
54
61
  abandoned: { userId: number; reason?: string };
62
+ /**
63
+ * A seat changed: `away` (dropped; held until `graceEndsAt`, so hand it to
64
+ * your computer player), `back` (reclaimed: give control back) or `gone`
65
+ * (freed: left, kicked or the grace ran out). The game never ends on its
66
+ * own because of this; `info.hostId` may have moved to another player.
67
+ */
68
+ seat: RoomSeatChange;
55
69
  }
56
70
 
57
71
  /** Who I am in a room. */
@@ -84,6 +98,8 @@ export interface HubRoom {
84
98
  readonly state: RoomState;
85
99
  /** Who I am in this room. */
86
100
  readonly me: RoomSelf;
101
+ /** The seats as last reported; `connected: false` while a seat is away. */
102
+ seats(): RoomSeat[];
87
103
  /** Subscribe to a room event. Returns the unsubscribe function. */
88
104
  on<Name extends keyof RoomEvents>(event: Name, listener: (payload: RoomEvents[Name]) => void): () => void;
89
105
  /** Mark my seat ready (or not). */
@@ -114,8 +130,18 @@ export interface RoomListOptions {
114
130
  mode?: string;
115
131
  }
116
132
 
133
+ /** What feature version 3 added to `hub.rooms`. */
134
+ export interface RoomReclaimMethods {
135
+ /**
136
+ * Take back the seat I still hold in this game (after a reload, from
137
+ * another tab): the room, or `null` when I hold none. The hub also does
138
+ * this on its own at boot, as a `join` launch with `via: 'rejoin'`.
139
+ */
140
+ reclaim(): Promise<HubRoom | null>;
141
+ }
142
+
117
143
  /** `hub.rooms`: hub relay rooms (static games, no server). */
118
- export interface HubRoomsApi {
144
+ export interface HubRoomsApi extends RoomReclaimMethods {
119
145
  /** Create a room and take its first seat. */
120
146
  create(options?: RoomCreateOptions): Promise<HubRoom>;
121
147
  /** Join a room by its 5-character code or its id. */
@@ -3,8 +3,9 @@
3
3
  * wrapped so its rejections use the core client's error classes.
4
4
  */
5
5
  import type { Json, RoomInfo } from '@diffenderfer-games/hub-contract';
6
- import { pageBridge, rethrowAsHubError } from './bridge';
6
+ import { outdatedRuntime, pageBridge, rethrowAsHubError } from './bridge';
7
7
  import type { HubRoom, HubRoomsApi, RoomEvents, RoomSelf, RoomSendOptions, RoomState, RoomStateOptions } from './room-types';
8
+ import type { RoomSeat } from './wire-types';
8
9
 
9
10
  /** A runtime room whose rejections are re-created as the core client's errors. */
10
11
  class ForwardedRoom implements HubRoom {
@@ -26,6 +27,10 @@ class ForwardedRoom implements HubRoom {
26
27
  return this.room.me;
27
28
  }
28
29
 
30
+ seats(): RoomSeat[] {
31
+ return this.room.info.seats;
32
+ }
33
+
29
34
  on<Name extends keyof RoomEvents>(event: Name, listener: (payload: RoomEvents[Name]) => void): () => void {
30
35
  return this.room.on(event, listener);
31
36
  }
@@ -76,4 +81,11 @@ export const roomsForwarder: HubRoomsApi = {
76
81
  create: (options) => pageBridge.call(async (realtime) => new ForwardedRoom(await realtime.rooms.create(options))),
77
82
  join: (codeOrId, options) => pageBridge.call(async (realtime) => new ForwardedRoom(await realtime.rooms.join(codeOrId, options))),
78
83
  list: (options) => pageBridge.call((realtime) => realtime.rooms.list(options)),
84
+ reclaim: () => pageBridge.call(async (realtime) => {
85
+ if (realtime.rooms.reclaim === undefined) {
86
+ throw outdatedRuntime('Taking back a seat');
87
+ }
88
+ const held = await realtime.rooms.reclaim();
89
+ return held === null ? null : new ForwardedRoom(held);
90
+ }),
79
91
  };
@@ -3,16 +3,27 @@
3
3
  * the page, and that `hub.presence`, `hub.social`, `hub.mp`, `hub.rooms`,
4
4
  * `hub.notify` and `hub.race` forward to.
5
5
  */
6
- import type { HubMpApi } from './multiplayer-types';
6
+ import type { HubMpApi, InviteLinkMethods } from './multiplayer-types';
7
7
  import type { HubRaceApi } from './race-types';
8
- import type { HubRoomsApi } from './room-types';
9
- import type { HubNotifyApi, HubPresenceApi, HubSocialApi } from './social-types';
8
+ import type { HubRoomsApi, RoomReclaimMethods } from './room-types';
9
+ import type { HubNotifyApi, HubPresenceApi, HubSocialApi, PlayOnlineMethods } from './social-types';
10
10
 
11
11
  /**
12
12
  * The realtime feature version the runtime reports in {@link RealtimeApi.version}.
13
- * 2 added races; a client feature-detects newer additions with it.
13
+ * 2 added races; 3 added invite links and taking back a held seat; 4 added
14
+ * "Play online" (tournaments). A client feature-detects newer additions with
15
+ * it (or by the optional method).
14
16
  */
15
- export const REALTIME_FEATURE_VERSION = 2;
17
+ export const REALTIME_FEATURE_VERSION = 4;
18
+
19
+ /** `hub.social` as a runtime installs it: version 4's `openPlayOnline` is missing on older runtimes. */
20
+ export type RuntimeSocialApi = Omit<HubSocialApi, keyof PlayOnlineMethods> & Partial<PlayOnlineMethods>;
21
+
22
+ /** `hub.mp` as a runtime installs it: version 3's invite-link methods are missing on older runtimes. */
23
+ export type RuntimeMpApi = Omit<HubMpApi, keyof InviteLinkMethods> & Partial<InviteLinkMethods>;
24
+
25
+ /** `hub.rooms` as a runtime installs it: version 3's `reclaim` is missing on older runtimes. */
26
+ export type RuntimeRoomsApi = Omit<HubRoomsApi, keyof RoomReclaimMethods> & Partial<RoomReclaimMethods>;
16
27
 
17
28
  /**
18
29
  * The cross-bundle shape number of the `rt` page singleton (plan §4.4). The
@@ -37,11 +48,11 @@ export interface RealtimeApi {
37
48
  /** Presence reporting. */
38
49
  presence: HubPresenceApi;
39
50
  /** Friends and the social dialogs. */
40
- social: HubSocialApi;
51
+ social: RuntimeSocialApi;
41
52
  /** Invites, launches, the leave guard and tickets. */
42
- mp: HubMpApi;
53
+ mp: RuntimeMpApi;
43
54
  /** Hub relay rooms. */
44
- rooms: HubRoomsApi;
55
+ rooms: RuntimeRoomsApi;
45
56
  /** Toast behaviour. */
46
57
  notify: HubNotifyApi;
47
58
  /** Races (feature version 2 and later). */
@@ -65,6 +65,11 @@ export const socialForwarder: HubSocialApi = {
65
65
  realtime.social.openNotify(options);
66
66
  });
67
67
  },
68
+ openPlayOnline: () => {
69
+ openWithRuntime((realtime) => {
70
+ realtime.social.openPlayOnline?.();
71
+ });
72
+ },
68
73
  friends: () => pageBridge.call((realtime) => realtime.social.friends()),
69
74
  counts: () => pageBridge.current()?.social.counts() ?? { ...NO_COUNTS },
70
75
  chatMode: (userIds) => pageBridge.current()?.social.chatMode(userIds) ?? 'off',
@@ -61,6 +61,11 @@ export interface HubSocialApi {
61
61
  openSuggest(): void;
62
62
  /** Open notification settings, optionally focused on a game's lobby bell. */
63
63
  openNotify(options?: { game?: string }): void;
64
+ /**
65
+ * Open "Play online": games waiting for a player, invites, tournaments and
66
+ * the quick 1v1 (feature version 4; a no-op on older runtimes).
67
+ */
68
+ openPlayOnline(): void;
64
69
  /** Friends with live presence. Rejects with `OfflineError` without the runtime. */
65
70
  friends(): Promise<FriendEntry[]>;
66
71
  /** Live counts; all zero until the runtime is up. */
@@ -71,6 +76,9 @@ export interface HubSocialApi {
71
76
  on<Name extends keyof SocialEvents>(event: Name, listener: (payload: SocialEvents[Name]) => void): () => void;
72
77
  }
73
78
 
79
+ /** What feature version 4 added to `hub.social`. */
80
+ export type PlayOnlineMethods = Pick<HubSocialApi, 'openPlayOnline'>;
81
+
74
82
  /** `hub.notify`: toast behaviour during intense play. */
75
83
  export interface HubNotifyApi {
76
84
  /** Quiet: toasts collapse into the menu badge; invites still show as a small pill. */
@@ -9,6 +9,7 @@ import type {
9
9
  OpAck,
10
10
  ops,
11
11
  InviteCreated as InviteCreatedSchema,
12
+ InviteLink as InviteLinkSchema,
12
13
  RaceHistory as RaceHistorySchema,
13
14
  RaceResultPlayer as RaceResultPlayerSchema,
14
15
  RaceRecordPlayer as RaceRecordPlayerSchema,
@@ -51,6 +52,12 @@ export type RoomMessage = ServerEvents['room.msg'];
51
52
  /** A shared-state change (`ev:room.state`); patches are shallow-merged unless `replace`. */
52
53
  export type RoomStateChange = ServerEvents['room.state'];
53
54
 
55
+ /** A seat went away, came back or is gone (`ev:room.seat`). */
56
+ export type RoomSeatChange = ServerEvents['room.seat'];
57
+
58
+ /** A shareable invite link into a room (`POST /mp/links`). */
59
+ export type InviteLink = SchemaOutput<typeof InviteLinkSchema>;
60
+
54
61
  /** A moderated room chat line (`ev:room.chat`). */
55
62
  export type RoomChatLine = ServerEvents['room.chat'];
56
63
 
package/src/rooms.ts CHANGED
@@ -6,5 +6,5 @@
6
6
  */
7
7
  export { roomsForwarder as rooms } from './realtime/rooms-forwarder';
8
8
  export type * from './realtime/room-types';
9
- export type { RoomChatLine, RoomMessage, RoomSeat, RoomStateChange } from './realtime/wire-types';
9
+ export type { RoomChatLine, RoomMessage, RoomSeat, RoomSeatChange, RoomStateChange } from './realtime/wire-types';
10
10
  export type { RoomInfo } from '@diffenderfer-games/hub-contract';