@diffenderfer-games/hub 2.0.0-next.0 → 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 (154) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -21
  3. package/README.md +206 -139
  4. package/dist/auto.d.ts +6 -6
  5. package/dist/auto.js +2 -2
  6. package/dist/doctor/cli.mjs +62 -25
  7. package/dist/doctor/cli.mjs.map +1 -1
  8. package/dist/index.d.ts +6 -6
  9. package/dist/index.js +2 -2
  10. package/dist/mp.d.ts +3 -3
  11. package/dist/race.d.ts +1 -1
  12. package/dist/rooms.d.ts +3 -3
  13. package/dist/shared/bridge.js +9 -3
  14. package/dist/shared/bridge.js.map +1 -1
  15. package/dist/shared/client-version.gen.js +1 -1
  16. package/dist/shared/daily.d.ts +10 -4
  17. package/dist/shared/daily.d.ts.map +1 -1
  18. package/dist/shared/daily.js +12 -2
  19. package/dist/shared/daily.js.map +1 -1
  20. package/dist/shared/globals.js +8 -3
  21. package/dist/shared/globals.js.map +1 -1
  22. package/dist/shared/index.d.ts +314 -35
  23. package/dist/shared/index.d.ts.map +1 -1
  24. package/dist/shared/input.js +7 -2
  25. package/dist/shared/input.js.map +1 -1
  26. package/dist/shared/latest-state.js +1 -1
  27. package/dist/shared/mp.js +10 -2
  28. package/dist/shared/mp.js.map +1 -1
  29. package/dist/shared/multiplayer-types.d.ts +15 -2
  30. package/dist/shared/multiplayer-types.d.ts.map +1 -1
  31. package/dist/shared/race-types.d.ts +1 -1
  32. package/dist/shared/race.js +1 -1
  33. package/dist/shared/room-types.d.ts +28 -3
  34. package/dist/shared/room-types.d.ts.map +1 -1
  35. package/dist/shared/rooms.d.ts.map +1 -1
  36. package/dist/shared/rooms.js +10 -2
  37. package/dist/shared/rooms.js.map +1 -1
  38. package/dist/shared/runtime-opener.js +1 -1
  39. package/dist/shared/social.d.ts +23 -8
  40. package/dist/shared/social.d.ts.map +1 -1
  41. package/dist/shared/social.js +6 -1
  42. package/dist/shared/social.js.map +1 -1
  43. package/dist/shared/src.js +1588 -388
  44. package/dist/shared/src.js.map +1 -1
  45. package/dist/shared/wire-types.d.ts +6 -2
  46. package/dist/shared/wire-types.d.ts.map +1 -1
  47. package/dist/social.d.ts +5 -5
  48. package/dist/social.js +1 -1
  49. package/package.json +89 -89
  50. package/src/auto.ts +39 -39
  51. package/src/core/connection.ts +394 -299
  52. package/src/core/errors.ts +147 -147
  53. package/src/core/game-data.ts +40 -38
  54. package/src/core/hub-economy.ts +216 -179
  55. package/src/core/hub-progress.ts +202 -205
  56. package/src/core/hub.ts +133 -131
  57. package/src/core/menu-control.ts +61 -61
  58. package/src/core/offline/events.ts +50 -50
  59. package/src/core/offline/favorites.ts +21 -21
  60. package/src/core/offline/hub-kinds/daily-kind.ts +68 -0
  61. package/src/core/offline/hub-kinds/economy-kinds.ts +86 -0
  62. package/src/core/offline/hub-kinds/game-fields.ts +2 -0
  63. package/src/core/offline/hub-kinds/index.ts +35 -0
  64. package/src/core/offline/hub-kinds/legacy-ops.ts +37 -0
  65. package/src/core/offline/hub-kinds/progress-kinds.ts +148 -0
  66. package/src/core/offline/kind-adapters.ts +48 -0
  67. package/src/core/offline/local-inventory.ts +64 -0
  68. package/src/core/offline/local-store.ts +259 -261
  69. package/src/core/offline/offline-api.ts +65 -0
  70. package/src/core/offline/outbox-entry.ts +59 -0
  71. package/src/core/offline/outbox-kinds.ts +78 -0
  72. package/src/core/offline/outbox-merge.ts +39 -0
  73. package/src/core/offline/outbox-replayer.ts +141 -0
  74. package/src/core/offline/outbox-store.ts +85 -0
  75. package/src/core/offline/outbox.ts +232 -0
  76. package/src/core/offline/queue.ts +126 -171
  77. package/src/core/offline/replay-lease.ts +87 -0
  78. package/src/core/offline/replay-policy.ts +52 -0
  79. package/src/core/page.ts +74 -74
  80. package/src/core/rest/endpoint-types.ts +22 -22
  81. package/src/core/rest/rest-client.ts +163 -152
  82. package/src/core/rest/routes.gen.ts +108 -87
  83. package/src/core/storage/json-guards.ts +80 -74
  84. package/src/core/storage/key-value.ts +66 -66
  85. package/src/core/ticket.ts +43 -43
  86. package/src/core/types.ts +134 -134
  87. package/src/core/version/client-version.gen.ts +1 -1
  88. package/src/core/version/hub-version.ts +62 -62
  89. package/src/core/version/semver.ts +97 -97
  90. package/src/daily/daily-system.ts +168 -161
  91. package/src/daily/page-daily.ts +36 -36
  92. package/src/daily/types.ts +68 -67
  93. package/src/daily.ts +10 -10
  94. package/src/doctor/checks.ts +86 -76
  95. package/src/doctor/cli.ts +98 -106
  96. package/src/doctor/hub-version-fetch.ts +43 -0
  97. package/src/doctor/tsconfig.json +12 -12
  98. package/src/globals.ts +115 -110
  99. package/src/index.ts +55 -50
  100. package/src/input/device-state.ts +146 -146
  101. package/src/input/edges.ts +55 -55
  102. package/src/input/gamepad.ts +61 -61
  103. package/src/input/input-events.ts +32 -32
  104. package/src/input/input-registry.ts +171 -164
  105. package/src/input/input-system.ts +393 -393
  106. package/src/input/legacy-controls.ts +64 -64
  107. package/src/input/nav-state.ts +191 -191
  108. package/src/input/navigator.ts +171 -171
  109. package/src/input/on-screen-controls.ts +101 -101
  110. package/src/input/page-control.ts +43 -43
  111. package/src/input/page-input.ts +193 -193
  112. package/src/input/player-remaps.ts +77 -77
  113. package/src/input/source-values.ts +116 -116
  114. package/src/input/spatial.ts +47 -47
  115. package/src/input/types.ts +331 -331
  116. package/src/input/zero-config.ts +177 -177
  117. package/src/input.ts +22 -22
  118. package/src/mp.ts +10 -10
  119. package/src/overlay/controls-panel.ts +163 -163
  120. package/src/overlay/dom.ts +46 -46
  121. package/src/overlay/layout-clusters.ts +162 -162
  122. package/src/overlay/layout-geometry.ts +136 -136
  123. package/src/overlay/layout.ts +203 -203
  124. package/src/overlay/listener-scope.ts +23 -23
  125. package/src/overlay/on-screen-keyboard.ts +137 -137
  126. package/src/overlay/overlay.ts +208 -208
  127. package/src/overlay/styles.ts +9 -9
  128. package/src/overlay/virtual-controls.ts +262 -262
  129. package/src/race.ts +10 -10
  130. package/src/realtime/bridge.ts +126 -121
  131. package/src/realtime/cross-bundle.ts +29 -29
  132. package/src/realtime/fallback-notice.ts +48 -48
  133. package/src/realtime/hub-link.ts +65 -65
  134. package/src/realtime/latest-state.ts +33 -33
  135. package/src/realtime/multiplayer-forwarder.ts +40 -28
  136. package/src/realtime/multiplayer-types.ts +66 -52
  137. package/src/realtime/page-runtime.ts +60 -60
  138. package/src/realtime/race-forwarder.ts +59 -59
  139. package/src/realtime/race-types.ts +132 -132
  140. package/src/realtime/room-types.ts +151 -125
  141. package/src/realtime/rooms-forwarder.ts +91 -79
  142. package/src/realtime/runtime-api.ts +60 -49
  143. package/src/realtime/runtime-opener.ts +61 -61
  144. package/src/realtime/social-forwarder.ts +77 -72
  145. package/src/realtime/social-types.ts +86 -78
  146. package/src/realtime/types.ts +53 -53
  147. package/src/realtime/wire-types.ts +79 -72
  148. package/src/rooms.ts +10 -10
  149. package/src/social.ts +18 -18
  150. package/src/types.ts +13 -13
  151. package/src/uistack/page-stack.ts +45 -45
  152. package/src/uistack/play-suspension.ts +82 -82
  153. package/src/uistack/types.ts +76 -76
  154. package/src/uistack/ui-stack.ts +144 -144
@@ -1,152 +1,163 @@
1
- /**
2
- * The REST transport: a small fetch router over the contract's route table
3
- * (plan §12 A10). It splits one merged fields object into the path, query
4
- * and JSON body the way ayepi's client does, stamps `x-hub-client`, and turns
5
- * failures into {@link HubError}s. It carries no schemas: replies are typed
6
- * from the contract and trusted, as ayepi's zod-free client trusts them.
7
- */
8
- import { errorFor, HubError, readErrorFields, UNSUPPORTED_BY_HUB } from '../errors';
9
- import { isRecord } from '../storage/json-guards';
10
- import { HUB_CLIENT_VERSION } from '../version/client-version.gen';
11
- import type { EndpointFields, EndpointName, EndpointReply } from './endpoint-types';
12
- import { ROUTES } from './routes.gen';
13
-
14
- /** The header that tells the hub which client version is calling. */
15
- export const CLIENT_VERSION_HEADER = 'x-hub-client';
16
-
17
- /** What both hubs answer for a route they do not have. */
18
- const NO_SUCH_ENDPOINT = 'No such API endpoint.';
19
-
20
- /** How a {@link RestClient} reaches the hub. */
21
- export interface RestClientOptions {
22
- /** The API's absolute base URL without a trailing slash, e.g. `https://diffenderfer.games/_api`. */
23
- readonly apiBase: string;
24
- /** The fetch to use. */
25
- readonly fetch: typeof fetch;
26
- /** Called whenever the hub answered at all (any status): the network is up. */
27
- readonly onReachable?: () => void;
28
- }
29
-
30
- /** The arguments of a call: the fields, optional when the endpoint takes none that are required. */
31
- export type CallArguments<K extends EndpointName> = Partial<EndpointFields<K>> extends EndpointFields<K>
32
- ? [fields?: EndpointFields<K>]
33
- : [fields: EndpointFields<K>];
34
-
35
- /** Typed calls to the hub's public REST API. */
36
- export interface RestClient {
37
- /** The API's absolute base URL. */
38
- readonly apiBase: string;
39
- /** Call an endpoint; resolves with its reply body, rejects with a {@link HubError}. */
40
- call<K extends EndpointName>(name: K, ...fieldsArgument: CallArguments<K>): Promise<EndpointReply<K>>;
41
- }
42
-
43
- /** One field value as URL text. */
44
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: a request field value of any contract type
45
- function urlText(fieldValue: unknown): string {
46
- if (typeof fieldValue === 'string') {
47
- return fieldValue;
48
- }
49
- if (typeof fieldValue === 'number' || typeof fieldValue === 'boolean') {
50
- return String(fieldValue);
51
- }
52
- return JSON.stringify(fieldValue);
53
- }
54
-
55
- /** The request one call makes: its URL (under the API base), method and JSON body. */
56
- export interface PlannedRequest {
57
- /** The path and query, e.g. `/games/pool/leaderboards/best?limit=50`. */
58
- readonly pathAndQuery: string;
59
- /** The HTTP method. */
60
- readonly method: string;
61
- /** The JSON body text, or `undefined` for none. */
62
- readonly bodyText: string | undefined;
63
- }
64
-
65
- /**
66
- * Split merged fields into path, query and body for one endpoint: `:name`
67
- * segments take their field, query keys go to the query string, and the
68
- * rest is the JSON body. POST and PUT always send a body (`{}` when empty),
69
- * as the v1 client did; DELETE sends one only when it has fields.
70
- */
71
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: the caller's typed fields, walked generically
72
- export function planRequest(name: EndpointName, fields: unknown): PlannedRequest {
73
- const route = ROUTES[name];
74
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: the caller's typed fields, walked generically
75
- const remaining = new Map<string, unknown>(Object.entries(isRecord(fields) ? fields : {}).filter(([, fieldValue]) => fieldValue !== undefined));
76
- const path = route.path.replace(/:([A-Za-z]+)/g, (_segment, paramName: string) => {
77
- const paramValue = remaining.get(paramName);
78
- remaining.delete(paramName);
79
- return encodeURIComponent(urlText(paramValue));
80
- });
81
- const queryKeys: readonly string[] = route.method === 'GET' ? [...remaining.keys()] : route.query;
82
- const query = new URLSearchParams();
83
- for (const key of queryKeys) {
84
- const queryValue = remaining.get(key);
85
- remaining.delete(key);
86
- if (queryValue !== undefined && queryValue !== null) {
87
- query.set(key, urlText(queryValue));
88
- }
89
- }
90
- const queryText = query.toString();
91
- const hasBody = route.method === 'POST' || route.method === 'PUT' || (route.method === 'DELETE' && remaining.size > 0);
92
- return {
93
- pathAndQuery: queryText === '' ? path : `${path}?${queryText}`,
94
- method: route.method,
95
- bodyText: hasBody ? JSON.stringify(Object.fromEntries(remaining)) : undefined,
96
- };
97
- }
98
-
99
- /**
100
- * The reply body, typed by the contract. This is the client's one trust
101
- * boundary on replies: there is no zod in the browser bundle (plan D16).
102
- */
103
- async function readReply<Reply>(response: Response): Promise<Reply> {
104
- try {
105
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- trust boundary: the hub's JSON reply, typed by the contract (the zod-free client trusts the server, plan D16)
106
- return await response.json();
107
- } catch {
108
- throw new HubError('The hub sent a reply that is not JSON.', response.status);
109
- }
110
- }
111
-
112
- /** The error for a failed response. */
113
- async function failureOf(name: EndpointName, response: Response): Promise<HubError> {
114
- let fields = readErrorFields(undefined);
115
- try {
116
- fields = readErrorFields(await response.json());
117
- } catch {
118
- // A non-JSON error body keeps the generic message.
119
- }
120
- if (response.status === 404 && fields.error === NO_SUCH_ENDPOINT) {
121
- return new HubError(`This hub does not have "${name}" yet: deploy the hub first, or use an older client.`, 404, { code: UNSUPPORTED_BY_HUB });
122
- }
123
- return errorFor(fields.error ?? `Request failed (${response.status})`, response.status, fields);
124
- }
125
-
126
- /** A REST client for one API base. */
127
- export function createRestClient(options: RestClientOptions): RestClient {
128
- // eslint-disable-next-line no-restricted-syntax -- trust boundary: the caller's typed fields, walked generically
129
- async function send<Reply>(name: EndpointName, fields: unknown): Promise<Reply> {
130
- const planned = planRequest(name, fields);
131
- let response: Response;
132
- try {
133
- response = await options.fetch(options.apiBase + planned.pathAndQuery, {
134
- method: planned.method,
135
- credentials: 'same-origin',
136
- headers: { 'content-type': 'application/json', [CLIENT_VERSION_HEADER]: HUB_CLIENT_VERSION },
137
- ...(planned.bodyText === undefined ? {} : { body: planned.bodyText }),
138
- });
139
- } catch (failure) {
140
- throw new HubError(`Hub unreachable: ${failure instanceof Error ? failure.message : String(failure)}`);
141
- }
142
- options.onReachable?.();
143
- if (!response.ok) {
144
- throw await failureOf(name, response);
145
- }
146
- return readReply<Reply>(response);
147
- }
148
- return {
149
- apiBase: options.apiBase,
150
- call: (name, ...fieldsArgument) => send(name, fieldsArgument[0]),
151
- };
152
- }
1
+ /**
2
+ * The REST transport: a small fetch router over the contract's route table
3
+ * (plan §12 A10). It splits one merged fields object into the path, query
4
+ * and JSON body the way ayepi's client does, stamps `x-hub-client`, and turns
5
+ * failures into {@link HubError}s. It carries no schemas: replies are typed
6
+ * from the contract and trusted, as ayepi's zod-free client trusts them.
7
+ */
8
+ import { errorFor, HubError, readErrorFields, UNSUPPORTED_BY_HUB } from '../errors';
9
+ import { isRecord } from '../storage/json-guards';
10
+ import { HUB_CLIENT_VERSION } from '../version/client-version.gen';
11
+ import type { EndpointFields, EndpointName, EndpointReply } from './endpoint-types';
12
+ import { OFFLINE_OP_HEADER, ROUTES } from './routes.gen';
13
+
14
+ /** The header that tells the hub which client version is calling. */
15
+ export const CLIENT_VERSION_HEADER = 'x-hub-client';
16
+
17
+ /** What both hubs answer for a route they do not have. */
18
+ const NO_SUCH_ENDPOINT = 'No such API endpoint.';
19
+
20
+ /** How a {@link RestClient} reaches the hub. */
21
+ export interface RestClientOptions {
22
+ /** The API's absolute base URL without a trailing slash, e.g. `https://diffenderfer.games/_api`. */
23
+ readonly apiBase: string;
24
+ /** The fetch to use. */
25
+ readonly fetch: typeof fetch;
26
+ /** Called whenever the hub answered at all (any status): the network is up. */
27
+ readonly onReachable?: () => void;
28
+ }
29
+
30
+ /** The arguments of a call: the fields, optional when the endpoint takes none that are required. */
31
+ export type CallArguments<K extends EndpointName> = Partial<EndpointFields<K>> extends EndpointFields<K>
32
+ ? [fields?: EndpointFields<K>]
33
+ : [fields: EndpointFields<K>];
34
+
35
+ /** Typed calls to the hub's public REST API. */
36
+ export interface RestClient {
37
+ /** The API's absolute base URL. */
38
+ readonly apiBase: string;
39
+ /** Call an endpoint; resolves with its reply body, rejects with a {@link HubError}. */
40
+ call<K extends EndpointName>(name: K, ...fieldsArgument: CallArguments<K>): Promise<EndpointReply<K>>;
41
+ /**
42
+ * Call an endpoint as part of an offline operation: an endpoint marked for
43
+ * offline replay gets `opKey` in the operation header, so the hub runs it
44
+ * once however often it is sent; any other endpoint is called as usual.
45
+ */
46
+ callOnce<K extends EndpointName>(opKey: string, name: K, ...fieldsArgument: CallArguments<K>): Promise<EndpointReply<K>>;
47
+ }
48
+
49
+ /** One field value as URL text. */
50
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: a request field value of any contract type
51
+ function urlText(fieldValue: unknown): string {
52
+ if (typeof fieldValue === 'string') {
53
+ return fieldValue;
54
+ }
55
+ if (typeof fieldValue === 'number' || typeof fieldValue === 'boolean') {
56
+ return String(fieldValue);
57
+ }
58
+ return JSON.stringify(fieldValue);
59
+ }
60
+
61
+ /** The request one call makes: its URL (under the API base), method and JSON body. */
62
+ export interface PlannedRequest {
63
+ /** The path and query, e.g. `/games/pool/leaderboards/best?limit=50`. */
64
+ readonly pathAndQuery: string;
65
+ /** The HTTP method. */
66
+ readonly method: string;
67
+ /** The JSON body text, or `undefined` for none. */
68
+ readonly bodyText: string | undefined;
69
+ }
70
+
71
+ /**
72
+ * Split merged fields into path, query and body for one endpoint: `:name`
73
+ * segments take their field, query keys go to the query string, and the
74
+ * rest is the JSON body. POST and PUT always send a body (`{}` when empty),
75
+ * as the v1 client did; DELETE sends one only when it has fields.
76
+ */
77
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: the caller's typed fields, walked generically
78
+ export function planRequest(name: EndpointName, fields: unknown): PlannedRequest {
79
+ const route = ROUTES[name];
80
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: the caller's typed fields, walked generically
81
+ const remaining = new Map<string, unknown>(Object.entries(isRecord(fields) ? fields : {}).filter(([, fieldValue]) => fieldValue !== undefined));
82
+ const path = route.path.replace(/:([A-Za-z]+)/g, (_segment, paramName: string) => {
83
+ const paramValue = remaining.get(paramName);
84
+ remaining.delete(paramName);
85
+ return encodeURIComponent(urlText(paramValue));
86
+ });
87
+ const queryKeys: readonly string[] = route.method === 'GET' ? [...remaining.keys()] : route.query;
88
+ const query = new URLSearchParams();
89
+ for (const key of queryKeys) {
90
+ const queryValue = remaining.get(key);
91
+ remaining.delete(key);
92
+ if (queryValue !== undefined && queryValue !== null) {
93
+ query.set(key, urlText(queryValue));
94
+ }
95
+ }
96
+ const queryText = query.toString();
97
+ const hasBody = route.method === 'POST' || route.method === 'PUT' || (route.method === 'DELETE' && remaining.size > 0);
98
+ return {
99
+ pathAndQuery: queryText === '' ? path : `${path}?${queryText}`,
100
+ method: route.method,
101
+ bodyText: hasBody ? JSON.stringify(Object.fromEntries(remaining)) : undefined,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * The reply body, typed by the contract. This is the client's one trust
107
+ * boundary on replies: there is no zod in the browser bundle (plan D16).
108
+ */
109
+ async function readReply<Reply>(response: Response): Promise<Reply> {
110
+ try {
111
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- trust boundary: the hub's JSON reply, typed by the contract (the zod-free client trusts the server, plan D16)
112
+ return await response.json();
113
+ } catch {
114
+ throw new HubError('The hub sent a reply that is not JSON.', response.status);
115
+ }
116
+ }
117
+
118
+ /** The error for a failed response. */
119
+ async function failureOf(name: EndpointName, response: Response): Promise<HubError> {
120
+ let fields = readErrorFields(undefined);
121
+ try {
122
+ fields = readErrorFields(await response.json());
123
+ } catch {
124
+ // A non-JSON error body keeps the generic message.
125
+ }
126
+ if (response.status === 404 && fields.error === NO_SUCH_ENDPOINT) {
127
+ return new HubError(`This hub does not have "${name}" yet: deploy the hub first, or use an older client.`, 404, { code: UNSUPPORTED_BY_HUB });
128
+ }
129
+ return errorFor(fields.error ?? `Request failed (${response.status})`, response.status, fields);
130
+ }
131
+
132
+ /** A REST client for one API base. */
133
+ export function createRestClient(options: RestClientOptions): RestClient {
134
+ // eslint-disable-next-line no-restricted-syntax -- trust boundary: the caller's typed fields, walked generically
135
+ async function send<Reply>(name: EndpointName, fields: unknown, opKey?: string): Promise<Reply> {
136
+ const planned = planRequest(name, fields);
137
+ let response: Response;
138
+ try {
139
+ response = await options.fetch(options.apiBase + planned.pathAndQuery, {
140
+ method: planned.method,
141
+ credentials: 'same-origin',
142
+ headers: {
143
+ 'content-type': 'application/json',
144
+ [CLIENT_VERSION_HEADER]: HUB_CLIENT_VERSION,
145
+ ...(opKey !== undefined && ROUTES[name].offline === 'replay' ? { [OFFLINE_OP_HEADER]: opKey } : {}),
146
+ },
147
+ ...(planned.bodyText === undefined ? {} : { body: planned.bodyText }),
148
+ });
149
+ } catch (failure) {
150
+ throw new HubError(`Hub unreachable: ${failure instanceof Error ? failure.message : String(failure)}`);
151
+ }
152
+ options.onReachable?.();
153
+ if (!response.ok) {
154
+ throw await failureOf(name, response);
155
+ }
156
+ return readReply<Reply>(response);
157
+ }
158
+ return {
159
+ apiBase: options.apiBase,
160
+ call: (name, ...fieldsArgument) => send(name, fieldsArgument[0]),
161
+ callOnce: (opKey, name, ...fieldsArgument) => send(name, fieldsArgument[0], opKey),
162
+ };
163
+ }
@@ -1,90 +1,111 @@
1
1
  // Generated by tools/hub-client/generate.ts. Do not edit; run `npm run gen:hub-client` in next/.
2
- /** Every public REST endpoint's method, path (under `/_api`) and query keys, from the contract. */
2
+ /** Every public REST endpoint's method, path (under `/_api`), query keys and offline mode (`'replay'` when it may be queued offline), from the contract. */
3
3
  export const ROUTES = {
4
- ensureGuestSession: { method: 'POST', path: '/auth/guest', query: [] },
5
- signUpWithPassword: { method: 'POST', path: '/auth/signup', query: [] },
6
- logInWithPassword: { method: 'POST', path: '/auth/login', query: [] },
7
- logOut: { method: 'POST', path: '/auth/logout', query: [] },
8
- getCurrentUser: { method: 'GET', path: '/me', query: [] },
9
- claimGuestAccount: { method: 'POST', path: '/auth/claim', query: [] },
10
- listGames: { method: 'GET', path: '/games', query: [] },
11
- listWhatsNewNotes: { method: 'GET', path: '/changes', query: ['game', 'since'] },
12
- listFavoriteGames: { method: 'GET', path: '/favorites', query: [] },
13
- setGameFavorite: { method: 'PUT', path: '/favorites/:slug', query: [] },
14
- markWhatsNewSeen: { method: 'POST', path: '/changes/seen', query: [] },
15
- getGameStats: { method: 'GET', path: '/games/:slug/stats', query: ['user'] },
16
- getGameProfile: { method: 'GET', path: '/games/:slug/profile', query: [] },
17
- setGameProfile: { method: 'PUT', path: '/games/:slug/profile', query: [] },
18
- listSaves: { method: 'GET', path: '/games/:slug/saves', query: [] },
19
- getSave: { method: 'GET', path: '/games/:slug/saves/:slot', query: [] },
20
- writeSave: { method: 'PUT', path: '/games/:slug/saves/:slot', query: [] },
21
- deleteSave: { method: 'DELETE', path: '/games/:slug/saves/:slot', query: [] },
22
- updateGameStat: { method: 'POST', path: '/games/:slug/stats', query: [] },
23
- resetMyGameProgress: { method: 'POST', path: '/games/:slug/reset', query: [] },
24
- listMyLeaderboardRecords: { method: 'GET', path: '/me/records', query: ['game'] },
25
- getMyOfflineSnapshot: { method: 'GET', path: '/me/snapshot', query: ['top'] },
26
- listTopOfEveryLeaderboard: { method: 'GET', path: '/leaderboards', query: ['top'] },
27
- listGameLeaderboards: { method: 'GET', path: '/games/:slug/leaderboards', query: [] },
28
- getLeaderboard: { method: 'GET', path: '/games/:slug/leaderboards/:key', query: ['limit'] },
29
- submitScore: { method: 'POST', path: '/games/:slug/leaderboards/:key/scores', query: [] },
30
- listDailyChallengesForAllGames: { method: 'GET', path: '/daily', query: [] },
31
- getGameDailyChallenges: { method: 'GET', path: '/games/:slug/daily', query: [] },
32
- startDailyChallenge: { method: 'POST', path: '/games/:slug/daily/:day/start', query: [] },
33
- completeDailyChallenge: { method: 'POST', path: '/games/:slug/daily/:day/complete', query: [] },
34
- getMyInventory: { method: 'GET', path: '/games/:slug/inventory', query: [] },
35
- grantInventoryItems: { method: 'POST', path: '/games/:slug/inventory/grant', query: [] },
36
- exchangeInventoryItems: { method: 'POST', path: '/games/:slug/inventory/exchange', query: [] },
37
- getPlayerInventory: { method: 'GET', path: '/games/:slug/players/:userId/inventory', query: [] },
38
- listTradePartners: { method: 'GET', path: '/games/:slug/trades/players', query: [] },
39
- listMyTrades: { method: 'GET', path: '/games/:slug/trades', query: ['box', 'status'] },
40
- declineTrade: { method: 'POST', path: '/games/:slug/trades/:id/decline', query: [] },
41
- cancelTrade: { method: 'POST', path: '/games/:slug/trades/:id/cancel', query: [] },
42
- createTrade: { method: 'POST', path: '/games/:slug/trades', query: [] },
43
- acceptTrade: { method: 'POST', path: '/games/:slug/trades/:id/accept', query: [] },
44
- counterTradeOffer: { method: 'POST', path: '/games/:slug/trades/:id/counter', query: [] },
45
- getSiteAnalytics: { method: 'GET', path: '/analytics', query: [] },
46
- reportClientError: { method: 'POST', path: '/client-log', query: [] },
47
- recordGameEvent: { method: 'POST', path: '/games/:slug/events', query: [] },
48
- getGameAnalytics: { method: 'GET', path: '/games/:slug/analytics', query: [] },
49
- getSocialConfig: { method: 'GET', path: '/social/config', query: [] },
50
- getOnlineCounts: { method: 'GET', path: '/social/counts', query: [] },
51
- getMySocialProfile: { method: 'GET', path: '/social/me', query: [] },
52
- listFriendsAndRequests: { method: 'GET', path: '/social/friends', query: [] },
53
- findPlayerByName: { method: 'GET', path: '/social/players/by-name/:name', query: [] },
54
- getPlayerCard: { method: 'GET', path: '/social/players/:id', query: [] },
55
- updateMySocialProfile: { method: 'PUT', path: '/social/me', query: [] },
56
- setParentChatLock: { method: 'POST', path: '/social/chat-lock', query: [] },
57
- sendFriendRequest: { method: 'POST', path: '/social/friends', query: [] },
58
- acceptFriendRequest: { method: 'POST', path: '/social/friends/:id/accept', query: [] },
59
- declineFriendRequest: { method: 'POST', path: '/social/friends/:id/decline', query: [] },
60
- removeFriend: { method: 'DELETE', path: '/social/friends/:id', query: [] },
61
- setFriendNotifications: { method: 'PUT', path: '/social/friends/:id/notify', query: [] },
62
- blockPlayer: { method: 'POST', path: '/social/blocks', query: [] },
63
- unblockPlayer: { method: 'DELETE', path: '/social/blocks/:id', query: [] },
64
- listRecentPlayers: { method: 'GET', path: '/social/recent', query: [] },
65
- listConversations: { method: 'GET', path: '/social/conversations', query: [] },
66
- listDirectMessages: { method: 'GET', path: '/social/messages/:userId', query: ['before'] },
67
- sendDirectMessage: { method: 'POST', path: '/social/messages/:userId', query: [] },
68
- markDirectMessagesRead: { method: 'POST', path: '/social/messages/:userId/read', query: [] },
69
- getNotificationPreferences: { method: 'GET', path: '/social/notify', query: [] },
70
- unsubscribeFromPush: { method: 'DELETE', path: '/social/push', query: [] },
71
- setNotificationPreferences: { method: 'PUT', path: '/social/notify', query: [] },
72
- subscribeToPush: { method: 'POST', path: '/social/push', query: [] },
73
- fileReport: { method: 'POST', path: '/social/reports', query: [] },
74
- listPublicSuggestions: { method: 'GET', path: '/suggestions', query: ['sort'] },
75
- submitSuggestion: { method: 'POST', path: '/suggestions', query: [] },
76
- voteForSuggestion: { method: 'POST', path: '/suggestions/:id/vote', query: [] },
77
- removeSuggestionVote: { method: 'DELETE', path: '/suggestions/:id/vote', query: [] },
78
- withdrawSuggestion: { method: 'DELETE', path: '/suggestions/:id', query: [] },
79
- sendGameInvite: { method: 'POST', path: '/mp/invites', query: [] },
80
- acceptGameInvite: { method: 'POST', path: '/mp/invites/:id/accept', query: [] },
81
- declineGameInvite: { method: 'POST', path: '/mp/invites/:id/decline', query: [] },
82
- cancelGameInvite: { method: 'DELETE', path: '/mp/invites/:id', query: [] },
83
- redeemLaunchToken: { method: 'GET', path: '/mp/launch/:token', query: [] },
84
- setPartyJoinInfo: { method: 'POST', path: '/mp/parties/:id/join-info', query: [] },
85
- leaveParty: { method: 'POST', path: '/mp/parties/:id/leave', query: [] },
86
- launchIntoFriendsRoom: { method: 'POST', path: '/mp/join', query: [] },
87
- getGameServerTicket: { method: 'POST', path: '/mp/ticket', query: [] },
88
- listMyRaceHistory: { method: 'GET', path: '/race/history', query: ['game', 'with', 'before', 'limit'] },
89
- getHubVersion: { method: 'GET', path: '/version', query: [] },
4
+ ensureGuestSession: { method: 'POST', path: '/auth/guest', query: [], offline: null },
5
+ signUpWithPassword: { method: 'POST', path: '/auth/signup', query: [], offline: null },
6
+ logInWithPassword: { method: 'POST', path: '/auth/login', query: [], offline: null },
7
+ logOut: { method: 'POST', path: '/auth/logout', query: [], offline: null },
8
+ getCurrentUser: { method: 'GET', path: '/me', query: [], offline: null },
9
+ claimGuestAccount: { method: 'POST', path: '/auth/claim', query: [], offline: null },
10
+ getGamePlayerCounts: { method: 'GET', path: '/catalog/players', query: [], offline: null },
11
+ getMyGameActivity: { method: 'GET', path: '/games/:slug/me/activity', query: [], offline: null },
12
+ listGames: { method: 'GET', path: '/games', query: [], offline: null },
13
+ listWhatsNewNotes: { method: 'GET', path: '/changes', query: ['game', 'since'], offline: null },
14
+ listFavoriteGames: { method: 'GET', path: '/favorites', query: [], offline: null },
15
+ setGameFavorite: { method: 'PUT', path: '/favorites/:slug', query: [], offline: 'replay' },
16
+ markWhatsNewSeen: { method: 'POST', path: '/changes/seen', query: [], offline: null },
17
+ getGameStats: { method: 'GET', path: '/games/:slug/stats', query: ['user'], offline: null },
18
+ getGameProfile: { method: 'GET', path: '/games/:slug/profile', query: [], offline: null },
19
+ setGameProfile: { method: 'PUT', path: '/games/:slug/profile', query: [], offline: 'replay' },
20
+ listSaves: { method: 'GET', path: '/games/:slug/saves', query: [], offline: null },
21
+ getSave: { method: 'GET', path: '/games/:slug/saves/:slot', query: [], offline: null },
22
+ writeSave: { method: 'PUT', path: '/games/:slug/saves/:slot', query: [], offline: 'replay' },
23
+ deleteSave: { method: 'DELETE', path: '/games/:slug/saves/:slot', query: [], offline: 'replay' },
24
+ updateGameStat: { method: 'POST', path: '/games/:slug/stats', query: [], offline: 'replay' },
25
+ resetMyGameProgress: { method: 'POST', path: '/games/:slug/reset', query: [], offline: null },
26
+ listMyLeaderboardRecords: { method: 'GET', path: '/me/records', query: ['game'], offline: null },
27
+ getMyOfflineSnapshot: { method: 'GET', path: '/me/snapshot', query: ['top'], offline: null },
28
+ listTopOfEveryLeaderboard: { method: 'GET', path: '/leaderboards', query: ['top'], offline: null },
29
+ listGameLeaderboards: { method: 'GET', path: '/games/:slug/leaderboards', query: [], offline: null },
30
+ getLeaderboard: { method: 'GET', path: '/games/:slug/leaderboards/:key', query: ['limit'], offline: null },
31
+ submitScore: { method: 'POST', path: '/games/:slug/leaderboards/:key/scores', query: [], offline: 'replay' },
32
+ listDailyChallengesForAllGames: { method: 'GET', path: '/daily', query: [], offline: null },
33
+ getGameDailyChallenges: { method: 'GET', path: '/games/:slug/daily', query: [], offline: null },
34
+ startDailyChallenge: { method: 'POST', path: '/games/:slug/daily/:day/start', query: [], offline: null },
35
+ completeDailyChallenge: { method: 'POST', path: '/games/:slug/daily/:day/complete', query: [], offline: 'replay' },
36
+ getMyInventory: { method: 'GET', path: '/games/:slug/inventory', query: [], offline: null },
37
+ grantInventoryItems: { method: 'POST', path: '/games/:slug/inventory/grant', query: [], offline: 'replay' },
38
+ exchangeInventoryItems: { method: 'POST', path: '/games/:slug/inventory/exchange', query: [], offline: 'replay' },
39
+ getPlayerInventory: { method: 'GET', path: '/games/:slug/players/:userId/inventory', query: [], offline: null },
40
+ listTradePartners: { method: 'GET', path: '/games/:slug/trades/players', query: [], offline: null },
41
+ listMyTrades: { method: 'GET', path: '/games/:slug/trades', query: ['box', 'status'], offline: null },
42
+ declineTrade: { method: 'POST', path: '/games/:slug/trades/:id/decline', query: [], offline: null },
43
+ cancelTrade: { method: 'POST', path: '/games/:slug/trades/:id/cancel', query: [], offline: null },
44
+ createTrade: { method: 'POST', path: '/games/:slug/trades', query: [], offline: null },
45
+ acceptTrade: { method: 'POST', path: '/games/:slug/trades/:id/accept', query: [], offline: null },
46
+ counterTradeOffer: { method: 'POST', path: '/games/:slug/trades/:id/counter', query: [], offline: null },
47
+ getSiteAnalytics: { method: 'GET', path: '/analytics', query: [], offline: null },
48
+ reportClientError: { method: 'POST', path: '/client-log', query: [], offline: null },
49
+ recordGameEvent: { method: 'POST', path: '/games/:slug/events', query: [], offline: null },
50
+ getGameAnalytics: { method: 'GET', path: '/games/:slug/analytics', query: [], offline: null },
51
+ getSocialConfig: { method: 'GET', path: '/social/config', query: [], offline: null },
52
+ getOnlineCounts: { method: 'GET', path: '/social/counts', query: [], offline: null },
53
+ getMySocialProfile: { method: 'GET', path: '/social/me', query: [], offline: null },
54
+ listFriendsAndRequests: { method: 'GET', path: '/social/friends', query: [], offline: null },
55
+ findPlayerByName: { method: 'GET', path: '/social/players/by-name/:name', query: [], offline: null },
56
+ getPlayerCard: { method: 'GET', path: '/social/players/:id', query: [], offline: null },
57
+ updateMySocialProfile: { method: 'PUT', path: '/social/me', query: [], offline: null },
58
+ setParentChatLock: { method: 'POST', path: '/social/chat-lock', query: [], offline: null },
59
+ sendFriendRequest: { method: 'POST', path: '/social/friends', query: [], offline: null },
60
+ acceptFriendRequest: { method: 'POST', path: '/social/friends/:id/accept', query: [], offline: null },
61
+ declineFriendRequest: { method: 'POST', path: '/social/friends/:id/decline', query: [], offline: null },
62
+ removeFriend: { method: 'DELETE', path: '/social/friends/:id', query: [], offline: null },
63
+ setFriendNotifications: { method: 'PUT', path: '/social/friends/:id/notify', query: [], offline: null },
64
+ blockPlayer: { method: 'POST', path: '/social/blocks', query: [], offline: null },
65
+ unblockPlayer: { method: 'DELETE', path: '/social/blocks/:id', query: [], offline: null },
66
+ listRecentPlayers: { method: 'GET', path: '/social/recent', query: [], offline: null },
67
+ listConversations: { method: 'GET', path: '/social/conversations', query: [], offline: null },
68
+ listDirectMessages: { method: 'GET', path: '/social/messages/:userId', query: ['before'], offline: null },
69
+ sendDirectMessage: { method: 'POST', path: '/social/messages/:userId', query: [], offline: null },
70
+ markDirectMessagesRead: { method: 'POST', path: '/social/messages/:userId/read', query: [], offline: null },
71
+ getNotificationPreferences: { method: 'GET', path: '/social/notify', query: [], offline: null },
72
+ unsubscribeFromPush: { method: 'DELETE', path: '/social/push', query: [], offline: null },
73
+ setNotificationPreferences: { method: 'PUT', path: '/social/notify', query: [], offline: null },
74
+ subscribeToPush: { method: 'POST', path: '/social/push', query: [], offline: null },
75
+ fileReport: { method: 'POST', path: '/social/reports', query: [], offline: null },
76
+ listPublicSuggestions: { method: 'GET', path: '/suggestions', query: ['sort'], offline: null },
77
+ submitSuggestion: { method: 'POST', path: '/suggestions', query: [], offline: null },
78
+ voteForSuggestion: { method: 'POST', path: '/suggestions/:id/vote', query: [], offline: null },
79
+ removeSuggestionVote: { method: 'DELETE', path: '/suggestions/:id/vote', query: [], offline: null },
80
+ withdrawSuggestion: { method: 'DELETE', path: '/suggestions/:id', query: [], offline: null },
81
+ sendGameInvite: { method: 'POST', path: '/mp/invites', query: [], offline: null },
82
+ acceptGameInvite: { method: 'POST', path: '/mp/invites/:id/accept', query: [], offline: null },
83
+ declineGameInvite: { method: 'POST', path: '/mp/invites/:id/decline', query: [], offline: null },
84
+ createInviteLink: { method: 'POST', path: '/mp/links', query: [], offline: null },
85
+ listInviteLinks: { method: 'GET', path: '/mp/links', query: ['game'], offline: null },
86
+ revokeInviteLink: { method: 'DELETE', path: '/mp/links/:token', query: [], offline: null },
87
+ cancelGameInvite: { method: 'DELETE', path: '/mp/invites/:id', query: [], offline: null },
88
+ redeemLaunchToken: { method: 'GET', path: '/mp/launch/:token', query: [], offline: null },
89
+ openInviteLink: { method: 'POST', path: '/mp/links/:token/open', query: [], offline: null },
90
+ setPartyJoinInfo: { method: 'POST', path: '/mp/parties/:id/join-info', query: [], offline: null },
91
+ leaveParty: { method: 'POST', path: '/mp/parties/:id/leave', query: [], offline: null },
92
+ launchIntoFriendsRoom: { method: 'POST', path: '/mp/join', query: [], offline: null },
93
+ getGameServerTicket: { method: 'POST', path: '/mp/ticket', query: [], offline: null },
94
+ listMyRaceHistory: { method: 'GET', path: '/race/history', query: ['game', 'with', 'before', 'limit'], offline: null },
95
+ listTournaments: { method: 'GET', path: '/tournaments', query: ['game'], offline: null },
96
+ getTournament: { method: 'GET', path: '/tournaments/:id', query: ['key'], offline: null },
97
+ createTournament: { method: 'POST', path: '/tournaments', query: [], offline: null },
98
+ inviteToTournament: { method: 'POST', path: '/tournaments/:id/invites', query: [], offline: null },
99
+ followTournament: { method: 'PUT', path: '/tournaments/:id/follow', query: [], offline: null },
100
+ unfollowTournament: { method: 'DELETE', path: '/tournaments/:id/follow', query: [], offline: null },
101
+ startTournament: { method: 'POST', path: '/tournaments/:id/start', query: [], offline: null },
102
+ cancelTournament: { method: 'DELETE', path: '/tournaments/:id', query: [], offline: null },
103
+ joinTournament: { method: 'POST', path: '/tournaments/:id/join', query: [], offline: null },
104
+ leaveTournament: { method: 'POST', path: '/tournaments/:id/leave', query: [], offline: null },
105
+ launchTournamentMatch: { method: 'POST', path: '/tournaments/:id/matches/:match/launch', query: [], offline: null },
106
+ getPlayOnline: { method: 'GET', path: '/play-online', query: [], offline: null },
107
+ getHubVersion: { method: 'GET', path: '/version', query: [], offline: null },
90
108
  } as const;
109
+
110
+ /** The request header that carries an offline operation key (the contract's `OFFLINE_OP_HEADER`). */
111
+ export const OFFLINE_OP_HEADER = 'x-hub-op';