@lootlog/sdk 0.1.1 → 0.3.0

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 (37) hide show
  1. package/dist/activity.d.ts +8 -2
  2. package/dist/activity.d.ts.map +1 -1
  3. package/dist/activity.js +2 -2
  4. package/dist/activity.js.map +1 -1
  5. package/dist/battlelog.d.ts +280 -5
  6. package/dist/battlelog.d.ts.map +1 -1
  7. package/dist/battlelog.js +11 -6
  8. package/dist/battlelog.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/main.d.ts +1457 -918
  12. package/dist/main.d.ts.map +1 -1
  13. package/dist/main.js +400 -284
  14. package/dist/main.js.map +1 -1
  15. package/dist/{mutators-Da_vMUva.d.ts → mutators-BaZDDfwh.d.ts} +2 -2
  16. package/dist/{mutators-Da_vMUva.d.ts.map → mutators-BaZDDfwh.d.ts.map} +1 -1
  17. package/dist/{mutators-CaBTxpi-.js → mutators-Dj5qBH3y.js} +2 -2
  18. package/dist/{mutators-CaBTxpi-.js.map → mutators-Dj5qBH3y.js.map} +1 -1
  19. package/dist/realtime.d.ts +430 -21
  20. package/dist/realtime.d.ts.map +1 -1
  21. package/dist/realtime.js +799 -587
  22. package/dist/realtime.js.map +1 -1
  23. package/dist/search.d.ts +247 -5
  24. package/dist/search.d.ts.map +1 -1
  25. package/dist/search.js +115 -2
  26. package/dist/search.js.map +1 -1
  27. package/dist/{transport-CIfewyM7.js → transport-Cn1V6y-o.js} +46 -8
  28. package/dist/transport-Cn1V6y-o.js.map +1 -0
  29. package/dist/{transport-BHbtsYo_.d.ts → transport-CoTymLxF.d.ts} +3 -1
  30. package/dist/transport-CoTymLxF.d.ts.map +1 -0
  31. package/openapi/activity.json +15 -1
  32. package/openapi/battlelog.json +807 -40
  33. package/openapi/main.json +5752 -4060
  34. package/openapi/search.json +721 -53
  35. package/package.json +5 -6
  36. package/dist/transport-BHbtsYo_.d.ts.map +0 -1
  37. package/dist/transport-CIfewyM7.js.map +0 -1
package/dist/realtime.js CHANGED
@@ -1,7 +1,7 @@
1
- import { Result, Schema, SchemaTransformation } from "effect";
1
+ import { Predicate, Result, Schema, SchemaTransformation } from "effect";
2
2
  import * as Schema$1 from "effect/Schema";
3
3
  import { TaggedError } from "effect/Schema";
4
- import { decode, encode } from "@msgpack/msgpack";
4
+ import * as SchemaTransformation$1 from "effect/SchemaTransformation";
5
5
  //#region ../schema/src/npc-type.ts
6
6
  const NpcTypeEnum = {
7
7
  COMMON: "COMMON",
@@ -12,6 +12,7 @@ const NpcTypeEnum = {
12
12
  EVENT_HERO: "EVENT_HERO",
13
13
  COLOSSUS: "COLOSSUS",
14
14
  TITAN: "TITAN",
15
+ HERO_TRACKER: "HERO_TRACKER",
15
16
  NPC: "NPC"
16
17
  };
17
18
  const NpcTypeSchema = Schema.Literals([
@@ -23,6 +24,7 @@ const NpcTypeSchema = Schema.Literals([
23
24
  NpcTypeEnum.EVENT_HERO,
24
25
  NpcTypeEnum.COLOSSUS,
25
26
  NpcTypeEnum.TITAN,
27
+ NpcTypeEnum.HERO_TRACKER,
26
28
  NpcTypeEnum.NPC
27
29
  ]);
28
30
  //#endregion
@@ -139,7 +141,7 @@ const NPC_PERMISSIONS = {
139
141
  Object.values(NPC_PERMISSIONS).flat();
140
142
  new Set(NPC_PERMISSIONS.loots);
141
143
  Object.values(Permission).sort();
142
- new Set(Object.values(NpcTypeEnum));
144
+ Schema.is(NpcTypeSchema);
143
145
  //#endregion
144
146
  //#region ../schema/src/item-rarity.ts
145
147
  const ItemRarityEnum = {
@@ -161,21 +163,11 @@ const LootSourceEnum = {
161
163
  DIALOG: "DIALOG",
162
164
  FIGHT: "FIGHT"
163
165
  };
164
- Schema.Literals([
166
+ const LootSourceSchema = Schema.Literals([
165
167
  LootSourceEnum.LOOTBOX,
166
168
  LootSourceEnum.DIALOG,
167
169
  LootSourceEnum.FIGHT
168
170
  ]);
169
- const LootShareSourceEnum = {
170
- NONE: "NONE",
171
- ITEM_OWNER: "ITEM_OWNER",
172
- CHAT_MESSAGE: "CHAT_MESSAGE"
173
- };
174
- Schema.Literals([
175
- LootShareSourceEnum.NONE,
176
- LootShareSourceEnum.ITEM_OWNER,
177
- LootShareSourceEnum.CHAT_MESSAGE
178
- ]);
179
171
  const ProfessionEnum = {
180
172
  WARRIOR: "WARRIOR",
181
173
  PALADIN: "PALADIN",
@@ -192,89 +184,56 @@ const ProfessionSchema = Schema.Literals([
192
184
  ProfessionEnum.BLADE_DANCER,
193
185
  ProfessionEnum.TRACKER
194
186
  ]);
195
- const ItemTypeEnum = {
196
- ONE_HAND_WEAPON: "ONE_HAND_WEAPON",
197
- TWO_HAND_WEAPON: "TWO_HAND_WEAPON",
198
- ONE_AND_HALF_HAND_WEAPON: "ONE_AND_HALF_HAND_WEAPON",
199
- DISTANCE_WEAPON: "DISTANCE_WEAPON",
200
- HELP_WEAPON: "HELP_WEAPON",
201
- WAND_WEAPON: "WAND_WEAPON",
202
- ORB_WEAPON: "ORB_WEAPON",
203
- ARMOR: "ARMOR",
204
- HELMET: "HELMET",
205
- BOOTS: "BOOTS",
206
- GLOVES: "GLOVES",
207
- RING: "RING",
208
- NECKLACE: "NECKLACE",
209
- SHIELD: "SHIELD",
210
- NEUTRAL: "NEUTRAL",
211
- CONSUME: "CONSUME",
212
- GOLD: "GOLD",
213
- KEYS: "KEYS",
214
- QUEST: "QUEST",
215
- RENEWABLE: "RENEWABLE",
216
- ARROWS: "ARROWS",
217
- TALISMAN: "TALISMAN",
218
- BOOK: "BOOK",
219
- BAG: "BAG",
220
- BLESS: "BLESS",
221
- UPGRADE: "UPGRADE",
222
- RECIPE: "RECIPE",
223
- COINAGE: "COINAGE",
224
- QUIVER: "QUIVER",
225
- OUTFITS: "OUTFITS",
226
- PETS: "PETS",
227
- TELEPORTS: "TELEPORTS"
228
- };
229
- Schema.Literals([
230
- ItemTypeEnum.ONE_HAND_WEAPON,
231
- ItemTypeEnum.TWO_HAND_WEAPON,
232
- ItemTypeEnum.ONE_AND_HALF_HAND_WEAPON,
233
- ItemTypeEnum.DISTANCE_WEAPON,
234
- ItemTypeEnum.HELP_WEAPON,
235
- ItemTypeEnum.WAND_WEAPON,
236
- ItemTypeEnum.ORB_WEAPON,
237
- ItemTypeEnum.ARMOR,
238
- ItemTypeEnum.HELMET,
239
- ItemTypeEnum.BOOTS,
240
- ItemTypeEnum.GLOVES,
241
- ItemTypeEnum.RING,
242
- ItemTypeEnum.NECKLACE,
243
- ItemTypeEnum.SHIELD,
244
- ItemTypeEnum.NEUTRAL,
245
- ItemTypeEnum.CONSUME,
246
- ItemTypeEnum.GOLD,
247
- ItemTypeEnum.KEYS,
248
- ItemTypeEnum.QUEST,
249
- ItemTypeEnum.RENEWABLE,
250
- ItemTypeEnum.ARROWS,
251
- ItemTypeEnum.TALISMAN,
252
- ItemTypeEnum.BOOK,
253
- ItemTypeEnum.BAG,
254
- ItemTypeEnum.BLESS,
255
- ItemTypeEnum.UPGRADE,
256
- ItemTypeEnum.RECIPE,
257
- ItemTypeEnum.COINAGE,
258
- ItemTypeEnum.QUIVER,
259
- ItemTypeEnum.OUTFITS,
260
- ItemTypeEnum.PETS,
261
- ItemTypeEnum.TELEPORTS
262
- ]);
263
187
  //#endregion
264
188
  //#region ../schema/src/http-scalars.ts
265
189
  const FiniteNumber = Schema$1.Number.check(Schema$1.isFinite().annotate({ expected: "a finite number" }));
266
- const dateTimeString = (pattern) => Schema$1.String.annotate({ format: "date-time" }).check(Schema$1.isPattern(new RegExp(pattern)).annotate({ expected: `a string matching the RegExp ${pattern}` }));
267
- dateTimeString("^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$");
268
- const DateTimeWithOffsetString = dateTimeString("^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$");
269
- Schema$1.String.check(Schema$1.isMinLength(1).annotate({ expected: "a value with a length of at least 1" }));
190
+ const dateTimeString = (pattern) => Schema$1.String.annotate({ format: "date-time" }).check(Schema$1.isPattern(new RegExp(pattern, "u")).annotate({ expected: `a string matching the RegExp ${pattern}` }));
191
+ const dateTimePattern = (timeZonePattern) => `^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:${timeZonePattern}))$`;
192
+ /** Strict ISO-8601 UTC date-time pattern shared by HTTP and domain schemas. */
193
+ const DATE_TIME_STRING_PATTERN = dateTimePattern("Z");
194
+ const DateTimeString = dateTimeString(DATE_TIME_STRING_PATTERN);
195
+ dateTimeString(dateTimePattern("Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)"));
196
+ const NonEmptyString = Schema$1.String.check(Schema$1.isMinLength(1).annotate({ expected: "a value with a length of at least 1" }));
270
197
  Schema$1.Json.annotate({ expected: "JSON value" });
271
- Schema$1.Number.check(Schema$1.isInt().annotate({ expected: "an integer" })).check(Schema$1.isGreaterThanOrEqualTo(-9007199254740991).annotate({ expected: "a value greater than or equal to -9007199254740991" })).check(Schema$1.isLessThanOrEqualTo(9007199254740991).annotate({ expected: "a value less than or equal to 9007199254740991" }));
272
- const PositiveSafeInteger = Schema$1.Number.check(Schema$1.isInt().annotate({ expected: "an integer" })).check(Schema$1.isGreaterThanOrEqualTo(1).annotate({ expected: "a value greater than or equal to 1" })).check(Schema$1.isLessThanOrEqualTo(9007199254740991).annotate({ expected: "a value less than or equal to 9007199254740991" }));
273
- Schema$1.Number.check(Schema$1.isInt().annotate({ expected: "an integer" })).check(Schema$1.isGreaterThanOrEqualTo(0).annotate({ expected: "a value greater than or equal to 0" })).check(Schema$1.isLessThanOrEqualTo(9007199254740991).annotate({ expected: "a value less than or equal to 9007199254740991" }));
274
- Schema$1.Number.check(Schema$1.isInt().annotate({ expected: "an integer" })).check(Schema$1.isGreaterThanOrEqualTo(0).annotate({ expected: "a value greater than or equal to 0" })).check(Schema$1.isLessThanOrEqualTo(500).annotate({ expected: "a value less than or equal to 500" }));
275
- Schema$1.Number.check(Schema$1.isInt().annotate({ expected: "an integer" })).check(Schema$1.isGreaterThanOrEqualTo(1).annotate({ expected: "a value greater than or equal to 1" })).check(Schema$1.isLessThanOrEqualTo(100).annotate({ expected: "a value less than or equal to 100" }));
198
+ const boundedInteger = (minimum, maximum) => Schema$1.Number.check(Schema$1.isInt().annotate({ expected: "an integer" })).check(Schema$1.isGreaterThanOrEqualTo(minimum).annotate({ expected: `a value greater than or equal to ${minimum}` })).check(Schema$1.isLessThanOrEqualTo(maximum).annotate({ expected: `a value less than or equal to ${maximum}` }));
199
+ boundedInteger(-9007199254740991, 9007199254740991);
200
+ const PositiveSafeInteger = boundedInteger(1, 9007199254740991);
201
+ const NonNegativeSafeInteger = boundedInteger(0, 9007199254740991);
202
+ const LevelFilter = boundedInteger(0, 500);
203
+ boundedInteger(1, 100);
204
+ /** Rejects a level filter whose lower bound exceeds its upper bound. */
205
+ const levelRangeCheck = (minKey, maxKey) => Schema$1.makeFilter((data) => {
206
+ const min = data[minKey];
207
+ const max = data[maxKey];
208
+ return min === void 0 || max === void 0 || min <= max ? void 0 : {
209
+ path: [minKey],
210
+ issue: `${minKey} must be <= ${maxKey}`
211
+ };
212
+ });
213
+ Schema$1.optionalKey(LevelFilter), Schema$1.optionalKey(LevelFilter), levelRangeCheck("npcLevelMin", "npcLevelMax");
214
+ Schema$1.Array(NpcTypeSchema);
215
+ Schema$1.Literals([
216
+ "24h",
217
+ "3d",
218
+ "7d",
219
+ "14d",
220
+ "30d",
221
+ "90d",
222
+ "180d",
223
+ "all"
224
+ ]).annotate({ default: "7d" });
225
+ Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(64)).pipe(Schema$1.decode(SchemaTransformation$1.trim()), Schema$1.decode(SchemaTransformation$1.toLowerCase())).check(Schema$1.isMinLength(1).annotate({ expected: "a non-empty world" }));
226
+ Schema$1.String.check(Schema$1.isMaxLength(100)).pipe(Schema$1.decode(SchemaTransformation$1.trim()));
227
+ Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(512));
276
228
  //#endregion
277
229
  //#region ../protocol/src/loot-summary.ts
230
+ const MapPlayersSnapshot = Schema.Array(Schema.Struct({
231
+ accountId: PositiveSafeInteger,
232
+ characterId: PositiveSafeInteger,
233
+ name: NonEmptyString,
234
+ prof: Schema.NullOr(ProfessionSchema),
235
+ icon: Schema.NullOr(Schema.String)
236
+ })).check(Schema.isMinLength(1));
278
237
  const LootItemResponse = Schema.Struct({
279
238
  id: FiniteNumber,
280
239
  hid: Schema.String,
@@ -314,6 +273,37 @@ const LootSummary = Schema.Struct({
314
273
  lootShare: LootShareResponse,
315
274
  location: Schema.String
316
275
  });
276
+ /** Same shape as the HTTP `MemberSummary`. */
277
+ const LootMember = Schema.Struct({
278
+ id: Schema.Number,
279
+ name: Schema.String,
280
+ avatar: Schema.NullOr(Schema.String),
281
+ userId: Schema.String
282
+ });
283
+ const LootSubmission = Schema.Struct({
284
+ guildId: Schema.String,
285
+ memberId: Schema.Number,
286
+ lootId: Schema.Number,
287
+ member: LootMember
288
+ });
289
+ /** One Organization's view of a loot, as its HTTP list and detail responses encode it. */
290
+ const LootSnapshot = Schema.Struct({
291
+ id: Schema.Number,
292
+ uniqueId: Schema.String,
293
+ world: Schema.String,
294
+ source: LootSourceSchema,
295
+ location: Schema.String,
296
+ items: Schema.Array(LootItemResponse),
297
+ players: Schema.Array(LootPlayerResponse),
298
+ mapPlayersSnapshot: Schema.NullOr(MapPlayersSnapshot),
299
+ npcs: Schema.Array(LootNpcResponse),
300
+ lootShare: LootShareResponse,
301
+ createdAt: Schema.String,
302
+ updatedAt: Schema.String,
303
+ submissions: Schema.optionalKey(Schema.Array(LootSubmission)),
304
+ commentsCount: Schema.Number,
305
+ likes: Schema.optionalKey(Schema.Array(LootMember))
306
+ });
317
307
  //#endregion
318
308
  //#region ../protocol/src/feed.ts
319
309
  const FeedNpc = Schema.Struct({
@@ -360,9 +350,204 @@ const UserFeedItem = Schema.Union([Schema.Struct({
360
350
  })]);
361
351
  //#endregion
362
352
  //#region ../schema/src/primitives.ts
363
- const NonEmptyString = Schema.NonEmptyString;
364
353
  const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
365
- Schema.String.annotate({ format: "date-time" }).pipe(Schema.check(Schema.isPattern(/^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$/)), Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromString));
354
+ const isoDateTimePattern = new RegExp(DATE_TIME_STRING_PATTERN, "u");
355
+ Schema.String.annotate({ format: "date-time" }).pipe(Schema.check(Schema.isPattern(isoDateTimePattern)), Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromString));
356
+ //#endregion
357
+ //#region ../schema/src/party-ready-room.ts
358
+ const PARTY_READY_ROOM_STATUSES = ["ACTIVE", "CANCELLED"];
359
+ const PARTY_READY_ROOM_PARTY_PRESENCE_STATES = ["OUTSIDE", "IN_PARTY"];
360
+ const PartyReadyRoomClanSchema = Schema.Struct({
361
+ id: Schema.optionalKey(Schema.Number),
362
+ name: Schema.optionalKey(Schema.String)
363
+ });
364
+ const PartyReadyRoomCharacterSchema = Schema.Struct({
365
+ accountId: Schema.String,
366
+ characterId: Schema.String,
367
+ icon: Schema.String,
368
+ lvl: Schema.Number,
369
+ nick: Schema.String,
370
+ prof: Schema.String,
371
+ clan: Schema.optionalKey(PartyReadyRoomClanSchema)
372
+ });
373
+ const PartyReadyRoomParticipantSchema = Schema.Struct({
374
+ participantId: Schema.String,
375
+ discordId: Schema.String,
376
+ character: PartyReadyRoomCharacterSchema,
377
+ partyPresence: Schema.Literals(PARTY_READY_ROOM_PARTY_PRESENCE_STATES),
378
+ createdAt: Schema.String,
379
+ updatedAt: Schema.String
380
+ });
381
+ const PartyGatheringNpcSchema = Schema.Struct({
382
+ prof: Schema.optionalKey(Schema.String),
383
+ icon: Schema.optionalKey(Schema.String),
384
+ name: Schema.String,
385
+ location: Schema.String,
386
+ lvl: Schema.Number,
387
+ type: Schema.String,
388
+ x: Schema.optionalKey(Schema.Number),
389
+ y: Schema.optionalKey(Schema.Number)
390
+ });
391
+ const PartyGatheringCharacterSchema = Schema.Struct({
392
+ characterId: NonEmptyString.check(Schema.isMaxLength(255)),
393
+ nick: Schema.String.check(Schema.isMaxLength(255)),
394
+ icon: Schema.String.check(Schema.isMaxLength(2048)),
395
+ lvl: FiniteNumber,
396
+ prof: Schema.String.check(Schema.isMaxLength(100))
397
+ });
398
+ const PartyGatheringPartyMemberSchema = Schema.Struct({
399
+ characterId: PartyGatheringCharacterSchema.fields.characterId,
400
+ nick: Schema.optionalKey(PartyGatheringCharacterSchema.fields.nick),
401
+ icon: Schema.optionalKey(PartyGatheringCharacterSchema.fields.icon),
402
+ lvl: Schema.optionalKey(PartyGatheringCharacterSchema.fields.lvl),
403
+ prof: Schema.optionalKey(PartyGatheringCharacterSchema.fields.prof)
404
+ });
405
+ const PartyGatheringPartyStateSchema = Schema.Union([Schema.Struct({ status: Schema.Literal("UNKNOWN") }), Schema.Struct({
406
+ status: Schema.Literal("OBSERVED"),
407
+ observedAt: DateTimeString,
408
+ members: Schema.Array(PartyGatheringPartyMemberSchema).check(Schema.isMaxLength(20))
409
+ })]);
410
+ const PartyGatheringVolunteerSchema = Schema.Struct({
411
+ ...PartyGatheringCharacterSchema.fields,
412
+ partyPresence: Schema.Literals(PARTY_READY_ROOM_PARTY_PRESENCE_STATES)
413
+ });
414
+ const PartyGatheringSummarySchema = Schema.Struct({
415
+ notificationId: Schema.String,
416
+ organizerName: Schema.String,
417
+ organizerDiscordId: Schema.optionalKey(Schema.String),
418
+ organizerLvl: Schema.optionalKey(FiniteNumber),
419
+ organizerProf: Schema.optionalKey(Schema.String),
420
+ applicantCount: NonNegativeSafeInteger,
421
+ inPartyCount: NonNegativeSafeInteger,
422
+ partyMemberCount: Schema.optionalKey(NonNegativeSafeInteger),
423
+ revision: Schema.optionalKey(PositiveSafeInteger),
424
+ volunteers: Schema.optionalKey(Schema.Array(PartyGatheringVolunteerSchema)),
425
+ partyState: Schema.optionalKey(PartyGatheringPartyStateSchema),
426
+ guildIds: Schema.Array(Schema.String),
427
+ world: Schema.String,
428
+ description: Schema.optionalKey(Schema.String),
429
+ minLvl: Schema.optionalKey(FiniteNumber),
430
+ maxLvl: Schema.optionalKey(FiniteNumber),
431
+ npc: Schema.optionalKey(Schema.Struct({
432
+ ...PartyGatheringNpcSchema.fields,
433
+ type: Schema.optionalKey(Schema.String)
434
+ })),
435
+ createdAt: DateTimeString,
436
+ expiresAt: DateTimeString
437
+ });
438
+ const PartyGatheringClientUpdateSchema = Schema.Union([Schema.Struct({
439
+ type: Schema.Literal("UPSERT"),
440
+ gathering: PartyGatheringSummarySchema
441
+ }), Schema.Struct({
442
+ type: Schema.Literal("REMOVE"),
443
+ notificationId: Schema.String,
444
+ revision: PositiveSafeInteger
445
+ })]);
446
+ Schema.Struct({
447
+ guildId: Schema.String,
448
+ guildIds: Schema.Array(Schema.String),
449
+ organizerDiscordId: Schema.String,
450
+ npc: Schema.optionalKey(PartyGatheringNpcSchema),
451
+ world: Schema.String,
452
+ notificationId: Schema.String,
453
+ revision: PositiveSafeInteger,
454
+ update: PartyGatheringClientUpdateSchema
455
+ });
456
+ const PartyReadyRoomAggregateSchema = Schema.Struct({
457
+ schemaVersion: Schema.Literal(3),
458
+ npc: Schema.optionalKey(PartyGatheringNpcSchema),
459
+ notificationId: Schema.String,
460
+ organizerDiscordId: Schema.String,
461
+ organizerCharacter: PartyReadyRoomCharacterSchema,
462
+ guildIds: Schema.mutable(Schema.Array(Schema.String)),
463
+ world: Schema.String,
464
+ description: Schema.optionalKey(Schema.String),
465
+ minLvl: Schema.optionalKey(Schema.Number),
466
+ maxLvl: Schema.optionalKey(Schema.Number),
467
+ partyMemberCount: Schema.optionalKey(NonNegativeSafeInteger),
468
+ partyState: Schema.optionalKey(PartyGatheringPartyStateSchema),
469
+ status: Schema.Literals(PARTY_READY_ROOM_STATUSES),
470
+ revision: Schema.Number,
471
+ createdAt: Schema.String,
472
+ updatedAt: Schema.String,
473
+ expiresAt: Schema.String,
474
+ participants: Schema.Record(Schema.String, PartyReadyRoomParticipantSchema)
475
+ });
476
+ const activeProjectionFields = {
477
+ volunteers: Schema.optionalKey(Schema.Array(PartyGatheringVolunteerSchema)),
478
+ ...PartyReadyRoomAggregateSchema.fields,
479
+ participants: Schema.Record(Schema.String, Schema.mutableKey(PartyReadyRoomParticipantSchema)),
480
+ status: Schema.Literal("ACTIVE")
481
+ };
482
+ const PartyReadyRoomOrganizerProjectionSchema = Schema.Struct({
483
+ ...activeProjectionFields,
484
+ viewer: Schema.Literal("ORGANIZER"),
485
+ ownedParticipantIds: Schema.mutable(Schema.Array(Schema.String))
486
+ });
487
+ const PartyReadyRoomParticipantProjectionSchema = Schema.Struct({
488
+ ...activeProjectionFields,
489
+ viewer: Schema.Literal("PARTICIPANT")
490
+ });
491
+ const PartyReadyRoomProjectionSchema = Schema.Union([PartyReadyRoomOrganizerProjectionSchema, PartyReadyRoomParticipantProjectionSchema]);
492
+ Schema.decodeUnknownSync(PartyReadyRoomProjectionSchema);
493
+ const PartyReadyRoomUpsertUpdateSchema = Schema.Struct({
494
+ schemaVersion: Schema.Literal(3),
495
+ type: Schema.Literal("UPSERT"),
496
+ projection: PartyReadyRoomProjectionSchema
497
+ });
498
+ const PartyReadyRoomRemoveUpdateSchema = Schema.Struct({
499
+ schemaVersion: Schema.Literal(3),
500
+ type: Schema.Literal("REMOVE"),
501
+ notificationId: Schema.String,
502
+ revision: Schema.Number
503
+ });
504
+ const PartyReadyRoomClientUpdateSchema = Schema.Union([PartyReadyRoomUpsertUpdateSchema, PartyReadyRoomRemoveUpdateSchema]);
505
+ Schema.decodeUnknownSync(PartyReadyRoomClientUpdateSchema);
506
+ const GlobalChatMessageText = Schema.String.check(Schema.isTrimmed(), Schema.isMinLength(1), Schema.isMaxLength(128));
507
+ /**
508
+ * A Margonem world with its own global chat channel. Wherever a channel is
509
+ * named, an absent world means the channel every world shares.
510
+ */
511
+ const GlobalChatWorld = Schema.NonEmptyString.check(Schema.isMaxLength(64));
512
+ /**
513
+ * A global chat message as every Member sees it. It crosses Organization
514
+ * boundaries, so it names the sender only by their Discord display name and
515
+ * never carries a User, Discord, Organization or character identifier.
516
+ *
517
+ * `isAdmin` and the worlds are optional only for messages published before
518
+ * channels existed; the API sets `isAdmin` on every message it stores.
519
+ */
520
+ const GlobalChatMessageSchema = Schema.Struct({
521
+ id: Schema.NonEmptyString,
522
+ displayName: Schema.NonEmptyString,
523
+ message: GlobalChatMessageText,
524
+ timestamp: DateTimeString,
525
+ /** The channel's world; absent in the channel every world shares. */
526
+ world: Schema.optionalKey(GlobalChatWorld),
527
+ /** In the shared channel only: the world the sender wrote from. */
528
+ originWorld: Schema.optionalKey(GlobalChatWorld),
529
+ isAdmin: Schema.optionalKey(Schema.Boolean)
530
+ });
531
+ Schema.Union([Schema.Struct({
532
+ type: Schema.Literal("deleted"),
533
+ world: Schema.optionalKey(GlobalChatWorld),
534
+ id: Schema.NonEmptyString
535
+ }), Schema.Struct({
536
+ type: Schema.Literal("pinned"),
537
+ world: Schema.optionalKey(GlobalChatWorld),
538
+ message: Schema.NullOr(GlobalChatMessageSchema)
539
+ })]);
540
+ /** Live counts the gateway sends to a channel's subscribers. */
541
+ const GlobalChatStatsSchema = Schema.Struct({
542
+ world: Schema.optionalKey(GlobalChatWorld),
543
+ /**
544
+ * Unique Users with a live Game client across every Organization: on the
545
+ * channel's world, or on any world in the shared channel.
546
+ */
547
+ online: NonNegativeInt,
548
+ /** Unique Users following this channel. */
549
+ listeners: NonNegativeInt
550
+ });
366
551
  //#endregion
367
552
  //#region ../schema/src/air-tag.ts
368
553
  const AIR_TAG_RELATIONS = [
@@ -386,6 +571,11 @@ const Coordinate = Schema.Int.check(Schema.isBetween({
386
571
  minimum: 0,
387
572
  maximum: AIR_TAG_MAX_COORDINATE
388
573
  }));
574
+ const Level = Schema.Int.check(Schema.isBetween({
575
+ minimum: 0,
576
+ maximum: 1e4
577
+ }));
578
+ const Profession = Schema.NonEmptyString.check(Schema.isMaxLength(1));
389
579
  const AirTagRelationSchema = Schema.Literals(AIR_TAG_RELATIONS);
390
580
  const AirTagClanSchema = Schema.Struct({
391
581
  id: SafeNatural,
@@ -397,24 +587,22 @@ const AirTagObservationSchema = Schema.Struct({
397
587
  clan: Schema.optionalKey(AirTagClanSchema),
398
588
  relation: AirTagRelationSchema,
399
589
  x: Coordinate,
400
- y: Coordinate
590
+ y: Coordinate,
591
+ lvl: Schema.optionalKey(Level),
592
+ prof: Schema.optionalKey(Profession),
593
+ stasis: Schema.optionalKey(Schema.Boolean)
594
+ });
595
+ const AirTagDepartureSchema = Schema.Struct({
596
+ targetId: ShortString,
597
+ reason: Schema.Literals(["left-map", "out-of-sight"])
401
598
  });
402
599
  const AirTagObservationBatchSchema = Schema.Struct({
403
600
  expectedMapId: Coordinate,
404
- observations: Schema.Array(AirTagObservationSchema)
405
- });
406
- Schema.Struct({
407
- requestId: ShortString,
408
- enabled: Schema.Boolean,
409
- expectedMapId: Schema.optionalKey(Coordinate)
601
+ observations: Schema.Array(AirTagObservationSchema),
602
+ departures: Schema.optionalKey(Schema.Array(AirTagDepartureSchema))
410
603
  });
411
604
  const AirTagTargetSchema = Schema.Struct({
412
- targetId: ShortString,
413
- nickname: ShortString,
414
- clan: Schema.optionalKey(AirTagClanSchema),
415
- relation: AirTagRelationSchema,
416
- x: Coordinate,
417
- y: Coordinate,
605
+ ...AirTagObservationSchema.fields,
418
606
  observedAt: SafeNatural,
419
607
  enemyObservedAt: Schema.optionalKey(SafeNatural),
420
608
  clanEnemyObservedAt: Schema.optionalKey(SafeNatural)
@@ -429,23 +617,220 @@ const AirTagScopeIdentityFields = {
429
617
  };
430
618
  const AirTagScopeSnapshotSchema = Schema.Struct({
431
619
  ...AirTagScopeIdentityFields,
432
- targets: Schema.Array(AirTagTargetSchema)
620
+ targets: Schema.Array(AirTagTargetSchema),
621
+ serverTime: Schema.optionalKey(SafeNatural)
433
622
  });
434
623
  const AirTagUpdateEventSchema = Schema.Struct({
435
624
  ...AirTagScopeIdentityFields,
436
625
  target: AirTagTargetSchema
437
626
  });
438
- Schema.is(AirTagRelationSchema);
627
+ const AirTagScopeUpdateEventSchema = Schema.Struct({
628
+ ...AirTagScopeIdentityFields,
629
+ targets: Schema.Array(AirTagTargetSchema),
630
+ removedTargetIds: Schema.Array(ShortString)
631
+ });
632
+ const AirTagMapThreatEventSchema = Schema.Struct({
633
+ guildId: GuildId,
634
+ world: ShortString,
635
+ mapId: Coordinate,
636
+ mapName: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
637
+ revision: SafeNatural,
638
+ enemies: Schema.Array(Schema.Struct({
639
+ targetId: AirTagObservationSchema.fields.targetId,
640
+ nickname: AirTagObservationSchema.fields.nickname,
641
+ clan: AirTagObservationSchema.fields.clan,
642
+ lvl: AirTagObservationSchema.fields.lvl,
643
+ prof: AirTagObservationSchema.fields.prof,
644
+ stasis: AirTagObservationSchema.fields.stasis,
645
+ ageMs: SafeNatural
646
+ }))
647
+ });
439
648
  Schema.is(AirTagObservationSchema);
440
- Schema.is(AirTagTargetSchema);
649
+ Schema.is(AirTagRelationSchema);
441
650
  Schema.is(AirTagScopeSnapshotSchema);
442
651
  Schema.is(AirTagUpdateEventSchema);
652
+ Schema.is(AirTagScopeUpdateEventSchema);
653
+ Schema.is(AirTagDepartureSchema);
654
+ //#endregion
655
+ //#region ../schema/src/battle-ping.ts
656
+ /** Pings placed on an enemy warrior. `attack` is the team's shared target. */
657
+ const BATTLE_ENEMY_PING_TYPES = [
658
+ "attack",
659
+ "caution",
660
+ "spare"
661
+ ];
662
+ /** Requests placed on an allied warrior (or on the sender's own warrior). */
663
+ const BATTLE_REQUEST_PING_TYPES = [
664
+ "attack-speed-aura",
665
+ "atmo",
666
+ "cleanse",
667
+ "emanating-arrow",
668
+ "fury",
669
+ "heal",
670
+ "healing-wave",
671
+ "need-heal",
672
+ "poison",
673
+ "protection-aura",
674
+ "rime",
675
+ "stigma",
676
+ "taunt"
677
+ ];
678
+ /**
679
+ * Calls to the whole battle team, sent from the sender's own warrior. Only
680
+ * sockets that negotiated `lootlog.battle-ping.team.v1` send or receive them.
681
+ */
682
+ const BATTLE_TEAM_PING_TYPES = ["quick-fight"];
683
+ const BATTLE_PING_TYPES = [
684
+ ...BATTLE_ENEMY_PING_TYPES,
685
+ ...BATTLE_REQUEST_PING_TYPES,
686
+ ...BATTLE_TEAM_PING_TYPES
687
+ ];
688
+ /** A Margonem party holds at most ten characters, so nine other recipients. */
689
+ const MAX_BATTLE_PING_RECIPIENTS = 9;
690
+ /** Margonem battle warrior ids: positive for characters, negative for NPCs. */
691
+ const MAX_BATTLE_WARRIOR_ID = 2147483647;
692
+ const BattlePingTypeSchema = Schema.Literals(BATTLE_PING_TYPES);
693
+ const BattleWarriorIdSchema = Schema.Int.check(Schema.isBetween({
694
+ minimum: -2147483647,
695
+ maximum: MAX_BATTLE_WARRIOR_ID
696
+ }), Schema.makeFilter((id) => id !== 0 || "warrior id must not be 0"));
697
+ const CharacterIdSchema$1 = Schema.String.check(Schema.isPattern(/^[1-9]\d{0,9}$/u));
698
+ Schema.is(BattlePingTypeSchema);
699
+ Schema.is(Schema.Literals(BATTLE_ENEMY_PING_TYPES));
700
+ Schema.is(Schema.Literals(BATTLE_TEAM_PING_TYPES));
701
+ const BattlePingSendPayloadSchema = Schema.Struct({
702
+ expectedMapId: NonNegativeInt,
703
+ type: BattlePingTypeSchema,
704
+ warriorId: BattleWarriorIdSchema,
705
+ recipientCharacterIds: Schema.Array(CharacterIdSchema$1).check(Schema.isMinLength(1), Schema.isMaxLength(MAX_BATTLE_PING_RECIPIENTS))
706
+ });
707
+ const BattlePingEventSchema = Schema.Struct({
708
+ pingId: Schema.NonEmptyString,
709
+ world: Schema.NonEmptyString,
710
+ mapId: NonNegativeInt,
711
+ type: BattlePingTypeSchema,
712
+ warriorId: BattleWarriorIdSchema,
713
+ sender: Schema.Struct({
714
+ characterId: Schema.NonEmptyString,
715
+ name: Schema.NonEmptyString
716
+ }),
717
+ createdAt: NonNegativeInt
718
+ });
719
+ //#endregion
720
+ //#region ../schema/src/loot-split.ts
721
+ /**
722
+ * A participant's choice for one item of a group loot split, as Margonem's
723
+ * `loot.states` reports it: 0 "nie chcę", 1 "chcę", 2 "bardzo chcę".
724
+ */
725
+ const LOOT_SPLIT_CHOICES = [
726
+ 0,
727
+ 1,
728
+ 2
729
+ ];
730
+ /** A Margonem party holds at most ten characters, so nine other recipients. */
731
+ const MAX_LOOT_SPLIT_RECIPIENTS = 9;
732
+ /** Bounds a frame; one group loot holds far fewer items. */
733
+ const MAX_LOOT_SPLIT_ITEMS = 64;
734
+ const ITEM_ID_PATTERN = /^[1-9]\d{0,17}$/u;
735
+ const ItemIdSchema = Schema.String.check(Schema.isPattern(ITEM_ID_PATTERN));
736
+ const CharacterIdSchema = Schema.String.check(Schema.isPattern(/^[1-9]\d{0,9}$/u));
737
+ const RecipientCharacterIdsSchema = Schema.Array(CharacterIdSchema).check(Schema.isMinLength(1), Schema.isMaxLength(MAX_LOOT_SPLIT_RECIPIENTS));
738
+ const SenderSchema = Schema.Struct({
739
+ characterId: Schema.NonEmptyString,
740
+ name: Schema.NonEmptyString
741
+ });
742
+ const LootSplitChoicesSchema = Schema.Record(Schema.String, Schema.Literals(LOOT_SPLIT_CHOICES)).check(Schema.makeFilter((choices) => {
743
+ const ids = Object.keys(choices);
744
+ return ids.length <= MAX_LOOT_SPLIT_ITEMS && ids.every((id) => ITEM_ID_PATTERN.test(id)) || "choices must map at most 64 item ids";
745
+ }));
746
+ const LootSplitSendPayloadSchema = Schema.Struct({
747
+ splitId: ItemIdSchema,
748
+ choices: LootSplitChoicesSchema,
749
+ accepted: Schema.Boolean,
750
+ recipientCharacterIds: RecipientCharacterIdsSchema
751
+ });
752
+ const LootSplitEventSchema = Schema.Struct({
753
+ splitId: ItemIdSchema,
754
+ world: Schema.NonEmptyString,
755
+ sender: SenderSchema,
756
+ choices: LootSplitChoicesSchema,
757
+ accepted: Schema.Boolean,
758
+ sentAt: NonNegativeInt
759
+ });
760
+ const LootSplitAutoAcceptPayloadSchema = Schema.Struct({ recipientCharacterIds: RecipientCharacterIdsSchema });
761
+ const LootSplitAutoAcceptedEventSchema = Schema.Struct({
762
+ world: Schema.NonEmptyString,
763
+ sender: SenderSchema,
764
+ sentAt: NonNegativeInt
765
+ });
766
+ const LootSplitAckSchema = Schema.Union([Schema.Struct({ status: Schema.Literal("accepted") }), Schema.Struct({
767
+ status: Schema.Literal("rejected"),
768
+ code: Schema.Literals([
769
+ "forbidden",
770
+ "invalid-context",
771
+ "invalid-payload",
772
+ "rate-limited",
773
+ "temporarily-unavailable"
774
+ ]),
775
+ retryAfterMs: Schema.optionalKey(NonNegativeInt)
776
+ })]);
777
+ /** The fields organization NPC visibility needs, keyed by the NPC's runtime id. */
778
+ const NpcPresenceNpcSchema = Schema.Struct({
779
+ id: NonNegativeInt,
780
+ lvl: NonNegativeInt,
781
+ wt: NonNegativeInt,
782
+ prof: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(8))),
783
+ type: Schema.optionalKey(NonNegativeInt)
784
+ });
785
+ /** Every NPC a character currently sees on its map; each report replaces the previous one. */
786
+ const NpcPresenceReportSchema = Schema.Struct({
787
+ world: Schema.NonEmptyString.check(Schema.isMaxLength(64)),
788
+ organizationIds: Schema.Array(Schema.NonEmptyString).check(Schema.isMaxLength(20)),
789
+ npcs: Schema.Array(NpcPresenceNpcSchema).check(Schema.isMaxLength(50))
790
+ });
791
+ /** Sent only when an NPC gains its first reporter or loses its last one. */
792
+ const NpcPresenceEventSchema = Schema.Struct({
793
+ organizationId: Schema.NonEmptyString,
794
+ world: Schema.NonEmptyString,
795
+ /** Orders the updates of one Organization and world across gateway replicas. */
796
+ revision: NonNegativeInt,
797
+ npc: NpcPresenceNpcSchema,
798
+ standing: Schema.Boolean,
799
+ /** Gateway time the NPC was first reported. */
800
+ since: NonNegativeInt
801
+ });
802
+ const NpcPresenceSnapshotSchema = Schema.Struct({
803
+ revision: NonNegativeInt,
804
+ npcs: Schema.Array(Schema.Struct({
805
+ npc: NpcPresenceNpcSchema,
806
+ since: NonNegativeInt
807
+ }))
808
+ });
809
+ const NpcPresenceRejectCode = Schema.Literals([
810
+ "forbidden",
811
+ "invalid-context",
812
+ "invalid-payload",
813
+ "rate-limited",
814
+ "temporarily-unavailable"
815
+ ]);
816
+ const NpcPresenceReportAckSchema = Schema.Union([Schema.Struct({ status: Schema.Literal("accepted") }), Schema.Struct({
817
+ status: Schema.Literal("rejected"),
818
+ code: NpcPresenceRejectCode,
819
+ retryAfterMs: Schema.optional(NonNegativeInt)
820
+ })]);
821
+ //#endregion
822
+ //#region ../schema/src/map-ping.ts
443
823
  const MapPingTypeSchema = Schema.Literals([
444
824
  "attention",
445
825
  "enemy",
446
826
  "regroup",
447
827
  "avoid"
448
828
  ]);
829
+ /** Margonem NPC and character ids are positive 32-bit integers. */
830
+ const MapPingCharacterIdSchema = Schema.Int.check(Schema.isBetween({
831
+ minimum: 1,
832
+ maximum: 2147483647
833
+ }));
449
834
  const MapPingRejectCodeSchema = Schema.Literals([
450
835
  "forbidden",
451
836
  "invalid-context",
@@ -454,11 +839,16 @@ const MapPingRejectCodeSchema = Schema.Literals([
454
839
  "temporarily-unavailable"
455
840
  ]);
456
841
  Schema.is(MapPingTypeSchema);
457
- const MapPingSendPayloadSchema = Schema.Struct({
458
- expectedMapId: NonNegativeInt,
842
+ const mapPingFields = {
459
843
  type: MapPingTypeSchema,
460
844
  x: NonNegativeInt,
461
- y: NonNegativeInt
845
+ y: NonNegativeInt,
846
+ npcId: Schema.optionalKey(MapPingCharacterIdSchema),
847
+ playerId: Schema.optionalKey(MapPingCharacterIdSchema)
848
+ };
849
+ const MapPingSendPayloadSchema = Schema.Struct({
850
+ expectedMapId: NonNegativeInt,
851
+ ...mapPingFields
462
852
  });
463
853
  const MapPingAckSchema = Schema.Union([Schema.Struct({
464
854
  status: Schema.Literal("accepted"),
@@ -472,9 +862,7 @@ const MapPingEventSchema = Schema.Struct({
472
862
  pingId: Schema.NonEmptyString,
473
863
  world: Schema.NonEmptyString,
474
864
  mapId: NonNegativeInt,
475
- type: MapPingTypeSchema,
476
- x: NonNegativeInt,
477
- y: NonNegativeInt,
865
+ ...mapPingFields,
478
866
  sender: Schema.Struct({
479
867
  characterId: Schema.NonEmptyString,
480
868
  name: Schema.NonEmptyString
@@ -482,302 +870,24 @@ const MapPingEventSchema = Schema.Struct({
482
870
  createdAt: NonNegativeInt
483
871
  });
484
872
  //#endregion
485
- //#region ../schema/src/discord.ts
486
- const discordPermissionFields = {
487
- hasRequiredPermissions: Schema.Boolean,
488
- requiredPermissions: Schema.Array(Schema.String),
489
- grantedPermissions: Schema.Array(Schema.String),
490
- missingPermissions: Schema.Array(Schema.String)
491
- };
492
- const DiscordGuildSyncStatus = Schema.Literals([
493
- "SYNCED",
494
- "SYNCING",
495
- "FAILED",
496
- "STALE",
497
- "NOT_FOUND"
498
- ]);
499
- //#endregion
500
- //#region ../protocol/src/rabbit/topology.ts
501
- const RabbitExchange = {
502
- DEFAULT: "default",
503
- DEAD_LETTER: "dlx",
504
- RETRY: "retry"
505
- };
506
- const RabbitExchangeName = Schema.Literals([
507
- RabbitExchange.DEFAULT,
508
- RabbitExchange.DEAD_LETTER,
509
- RabbitExchange.RETRY
510
- ]);
511
- const RabbitRoutingKey = {
512
- USERS_ONLINE_CHECKPOINT_V1: "users.online.checkpoint.v1",
513
- USERS_ONLINE_CHECKPOINT_V1_RETRY: "users.online.checkpoint.v1.retry",
514
- USERS_ONLINE_CHECKPOINT_V1_DLQ: "users.online.checkpoint.v1.dlq",
515
- ACTIVITY_LOG_CREATE: "activity.log.create",
516
- ACTIVITY_LOG_CREATE_DLQ: "activity.log.create.dlq",
517
- ACTIVITY_LOG_CREATE_RETRY: "activity.log.create.retry",
518
- DISCORD_GUILD_CHANNEL_DELETED: "discord.guild.channel.deleted",
519
- DISCORD_GUILD_CHANNEL_UPSERTED: "discord.guild.channel.upserted",
520
- DISCORD_GUILD_CHANNELS_SYNC_FAILED: "discord.guild.channels.sync.failed",
521
- DISCORD_GUILD_CHANNELS_SYNCED: "discord.guild.channels.synced",
522
- DISCORD_GUILD_SYNC_STATE_UPDATED: "discord.guild.sync-state.updated",
523
- EVENT_HERO_KILLED: "event.hero.killed",
524
- EVENT_MAP_STATUS_UPDATE: "event.map-status.update",
525
- EVENT_RANKING_UPDATE: "event.ranking.update",
526
- EVENT_RESPAWN_WINDOW_CLOSED: "event.respawn-window.closed",
527
- EVENT_RESPAWN_WINDOW_OPENED: "event.respawn-window.opened",
528
- GUILDS_CLEAR_MESSAGES: "guilds.clear.messages",
529
- GUILDS_CREATE: "guilds.create",
530
- GUILDS_CREATE_DLQ: "guilds.create.dlq",
531
- GUILDS_CREATE_RETRY: "guilds.create.retry",
532
- GUILDS_CREATE_ROLE: "guilds.create.role",
533
- GUILDS_CREATE_ROLE_DLQ: "guilds.create.role.dlq",
534
- GUILDS_CREATE_ROLE_RETRY: "guilds.create.role.retry",
535
- GUILDS_DELETE: "guilds.delete",
536
- GUILDS_DELETE_DLQ: "guilds.delete.dlq",
537
- GUILDS_DELETE_MESSAGE: "guilds.delete.message",
538
- GUILDS_DELETE_RETRY: "guilds.delete.retry",
539
- GUILDS_DELETE_ROLE: "guilds.delete.role",
540
- GUILDS_DELETE_ROLE_DLQ: "guilds.delete.role.dlq",
541
- GUILDS_DELETE_ROLE_RETRY: "guilds.delete.role.retry",
542
- GUILDS_INITIALIZE: "guilds.initialize",
543
- GUILDS_INITIALIZE_BOT: "guilds.initialize-bot",
544
- GUILDS_INITIALIZE_DLQ: "guilds.initialize.dlq",
545
- GUILDS_INITIALIZE_RETRY: "guilds.initialize.retry",
546
- GUILDS_KILLS_ACCEPTED_V1: "guilds.kills.accepted.v1",
547
- GUILDS_KILLS_ACCEPTED_V1_RETRY: "guilds.kills.accepted.v1.retry",
548
- GUILDS_KILLS_ACCEPTED_V1_DLQ: "guilds.kills.accepted.v1.dlq",
549
- GUILDS_LOOTS_CREATE: "guilds.loots.create",
550
- GUILDS_LOOTS_CREATE_DLQ: "guilds.loots.create.dlq",
551
- GUILDS_LOOTS_CREATE_RETRY: "guilds.loots.create.retry",
552
- GUILDS_LOOTS_DELETE: "guilds.loots.delete",
553
- GUILDS_LOOTS_SHARE_UPDATE: "guilds.loots.share.update",
554
- GUILDS_LOOTS_SHARE_UPDATE_DLQ: "guilds.loots.share.update.dlq",
555
- GUILDS_LOOTS_SHARE_UPDATE_RETRY: "guilds.loots.share.update.retry",
556
- GUILDS_LOOTS_UPDATE: "guilds.loots.update",
557
- GUILDS_MEMBERS_ADD: "guilds.members.add",
558
- GUILDS_MEMBERS_ADD_DLQ: "guilds.members.add.dlq",
559
- GUILDS_MEMBERS_ADD_RETRY: "guilds.members.add.retry",
560
- GUILDS_MEMBERS_ADD_ROLE: "guilds.members.add.role",
561
- GUILDS_MEMBERS_ADD_ROLE_DLQ: "guilds.members.add.role.dlq",
562
- GUILDS_MEMBERS_ADD_ROLE_RETRY: "guilds.members.add.role.retry",
563
- GUILDS_MEMBERS_REFRESH_JOB_UPDATE: "guilds.members.refresh.job.update",
564
- GUILDS_MEMBERS_REMOVE: "guilds.members.remove",
565
- GUILDS_MEMBERS_REMOVE_DLQ: "guilds.members.remove.dlq",
566
- GUILDS_MEMBERS_REMOVE_RETRY: "guilds.members.remove.retry",
567
- GUILDS_MEMBERS_REMOVE_ROLE: "guilds.members.remove.role",
568
- GUILDS_MEMBERS_REMOVE_ROLE_DLQ: "guilds.members.remove.role.dlq",
569
- GUILDS_MEMBERS_REMOVE_ROLE_RETRY: "guilds.members.remove.role.retry",
570
- GUILDS_MEMBERS_UPDATE: "guilds.members.update",
571
- GUILDS_MEMBERS_UPDATE_DLQ: "guilds.members.update.dlq",
572
- GUILDS_MEMBERS_UPDATE_RETRY: "guilds.members.update.retry",
573
- GUILDS_NOTIFICATIONS_SEND: "guilds.notifications.send",
574
- GUILDS_NOTIFICATIONS_SEND_DLQ: "guilds.notifications.send.dlq",
575
- GUILDS_NOTIFICATIONS_SEND_RETRY: "guilds.notifications.send.retry",
576
- GUILDS_NOTIFICATIONS_VOLUNTEER: "guilds.notifications.volunteer",
577
- GUILDS_NOTIFICATIONS_VOLUNTEER_DLQ: "guilds.notifications.volunteer.dlq",
578
- GUILDS_NOTIFICATIONS_VOLUNTEER_RETRY: "guilds.notifications.volunteer.retry",
579
- GUILDS_PARTY_GATHERING: "guilds.party-gathering",
580
- GUILDS_PARTY_GATHERING_CANCEL: "guilds.party-gathering.cancel",
581
- GUILDS_PARTY_GATHERING_DLQ: "guilds.party-gathering.dlq",
582
- GUILDS_PARTY_GATHERING_RETRY: "guilds.party-gathering.retry",
583
- GUILDS_RESERVATIONS_CHANGED_V2: "guilds.reservations.v2.changed",
584
- GUILDS_RESERVATIONS_CHANGED_V2_DLQ: "guilds.reservations.v2.changed.dlq",
585
- GUILDS_RESERVATIONS_CHANGED_V2_RETRY: "guilds.reservations.v2.changed.retry",
586
- GUILDS_RESERVATIONS_CREATE: "guilds.reservations.create",
587
- GUILDS_RESERVATIONS_CREATE_DLQ: "guilds.reservations.create.dlq",
588
- GUILDS_RESERVATIONS_CREATE_RETRY: "guilds.reservations.create.retry",
589
- GUILDS_RESERVATIONS_DELETE: "guilds.reservations.delete",
590
- GUILDS_RESERVATIONS_DELETE_DLQ: "guilds.reservations.delete.dlq",
591
- GUILDS_RESERVATIONS_DELETE_RETRY: "guilds.reservations.delete.retry",
592
- GUILDS_SEND_MESSAGE: "guilds.send.message",
593
- GUILDS_SEND_MESSAGE_DLQ: "guilds.send.message.dlq",
594
- GUILDS_SEND_MESSAGE_RETRY: "guilds.send.message.retry",
595
- GUILDS_SYNC: "guilds.sync",
596
- GUILDS_SYNC_TRIGGER: "guilds.sync.trigger",
597
- GUILDS_TIMERS_DELETE: "guilds.timers.delete",
598
- GUILDS_TIMERS_DELETE_DLQ: "guilds.timers.delete.dlq",
599
- GUILDS_TIMERS_DELETE_RETRY: "guilds.timers.delete.retry",
600
- GUILDS_TIMERS_UPDATE: "guilds.timers.update",
601
- GUILDS_TIMERS_UPDATE_DLQ: "guilds.timers.update.dlq",
602
- GUILDS_TIMERS_UPDATE_RETRY: "guilds.timers.update.retry",
603
- GUILDS_UPDATE: "guilds.update",
604
- GUILDS_UPDATE_DLQ: "guilds.update.dlq",
605
- GUILDS_UPDATE_MESSAGE: "guilds.update.message",
606
- GUILDS_UPDATE_RETRY: "guilds.update.retry",
607
- GUILDS_UPDATE_ROLE: "guilds.update.role",
608
- GUILDS_UPDATE_ROLE_DLQ: "guilds.update.role.dlq",
609
- GUILDS_UPDATE_ROLE_RETRY: "guilds.update.role.retry",
610
- NOTIFICATIONS_DELIVERY_RESULT: "notifications.delivery.result",
611
- NOTIFICATIONS_DISCORD_SEND: "notifications.discord.send",
612
- NOTIFICATIONS_LOOT_CREATED: "notifications.loot.created",
613
- NOTIFICATIONS_TIMER_DELETED: "notifications.timer.deleted",
614
- NOTIFICATIONS_TIMER_UPDATED: "notifications.timer.updated",
615
- GAME_CHARACTER_OFFLINE: "game.character.offline",
616
- PRESENCE_CHECK_REQUEST: "presence.check.request",
617
- PRESENCE_COVERAGE_CHECK: "presence.coverage.check",
618
- SEARCH_ITEMS_INDEX: "search.items.index",
619
- SEARCH_NPCS_INDEX: "search.npcs.index",
620
- SEARCH_PLAYERS_INDEX: "search.players.index",
621
- USERS_PARTY_READY_ROOM_UPDATED: "users.party-ready-room.updated",
622
- USERS_PARTY_READY_ROOM_UPDATED_DLQ: "users.party-ready-room.updated.dlq",
623
- USERS_PARTY_READY_ROOM_UPDATED_RETRY: "users.party-ready-room.updated.retry"
624
- };
625
- const RabbitRoutingKeyName = Schema.Literals(Object.values(RabbitRoutingKey));
626
- Schema.Struct({
627
- name: RabbitExchangeName,
628
- type: Schema.Literal("topic"),
629
- durable: Schema.Boolean
630
- });
631
- Schema.Struct({
632
- name: Schema.String,
633
- exchange: RabbitExchangeName,
634
- routingKey: RabbitRoutingKeyName,
635
- durable: Schema.Boolean,
636
- messageTtl: Schema.optional(Schema.Int),
637
- singleActiveConsumer: Schema.optional(Schema.Boolean),
638
- deadLetterExchange: Schema.optional(RabbitExchangeName),
639
- deadLetterRoutingKey: Schema.optional(RabbitRoutingKeyName)
640
- });
641
- RabbitExchange.DEFAULT, RabbitExchange.RETRY, RabbitExchange.DEAD_LETTER;
642
- Schema.Struct({
643
- userId: NonEmptyString,
644
- discordId: NonEmptyString,
645
- world: NonEmptyString,
646
- characterId: NonEmptyString,
647
- organizationIds: Schema.Array(NonEmptyString),
648
- disconnectedAt: NonNegativeInt
649
- });
650
- const NullableString = Schema.NullOr(Schema.String);
651
- const NullableNumber = Schema.NullOr(Schema.Number);
652
- const GuildRole = Schema.Struct({
653
- id: NonEmptyString,
654
- name: NonEmptyString,
655
- color: Schema.Number,
656
- admin: Schema.Boolean,
657
- position: Schema.Number
658
- });
659
- Schema.Struct({
660
- guildId: NonEmptyString,
661
- name: NonEmptyString,
662
- icon: NullableString,
663
- ownerId: NonEmptyString,
664
- roles: Schema.Array(GuildRole)
665
- });
666
- Schema.Struct({
667
- guildId: NonEmptyString,
668
- name: NonEmptyString,
669
- icon: NullableString,
670
- ownerId: NonEmptyString
671
- });
672
- Schema.Struct({ guildId: NonEmptyString });
673
- Schema.Struct({
674
- guildId: NonEmptyString,
675
- ...GuildRole.fields
676
- });
677
- Schema.Struct({
678
- guildId: NonEmptyString,
679
- id: NonEmptyString
680
- });
681
- Schema.Struct({
682
- guildId: NonEmptyString,
683
- discordId: NonEmptyString,
684
- userId: NonEmptyString
685
- });
686
- Schema.Struct({
687
- guildId: NonEmptyString,
688
- mapName: NonEmptyString
689
- });
690
- Schema.Struct({
691
- guildId: NonEmptyString,
692
- mapName: NonEmptyString,
693
- discordId: NonEmptyString,
694
- hasPlayer: Schema.Boolean,
695
- isAfk: Schema.optional(Schema.Boolean)
696
- });
697
- const DiscordGuildChannel = Schema.Struct({
698
- guildId: NonEmptyString,
699
- channelId: NonEmptyString,
700
- name: NonEmptyString,
701
- channelType: NonEmptyString,
702
- parentId: NullableString,
703
- position: Schema.Number,
704
- active: Schema.Boolean,
705
- canView: Schema.Boolean,
706
- canSend: Schema.Boolean,
707
- ...discordPermissionFields,
708
- lastSyncedAt: NonEmptyString
709
- });
710
- const DiscordGuildSyncState = Schema.Struct({
711
- guildId: NonEmptyString,
712
- status: DiscordGuildSyncStatus,
713
- ...discordPermissionFields,
714
- channelCount: NonNegativeInt,
715
- selectableChannelCount: NonNegativeInt,
716
- lastAttemptAt: NullableString,
717
- lastSuccessAt: NullableString,
718
- lastError: NullableString,
719
- updatedAt: NonEmptyString
720
- });
721
- Schema.Struct({
722
- guildId: NonEmptyString,
723
- channels: Schema.Array(DiscordGuildChannel),
724
- syncState: DiscordGuildSyncState
725
- });
726
- Schema.Struct({
727
- guildId: NonEmptyString,
728
- channel: DiscordGuildChannel,
729
- syncState: DiscordGuildSyncState
730
- });
731
- Schema.Struct({
732
- guildId: NonEmptyString,
733
- status: DiscordGuildSyncStatus,
734
- lastAttemptAt: NonEmptyString,
735
- lastError: NonEmptyString
736
- });
737
- Schema.Struct({
738
- guildId: NonEmptyString,
739
- syncState: DiscordGuildSyncState
740
- });
741
- Schema.Union([Schema.Struct({ guildId: NonEmptyString }), Schema.Struct({ organizationId: NonEmptyString })]);
742
- Schema.Struct({
743
- recipientDiscordId: NonEmptyString,
744
- eligibleGuildIds: Schema.Array(NonEmptyString),
745
- update: Schema.Unknown
746
- });
747
- Schema.Struct({
748
- notificationId: NonEmptyString,
749
- targetDiscordId: NonEmptyString,
750
- volunteerDiscordId: NonEmptyString,
751
- world: NonEmptyString,
752
- character: Schema.Unknown
753
- });
873
+ //#region ../protocol/src/loot-events.ts
754
874
  const GuildLootEventNpc = Schema.Struct({
755
- lvl: Schema.optional(NullableNumber),
756
- prof: Schema.optional(NullableString),
875
+ lvl: Schema.optional(Schema.NullOr(Schema.Number)),
876
+ prof: Schema.optional(Schema.NullOr(Schema.String)),
757
877
  type: Schema.optional(Schema.NullOr(Schema.Union([Schema.Number, Schema.String]))),
758
878
  wt: Schema.optional(Schema.NullOr(Schema.Union([Schema.Number, Schema.String])))
759
879
  });
760
- Schema.Struct({
761
- sourceNpcs: Schema.optional(Schema.Array(Schema.Struct({
762
- level: NonNegativeInt,
763
- type: NpcTypeSchema
764
- }))),
765
- feedEntry: Schema.optional(UserFeedItem),
766
- version: Schema.Literal(1),
767
- guildId: NonEmptyString,
768
- world: NonEmptyString,
769
- npc: Schema.Struct({
770
- type: NpcTypeSchema,
771
- lvl: NonNegativeInt
772
- })
773
- });
774
880
  const GuildLootCreatedEventV2 = Schema.Struct({
775
881
  feedEntry: Schema.optional(UserFeedItem),
776
882
  version: Schema.Literal(2),
777
- guildId: NonEmptyString,
883
+ guildId: Schema.NonEmptyString,
778
884
  lootId: NonNegativeInt,
779
885
  npcs: Schema.Array(GuildLootEventNpc)
780
886
  });
887
+ const GuildLootCreatedMessageV2 = Schema.Struct({
888
+ ...GuildLootCreatedEventV2.fields,
889
+ loot: Schema.optional(LootSnapshot)
890
+ });
781
891
  const GuildLootShareUpdatedEventV2 = Schema.Struct({
782
892
  ...GuildLootCreatedEventV2.fields,
783
893
  lootShare: Schema.Record(Schema.String, Schema.Array(Schema.String))
@@ -790,105 +900,29 @@ const ReservationChangedEventV2 = Schema.Struct({
790
900
  "deleted",
791
901
  "sharing-changed"
792
902
  ]),
793
- sourceGuildId: NonEmptyString,
794
- audienceGuildIds: Schema.Array(NonEmptyString),
903
+ sourceGuildId: Schema.NonEmptyString,
904
+ audienceGuildIds: Schema.Array(Schema.NonEmptyString),
795
905
  reservationId: Schema.NullOr(NonNegativeInt),
796
906
  spotId: Schema.NullOr(Schema.String)
797
907
  });
798
- const EventScope = Schema.Struct({
799
- guildId: NonEmptyString,
800
- eventId: NonEmptyString
801
- });
802
- Schema.Struct({
803
- ...EventScope.fields,
804
- heroNpcLvl: Schema.optional(Schema.NullOr(NonNegativeInt)),
805
- mapId: NonEmptyString,
806
- reason: Schema.optional(Schema.String)
807
- });
808
- Schema.Struct({
809
- ...EventScope.fields,
810
- heroNpcLvl: Schema.optional(Schema.NullOr(NonNegativeInt)),
811
- heroId: Schema.optional(NonEmptyString)
812
- });
813
- Schema.Struct({
814
- ...EventScope.fields,
815
- heroNpcLvl: Schema.optional(Schema.NullOr(NonNegativeInt)),
816
- heroId: NonEmptyString
817
- });
818
- Schema.Struct({
819
- userId: NonEmptyString,
820
- guildId: Schema.optional(NonEmptyString),
821
- action: NonEmptyString,
822
- entityType: Schema.optional(NonEmptyString),
823
- entityId: Schema.optional(Schema.Union([Schema.String, Schema.Number])),
824
- metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
825
- });
826
- Schema.Struct({
827
- guildId: NonEmptyString,
828
- world: NonEmptyString,
829
- npcId: NonNegativeInt,
830
- timerKey: NonEmptyString,
831
- minSpawnTime: NonEmptyString,
832
- maxSpawnTime: NonEmptyString,
833
- npc: Schema.optional(Schema.NullOr(Schema.Struct({ name: Schema.optional(Schema.String) })))
834
- });
835
- Schema.Struct({
836
- guildId: NonEmptyString,
837
- world: NonEmptyString,
838
- timerKey: NonEmptyString,
839
- npcId: Schema.optional(NonNegativeInt)
840
- });
841
- Schema.Struct({
842
- version: Schema.Literal(2),
843
- lootId: NonNegativeInt,
844
- world: NonEmptyString,
845
- guildIds: Schema.Array(NonEmptyString),
846
- itemIds: Schema.Array(NonNegativeInt),
847
- itemNames: Schema.Array(Schema.String),
848
- npcs: Schema.Array(Schema.Struct({
849
- type: NullableString,
850
- lvl: NullableNumber
851
- }))
852
- });
853
- Schema.Struct({
854
- notificationJobId: NonEmptyString,
855
- success: Schema.Boolean,
856
- retryable: Schema.Boolean,
857
- providerMessageId: Schema.optional(NullableString),
858
- errorCode: Schema.optional(NullableString),
859
- errorMessage: Schema.optional(NullableString),
860
- deliveredAt: NonEmptyString
861
- });
862
- Schema.Struct({
863
- guildId: NonEmptyString,
864
- channelId: NonEmptyString,
865
- syncState: DiscordGuildSyncState
866
- });
867
- /** Cumulative confirmed game presence. A gap starts a new segment; retries never extend it. */
868
- const UserOnlineCheckpointV1 = Schema.Struct({
869
- version: Schema.Literal(1),
870
- type: Schema.Literal("checkpoint"),
871
- userId: NonEmptyString,
872
- sessionId: NonEmptyString,
873
- segmentId: NonEmptyString,
874
- world: Schema.optional(NonEmptyString),
875
- startedAt: DateTimeWithOffsetString,
876
- endedAt: DateTimeWithOffsetString,
877
- observedAt: DateTimeWithOffsetString
878
- }).check(Schema.makeFilter((event) => Date.parse(event.startedAt) <= Date.parse(event.endedAt) && Date.parse(event.endedAt) <= Date.parse(event.observedAt), { expected: "ordered confirmed interval timestamps" }));
879
- /** Published only after the durable publisher has drained its pending checkpoints. */
880
- const UserOnlineHealthV1 = Schema.Struct({
881
- version: Schema.Literal(1),
882
- type: Schema.Literal("collector"),
883
- observedAt: DateTimeWithOffsetString,
884
- status: Schema.Literals(["healthy", "degraded"])
885
- });
886
- Schema.Union([UserOnlineCheckpointV1, UserOnlineHealthV1]);
887
- RabbitRoutingKey.GAME_CHARACTER_OFFLINE, RabbitRoutingKey.GUILDS_KILLS_ACCEPTED_V1, RabbitRoutingKey.USERS_ONLINE_CHECKPOINT_V1, RabbitRoutingKey.ACTIVITY_LOG_CREATE, RabbitRoutingKey.DISCORD_GUILD_CHANNEL_DELETED, RabbitRoutingKey.DISCORD_GUILD_CHANNEL_UPSERTED, RabbitRoutingKey.DISCORD_GUILD_CHANNELS_SYNC_FAILED, RabbitRoutingKey.DISCORD_GUILD_CHANNELS_SYNCED, RabbitRoutingKey.DISCORD_GUILD_SYNC_STATE_UPDATED, RabbitRoutingKey.EVENT_HERO_KILLED, RabbitRoutingKey.EVENT_MAP_STATUS_UPDATE, RabbitRoutingKey.EVENT_RANKING_UPDATE, RabbitRoutingKey.EVENT_RESPAWN_WINDOW_CLOSED, RabbitRoutingKey.EVENT_RESPAWN_WINDOW_OPENED, RabbitRoutingKey.GUILDS_LOOTS_CREATE, RabbitRoutingKey.GUILDS_LOOTS_SHARE_UPDATE, RabbitRoutingKey.GUILDS_CREATE, RabbitRoutingKey.GUILDS_DELETE, RabbitRoutingKey.GUILDS_UPDATE, RabbitRoutingKey.GUILDS_CREATE_ROLE, RabbitRoutingKey.GUILDS_DELETE_ROLE, RabbitRoutingKey.GUILDS_UPDATE_ROLE, RabbitRoutingKey.GUILDS_MEMBERS_ADD, RabbitRoutingKey.GUILDS_MEMBERS_ADD_ROLE, RabbitRoutingKey.GUILDS_MEMBERS_REMOVE, RabbitRoutingKey.GUILDS_MEMBERS_REMOVE_ROLE, RabbitRoutingKey.GUILDS_MEMBERS_UPDATE, RabbitRoutingKey.GUILDS_TIMERS_UPDATE, RabbitRoutingKey.GUILDS_TIMERS_DELETE, RabbitRoutingKey.GUILDS_RESERVATIONS_CREATE, RabbitRoutingKey.GUILDS_RESERVATIONS_DELETE, RabbitRoutingKey.GUILDS_SEND_MESSAGE, RabbitRoutingKey.GUILDS_UPDATE_MESSAGE, RabbitRoutingKey.GUILDS_DELETE_MESSAGE, RabbitRoutingKey.GUILDS_CLEAR_MESSAGES, RabbitRoutingKey.GUILDS_NOTIFICATIONS_SEND, RabbitRoutingKey.GUILDS_NOTIFICATIONS_VOLUNTEER, RabbitRoutingKey.GUILDS_PARTY_GATHERING, RabbitRoutingKey.GUILDS_PARTY_GATHERING_CANCEL, RabbitRoutingKey.GUILDS_MEMBERS_REFRESH_JOB_UPDATE, RabbitRoutingKey.GUILDS_RESERVATIONS_CHANGED_V2, RabbitRoutingKey.NOTIFICATIONS_DELIVERY_RESULT, RabbitRoutingKey.NOTIFICATIONS_LOOT_CREATED, RabbitRoutingKey.NOTIFICATIONS_TIMER_DELETED, RabbitRoutingKey.NOTIFICATIONS_TIMER_UPDATED, RabbitRoutingKey.PRESENCE_CHECK_REQUEST, RabbitRoutingKey.PRESENCE_COVERAGE_CHECK, RabbitRoutingKey.USERS_PARTY_READY_ROOM_UPDATED;
888
- const REALTIME_SUBPROTOCOL = "lootlog.realtime.v1";
908
+ const { feedEntry: _feedEntry, ...lootCreatedFields } = GuildLootCreatedMessageV2.fields;
909
+ const LootSnapshotEvent = Schema.Struct(lootCreatedFields);
889
910
  const REALTIME_JSON_SUBPROTOCOL = "lootlog.realtime.json.v1";
911
+ const REALTIME_CLIENT_CLOSE_CODES = {
912
+ heartbeatTimeout: 4001,
913
+ heartbeatRejected: 4002,
914
+ heartbeatUnavailable: 4003,
915
+ malformedFrame: 4007,
916
+ sessionJoinFailed: 4008
917
+ };
918
+ const REALTIME_SERVER_CLOSE_CODES = {
919
+ restarting: 1012,
920
+ tryAgainLater: 1013
921
+ };
922
+ const REALTIME_SERVER_CLOSE_RECONNECT_WINDOW_MS = 3e4;
890
923
  const PRESENCE_HEARTBEAT_INTERVAL_MS = 25e3;
891
- const RequestId = NonEmptyString;
924
+ const PRESENCE_EXPIRY_MS = 6e4;
925
+ const RequestId = Schema.NonEmptyString;
892
926
  const Revision = NonNegativeInt;
893
927
  const Timestamp = NonNegativeInt;
894
928
  const RealtimeLogicalTopic = Schema.Literals([
@@ -903,13 +937,15 @@ const RealtimeLogicalTopic = Schema.Literals([
903
937
  "event.coordination",
904
938
  "map.air-tags",
905
939
  "map.pings",
906
- "party.ready-room"
940
+ "party.ready-room",
941
+ "global.chat",
942
+ "global.chat.world"
907
943
  ]);
908
944
  const SubscriptionScope = Schema.Struct({
909
945
  topic: RealtimeLogicalTopic,
910
- organizationId: Schema.optional(NonEmptyString),
911
- eventId: Schema.optional(NonEmptyString),
912
- world: Schema.optional(NonEmptyString),
946
+ organizationId: Schema.optional(Schema.NonEmptyString),
947
+ eventId: Schema.optional(Schema.NonEmptyString),
948
+ world: Schema.optional(Schema.NonEmptyString),
913
949
  mapId: Schema.optional(NonNegativeInt)
914
950
  });
915
951
  const PresencePlatform = Schema.Literals(["game", "web-app"]);
@@ -921,26 +957,26 @@ const PresenceClan = Schema.Struct({
921
957
  rank: Schema.optional(Schema.Int)
922
958
  });
923
959
  const PresenceCharacter = Schema.Struct({
924
- world: NonEmptyString,
925
- name: NonEmptyString,
960
+ world: Schema.NonEmptyString,
961
+ name: Schema.NonEmptyString,
926
962
  lvl: NonNegativeInt,
927
963
  icon: Schema.String,
928
- characterId: NonEmptyString,
929
- accountId: NonEmptyString,
964
+ characterId: Schema.NonEmptyString,
965
+ accountId: Schema.NonEmptyString,
930
966
  prof: Schema.String,
931
967
  clan: Schema.optional(PresenceClan)
932
968
  });
933
969
  const PrecisePresenceLocation = Schema.Struct({
934
970
  mapId: Schema.optional(NonNegativeInt),
935
- map: NonEmptyString,
971
+ map: Schema.NonEmptyString,
936
972
  x: Schema.optional(NonNegativeInt),
937
973
  y: Schema.optional(NonNegativeInt)
938
974
  });
939
975
  const BasicPresence = Schema.Struct({
940
- userId: NonEmptyString,
941
- discordId: Schema.optional(NonEmptyString),
942
- sessionId: NonEmptyString,
943
- organizationIds: Schema.Array(NonEmptyString),
976
+ userId: Schema.NonEmptyString,
977
+ discordId: Schema.optional(Schema.NonEmptyString),
978
+ sessionId: Schema.NonEmptyString,
979
+ organizationIds: Schema.Array(Schema.NonEmptyString),
944
980
  platform: PresencePlatform,
945
981
  status: PresenceStatus,
946
982
  confidence: PresenceConfidence,
@@ -953,29 +989,29 @@ const PresenceWithLocation = Schema.Struct({
953
989
  location: PrecisePresenceLocation
954
990
  });
955
991
  const PublishedPresence = Schema.Struct({
956
- organizationIds: Schema.Array(NonEmptyString),
992
+ organizationIds: Schema.Array(Schema.NonEmptyString),
957
993
  isAfk: Schema.optional(Schema.Boolean),
958
994
  character: Schema.optional(PresenceCharacter),
959
995
  location: Schema.optional(PrecisePresenceLocation),
960
996
  clientObservedAt: Schema.optional(Timestamp)
961
997
  });
962
998
  const PresenceSnapshot = Schema.Struct({
963
- organizationId: NonEmptyString,
964
- world: Schema.optional(NonEmptyString),
999
+ organizationId: Schema.NonEmptyString,
1000
+ world: Schema.optional(Schema.NonEmptyString),
965
1001
  revision: Revision,
966
1002
  presences: Schema.Array(Schema.Union([PresenceWithLocation, BasicPresence]))
967
1003
  });
968
1004
  const PresenceDelta = Schema.Struct({
969
- organizationId: NonEmptyString,
1005
+ organizationId: Schema.NonEmptyString,
970
1006
  revision: Revision,
971
1007
  changes: Schema.Array(Schema.Union([Schema.Struct({
972
1008
  action: Schema.Literal("upsert"),
973
1009
  presence: Schema.Union([PresenceWithLocation, BasicPresence])
974
1010
  }), Schema.Struct({
975
1011
  action: Schema.Literal("remove"),
976
- userId: NonEmptyString,
977
- discordId: Schema.optional(NonEmptyString),
978
- sessionId: NonEmptyString
1012
+ userId: Schema.NonEmptyString,
1013
+ discordId: Schema.optional(Schema.NonEmptyString),
1014
+ sessionId: Schema.NonEmptyString
979
1015
  })]))
980
1016
  });
981
1017
  const command = (type, data) => Schema.Struct({
@@ -985,25 +1021,40 @@ const command = (type, data) => Schema.Struct({
985
1021
  data
986
1022
  });
987
1023
  const SessionJoinCommand = command("session.join", Schema.Struct({
988
- world: Schema.optional(NonEmptyString),
1024
+ world: Schema.optional(Schema.NonEmptyString),
989
1025
  character: Schema.optional(PresenceCharacter),
990
1026
  margonemAccountProof: Schema.optional(Schema.Unknown)
991
1027
  }));
992
- const HeartbeatCommand = command("presence.heartbeat", Schema.Struct({ sessionId: NonEmptyString }));
1028
+ const HeartbeatCommand = command("presence.heartbeat", Schema.Struct({ sessionId: Schema.NonEmptyString }));
993
1029
  const PresencePublishCommand = command("presence.publish", PublishedPresence);
1030
+ const PingCommand = command("connection.ping", Schema.Struct({}));
994
1031
  const PresenceFetchCommand = command("presence.fetch", Schema.Struct({
995
- organizationId: NonEmptyString,
996
- world: Schema.optional(NonEmptyString)
1032
+ organizationId: Schema.NonEmptyString,
1033
+ world: Schema.optional(Schema.NonEmptyString),
1034
+ delivery: Schema.optional(Schema.Literal("response"))
997
1035
  }));
998
1036
  const SubscribeCommand = command("subscription.subscribe", SubscriptionScope);
999
1037
  const UnsubscribeCommand = command("subscription.unsubscribe", SubscriptionScope);
1000
1038
  const MapPingCommand = command("map-ping.send", MapPingSendPayloadSchema);
1039
+ const BattlePingCommand = command("battle-ping.send", BattlePingSendPayloadSchema);
1040
+ const LootSplitCommand = command("loot-split.send", LootSplitSendPayloadSchema);
1041
+ const LootSplitAutoAcceptCommand = command("loot-split.auto-accept", LootSplitAutoAcceptPayloadSchema);
1001
1042
  const AirTagSubscriptionCommand = command("air-tag.subscription", Schema.Struct({
1002
1043
  requestId: RequestId,
1003
1044
  enabled: Schema.Boolean,
1004
1045
  expectedMapId: Schema.optional(NonNegativeInt)
1005
1046
  }));
1006
1047
  const AirTagObservationCommand = command("air-tag.observation", AirTagObservationBatchSchema);
1048
+ const AirTagMapThreatsFetchCommand = command("air-tag.map-threats.fetch", Schema.Struct({
1049
+ organizationId: Schema.NonEmptyString,
1050
+ world: Schema.NonEmptyString
1051
+ }));
1052
+ const AirTagMapThreatsFetchResponse = Schema.Struct({ threats: Schema.Array(AirTagMapThreatEventSchema) });
1053
+ const NpcPresenceReportCommand = command("npc-presence.report", NpcPresenceReportSchema);
1054
+ const NpcPresenceFetchCommand = command("npc-presence.fetch", Schema.Struct({
1055
+ organizationId: Schema.NonEmptyString,
1056
+ world: Schema.NonEmptyString
1057
+ }));
1007
1058
  const AirTagRejectCode = Schema.Literals([
1008
1059
  "forbidden",
1009
1060
  "invalid-context",
@@ -1032,17 +1083,24 @@ const AirTagObservationAck = Schema.Union([Schema.Struct({
1032
1083
  const ClientCommand = Schema.Union([
1033
1084
  SessionJoinCommand,
1034
1085
  HeartbeatCommand,
1086
+ PingCommand,
1035
1087
  PresencePublishCommand,
1036
1088
  PresenceFetchCommand,
1037
1089
  SubscribeCommand,
1038
1090
  UnsubscribeCommand,
1039
1091
  MapPingCommand,
1092
+ BattlePingCommand,
1093
+ LootSplitCommand,
1094
+ LootSplitAutoAcceptCommand,
1040
1095
  AirTagSubscriptionCommand,
1041
- AirTagObservationCommand
1096
+ AirTagObservationCommand,
1097
+ AirTagMapThreatsFetchCommand,
1098
+ NpcPresenceReportCommand,
1099
+ NpcPresenceFetchCommand
1042
1100
  ]);
1043
1101
  const RealtimeError = Schema.Struct({
1044
- code: NonEmptyString,
1045
- message: NonEmptyString,
1102
+ code: Schema.NonEmptyString,
1103
+ message: Schema.NonEmptyString,
1046
1104
  retryable: Schema.Boolean,
1047
1105
  retryAfterMs: Schema.optional(NonNegativeInt),
1048
1106
  details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
@@ -1065,19 +1123,21 @@ const serverEvent = (type, data) => Schema.Struct({
1065
1123
  data
1066
1124
  });
1067
1125
  const OrganizationEvent = Schema.Struct({
1068
- organizationId: NonEmptyString,
1126
+ organizationId: Schema.NonEmptyString,
1069
1127
  payload: Schema.Unknown
1070
1128
  });
1071
1129
  const ServerEvent = Schema.Union([
1130
+ serverEvent("session.hello", Schema.Struct({ connectionId: Schema.NonEmptyString })),
1072
1131
  serverEvent("session.joined", Schema.Struct({
1073
- connectionId: NonEmptyString,
1074
- organizationIds: Schema.Array(NonEmptyString),
1132
+ connectionId: Schema.NonEmptyString,
1133
+ organizationIds: Schema.Array(Schema.NonEmptyString),
1075
1134
  subscriptionScopes: Schema.Array(SubscriptionScope),
1076
- accessPolicy: Schema.optional(AccessPolicySnapshot)
1135
+ accessPolicy: Schema.optional(AccessPolicySnapshot),
1136
+ capabilities: Schema.optional(Schema.Array(Schema.NonEmptyString))
1077
1137
  })),
1078
1138
  serverEvent("permissions.updated", Schema.Struct({
1079
1139
  changes: Schema.optional(Schema.Array(AccessPolicyChange)),
1080
- organizationIds: Schema.Array(NonEmptyString),
1140
+ organizationIds: Schema.Array(Schema.NonEmptyString),
1081
1141
  subscriptionScopes: Schema.Array(SubscriptionScope),
1082
1142
  accessPolicy: Schema.optional(AccessPolicySnapshot)
1083
1143
  })),
@@ -1087,12 +1147,24 @@ const ServerEvent = Schema.Union([
1087
1147
  serverEvent("chat.updated", OrganizationEvent),
1088
1148
  serverEvent("chat.deleted", OrganizationEvent),
1089
1149
  serverEvent("chat.cleared", OrganizationEvent),
1150
+ serverEvent("global-chat.created", GlobalChatMessageSchema),
1151
+ serverEvent("global-chat.deleted", Schema.Struct({
1152
+ world: Schema.optionalKey(GlobalChatWorld),
1153
+ id: Schema.NonEmptyString
1154
+ })),
1155
+ serverEvent("global-chat.pinned", Schema.Struct({
1156
+ world: Schema.optionalKey(GlobalChatWorld),
1157
+ message: Schema.NullOr(GlobalChatMessageSchema)
1158
+ })),
1159
+ serverEvent("global-chat.stats", GlobalChatStatsSchema),
1090
1160
  serverEvent("feed.entry", UserFeedItem),
1091
- serverEvent("kills.changed", Schema.Struct({ guildId: NonEmptyString })),
1161
+ serverEvent("kills.changed", Schema.Struct({ guildId: Schema.NonEmptyString })),
1092
1162
  serverEvent("loot.created", GuildLootCreatedEventV2),
1093
1163
  serverEvent("loot.share-updated", GuildLootShareUpdatedEventV2),
1164
+ serverEvent("loot.snapshot", LootSnapshotEvent),
1094
1165
  serverEvent("timer.created", OrganizationEvent),
1095
1166
  serverEvent("timer.deleted", OrganizationEvent),
1167
+ serverEvent("hero-sighting.updated", OrganizationEvent),
1096
1168
  serverEvent("reservation.created", OrganizationEvent),
1097
1169
  serverEvent("reservation.deleted", OrganizationEvent),
1098
1170
  serverEvent("reservation.changed", ReservationChangedEventV2),
@@ -1100,19 +1172,29 @@ const ServerEvent = Schema.Union([
1100
1172
  serverEvent("member-refresh.updated", OrganizationEvent),
1101
1173
  serverEvent("party-gathering.updated", OrganizationEvent),
1102
1174
  serverEvent("party-gathering.cancelled", OrganizationEvent),
1175
+ serverEvent("party-gathering.state-updated", Schema.Struct({
1176
+ organizationId: Schema.NonEmptyString,
1177
+ payload: PartyGatheringClientUpdateSchema
1178
+ })),
1103
1179
  serverEvent("party-ready-room.updated", OrganizationEvent),
1104
1180
  serverEvent("map-ping.received", MapPingEventSchema),
1181
+ serverEvent("battle-ping.received", BattlePingEventSchema),
1182
+ serverEvent("loot-split.received", LootSplitEventSchema),
1183
+ serverEvent("loot-split.auto-accepted", LootSplitAutoAcceptedEventSchema),
1105
1184
  serverEvent("air-tag.updated", AirTagUpdateEventSchema),
1185
+ serverEvent("air-tag.scope-updated", AirTagScopeUpdateEventSchema),
1186
+ serverEvent("air-tag.map-threat-updated", AirTagMapThreatEventSchema),
1187
+ serverEvent("npc-presence.updated", NpcPresenceEventSchema),
1106
1188
  serverEvent("event.map-status-updated", OrganizationEvent),
1107
1189
  serverEvent("event.hero-killed", OrganizationEvent),
1108
1190
  serverEvent("event.ranking-updated", OrganizationEvent),
1109
1191
  serverEvent("event.respawn-window-opened", OrganizationEvent),
1110
1192
  serverEvent("event.respawn-window-closed", OrganizationEvent),
1111
1193
  serverEvent("notification.volunteer", Schema.Struct({
1112
- notificationId: NonEmptyString,
1194
+ notificationId: Schema.NonEmptyString,
1113
1195
  volunteer: Schema.StructWithRest(Schema.Struct({
1114
- discordId: NonEmptyString,
1115
- world: NonEmptyString
1196
+ discordId: Schema.NonEmptyString,
1197
+ world: Schema.NonEmptyString
1116
1198
  }), [Schema.Record(Schema.String, Schema.Unknown)])
1117
1199
  }))
1118
1200
  ]);
@@ -1122,9 +1204,8 @@ const RealtimeFrame = Schema.Union([
1122
1204
  ServerEvent
1123
1205
  ]);
1124
1206
  Schema.decodeUnknownSync(ClientCommand);
1125
- Schema.decodeUnknownSync(Response);
1126
1207
  Schema.decodeUnknownSync(ServerEvent);
1127
- const decodeRealtimeFrame = Schema.decodeUnknownSync(RealtimeFrame);
1208
+ Schema.decodeUnknownSync(RealtimeFrame);
1128
1209
  const serverEventTypes = new Set(ServerEvent.members.map((member) => member.fields.type.literal));
1129
1210
  const isServerEventFrame = (frame) => "type" in frame && serverEventTypes.has(frame.type);
1130
1211
  //#endregion
@@ -1140,7 +1221,7 @@ const tryEncodeFrame = (frame) => Result.try({
1140
1221
  const validated = Schema.encodeUnknownSync(RealtimeFrame)(frame);
1141
1222
  return {
1142
1223
  frame: validated,
1143
- bytes: encode(validated, { ignoreUndefined: true })
1224
+ text: JSON.stringify(validated)
1144
1225
  };
1145
1226
  },
1146
1227
  catch: (error) => new RealtimeCodecError({
@@ -1149,26 +1230,62 @@ const tryEncodeFrame = (frame) => Result.try({
1149
1230
  cause: error
1150
1231
  })
1151
1232
  });
1152
- const tryEncodeRealtimeFrame = (frame) => Result.map(tryEncodeFrame(frame), ({ bytes }) => bytes);
1233
+ const tryEncodeRealtimeFrame = (frame) => Result.map(tryEncodeFrame(frame), ({ text }) => text);
1153
1234
  const encodeRealtimeFrame = (frame) => Result.getOrThrow(tryEncodeRealtimeFrame(frame));
1154
- const tryDecodeRealtimeFrame = (bytes) => Result.try({
1155
- try: () => {
1156
- const input = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
1157
- return Schema.decodeUnknownSync(RealtimeFrame)(decode(input));
1158
- },
1235
+ const rejectProtoKey = (key, value) => {
1236
+ if (key === "__proto__") throw new Error("The key __proto__ is not allowed");
1237
+ return value;
1238
+ };
1239
+ /**
1240
+ * Whether a JSON text cannot have a `__proto__` key. A key can spell it only
1241
+ * literally or through a `\u` escape.
1242
+ */
1243
+ const isWithoutProtoKey = (text) => !text.includes("__proto__") && !text.includes("\\u");
1244
+ /**
1245
+ * JSON-parse a realtime text, rejecting any `__proto__` key. The reviver makes
1246
+ * parsing several times slower, so it runs only when the text may need it.
1247
+ */
1248
+ const parseRealtimeJson = (text) => isWithoutProtoKey(text) ? JSON.parse(text) : JSON.parse(text, rejectProtoKey);
1249
+ const tryDecode = (input) => Result.try({
1250
+ try: () => Schema.decodeUnknownSync(RealtimeFrame)(input()),
1159
1251
  catch: (error) => new RealtimeCodecError({
1160
1252
  operation: "decode",
1161
1253
  message: getErrorMessage(error),
1162
1254
  cause: error
1163
1255
  })
1164
1256
  });
1257
+ const tryDecodeRealtimeFrame = (text) => tryDecode(() => parseRealtimeJson(text));
1258
+ const decodeRealtimeFrame = (text) => Result.getOrThrow(tryDecodeRealtimeFrame(text));
1165
1259
  Schema.decodeUnknownSync(PresenceSnapshot);
1166
1260
  Schema.is(MapPingAckSchema);
1261
+ Schema.is(LootSplitAckSchema);
1167
1262
  Schema.is(AirTagSubscriptionAck);
1168
1263
  Schema.is(AirTagObservationAck);
1264
+ Schema.is(AirTagMapThreatsFetchResponse);
1265
+ Schema.is(NpcPresenceReportAckSchema);
1266
+ Schema.is(NpcPresenceSnapshotSchema);
1267
+ const hasRealtimeCapabilities = Schema.is(Schema.Struct({ capabilities: Schema.Array(Schema.NonEmptyString) }));
1169
1268
  Schema.is(Schema.Struct({ presences: Schema.optional(PresenceSnapshot.fields.presences) }));
1170
1269
  //#endregion
1270
+ //#region ../client/src/realtime/notify-listeners.ts
1271
+ const reportListenerError = ({ source, error }) => {
1272
+ try {
1273
+ console.warn(`[Realtime] ${source} listener failed:`, error);
1274
+ } catch {}
1275
+ };
1276
+ const notifyListeners = (listeners, notify, source) => {
1277
+ for (const listener of listeners) try {
1278
+ notify(listener);
1279
+ } catch (error) {
1280
+ reportListenerError({
1281
+ source,
1282
+ error
1283
+ });
1284
+ }
1285
+ };
1286
+ //#endregion
1171
1287
  //#region ../client/src/realtime/realtime-client.ts
1288
+ var RealtimeRequestTimeout = class extends Error {};
1172
1289
  var RealtimeRequestError = class extends Error {
1173
1290
  code;
1174
1291
  retryable;
@@ -1193,6 +1310,7 @@ const normalizeUrl = (baseUrl, path) => {
1193
1310
  };
1194
1311
  const nativeWebSocketFactory = (url, protocols) => new WebSocket(url, protocols);
1195
1312
  const WEBSOCKET_OPEN = 1;
1313
+ const MIN_HEARTBEAT_RETRY_BUDGET_MS = 2e3;
1196
1314
  const scopeKey = (scope) => JSON.stringify([
1197
1315
  scope.topic,
1198
1316
  scope.organizationId ?? null,
@@ -1200,12 +1318,7 @@ const scopeKey = (scope) => JSON.stringify([
1200
1318
  scope.world ?? null,
1201
1319
  scope.mapId ?? null
1202
1320
  ]);
1203
- const toBytes = async (data) => {
1204
- if (data instanceof Uint8Array) return data;
1205
- if (data instanceof ArrayBuffer) return new Uint8Array(data);
1206
- if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
1207
- throw new Error("Gateway returned a non-binary realtime frame");
1208
- };
1321
+ const SERVER_CLOSE_CODES = new Set(Object.values(REALTIME_SERVER_CLOSE_CODES));
1209
1322
  const getPresenceSessionId = (data) => {
1210
1323
  if (!data || typeof data !== "object" || !("sessionId" in data)) return null;
1211
1324
  return typeof data.sessionId === "string" ? data.sessionId : null;
@@ -1218,30 +1331,34 @@ var RealtimeClient = class {
1218
1331
  reconnectMaxDelayMs;
1219
1332
  random;
1220
1333
  webSocketFactory;
1221
- frameEncoding;
1222
1334
  eventListeners = /* @__PURE__ */ new Set();
1223
1335
  stateListeners = /* @__PURE__ */ new Set();
1336
+ heartbeatLatencyMs = null;
1337
+ heartbeatLatencyListeners = /* @__PURE__ */ new Set();
1224
1338
  pending = /* @__PURE__ */ new Map();
1225
1339
  subscriptions = /* @__PURE__ */ new Map();
1226
1340
  socket = null;
1227
1341
  stateValue = "disconnected";
1228
1342
  joinData = null;
1343
+ joinAttempt = null;
1229
1344
  reconnectAttempt = 0;
1345
+ reconnectNotBeforeMs = 0;
1230
1346
  reconnectTimeout = null;
1231
1347
  heartbeatTimeout = null;
1232
1348
  presenceSessionId = null;
1349
+ presenceRefreshedAt = 0;
1350
+ pingSupported = false;
1351
+ latencyProbeInFlight = false;
1233
1352
  manuallyClosed = false;
1234
- messageChain = Promise.resolve();
1235
1353
  rejoinHandler = null;
1236
1354
  constructor(options) {
1237
1355
  this.url = normalizeUrl(options.url, options.path ?? "/ws");
1238
- this.protocols = options.protocols ? [...options.protocols] : void 0;
1356
+ this.protocols = [.../* @__PURE__ */ new Set([REALTIME_JSON_SUBPROTOCOL, ...options.protocols ?? []])];
1239
1357
  this.requestTimeoutMs = options.requestTimeoutMs ?? 2e4;
1240
1358
  this.reconnectBaseDelayMs = options.reconnectBaseDelayMs ?? 1e3;
1241
1359
  this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? 3e4;
1242
1360
  this.random = options.random ?? Math.random;
1243
1361
  this.webSocketFactory = options.webSocketFactory ?? nativeWebSocketFactory;
1244
- this.frameEncoding = options.frameEncoding ?? "messagepack";
1245
1362
  }
1246
1363
  get state() {
1247
1364
  return this.stateValue;
@@ -1259,6 +1376,7 @@ var RealtimeClient = class {
1259
1376
  this.manuallyClosed = true;
1260
1377
  this.clearReconnect();
1261
1378
  this.clearHeartbeat();
1379
+ this.pingSupported = false;
1262
1380
  this.rejectPending(/* @__PURE__ */ new Error("Realtime client disconnected"));
1263
1381
  const activeSocket = this.socket;
1264
1382
  this.socket = null;
@@ -1274,6 +1392,30 @@ var RealtimeClient = class {
1274
1392
  listener(this.stateValue);
1275
1393
  return () => this.stateListeners.delete(listener);
1276
1394
  }
1395
+ subscribeHeartbeatLatency(listener) {
1396
+ this.heartbeatLatencyListeners.add(listener);
1397
+ listener(this.heartbeatLatencyMs);
1398
+ return () => this.heartbeatLatencyListeners.delete(listener);
1399
+ }
1400
+ /**
1401
+ * Measures the round trip now instead of at the next heartbeat. A gateway
1402
+ * that does not advertise `connection.ping` keeps the heartbeat measurement.
1403
+ */
1404
+ probeLatency() {
1405
+ if (!this.pingSupported || this.stateValue !== "ready" || this.latencyProbeInFlight) return;
1406
+ const socket = this.socket;
1407
+ const startedAt = performance.now();
1408
+ this.latencyProbeInFlight = true;
1409
+ this.request("connection.ping", {}).then(() => {
1410
+ if (this.socket === socket) this.setHeartbeatLatency(Math.round(performance.now() - startedAt));
1411
+ }).catch(() => {}).finally(() => {
1412
+ this.latencyProbeInFlight = false;
1413
+ });
1414
+ }
1415
+ setHeartbeatLatency(latencyMs) {
1416
+ this.heartbeatLatencyMs = latencyMs;
1417
+ notifyListeners(this.heartbeatLatencyListeners, (listener) => listener(latencyMs), "heartbeat latency");
1418
+ }
1277
1419
  setReconnectHandler(handler) {
1278
1420
  this.rejoinHandler = handler;
1279
1421
  }
@@ -1292,22 +1434,26 @@ var RealtimeClient = class {
1292
1434
  return this.request("subscription.unsubscribe", scope);
1293
1435
  }
1294
1436
  request(type, data) {
1437
+ return this.requestWithTimeout(type, data, this.requestTimeoutMs);
1438
+ }
1439
+ requestWithTimeout(type, data, timeoutMs) {
1295
1440
  const activeSocket = this.socket;
1296
1441
  if (!activeSocket || activeSocket.readyState !== WEBSOCKET_OPEN) return Promise.reject(/* @__PURE__ */ new Error("Realtime connection is not open"));
1297
1442
  const requestId = crypto.randomUUID();
1298
1443
  return new Promise((resolve, reject) => {
1299
1444
  const timeout = setTimeout(() => {
1300
1445
  this.pending.delete(requestId);
1301
- reject(/* @__PURE__ */ new Error(`Realtime request timed out: ${type}`));
1302
- }, this.requestTimeoutMs);
1446
+ reject(new RealtimeRequestTimeout(`Realtime request timed out: ${type}`));
1447
+ }, timeoutMs);
1303
1448
  this.pending.set(requestId, {
1304
1449
  type,
1450
+ startedAt: performance.now(),
1305
1451
  resolve,
1306
1452
  reject,
1307
1453
  timeout
1308
1454
  });
1309
1455
  try {
1310
- activeSocket.send(this.encodeFrame({
1456
+ activeSocket.send(encodeRealtimeFrame({
1311
1457
  v: 1,
1312
1458
  type,
1313
1459
  requestId,
@@ -1323,7 +1469,7 @@ var RealtimeClient = class {
1323
1469
  send(type, data) {
1324
1470
  const activeSocket = this.socket;
1325
1471
  if (!activeSocket || activeSocket.readyState !== WEBSOCKET_OPEN) return;
1326
- activeSocket.send(this.encodeFrame({
1472
+ activeSocket.send(encodeRealtimeFrame({
1327
1473
  v: 1,
1328
1474
  type,
1329
1475
  data
@@ -1333,44 +1479,79 @@ var RealtimeClient = class {
1333
1479
  this.setState(state);
1334
1480
  if (this.manuallyClosed) return;
1335
1481
  const socket = this.webSocketFactory(this.url, this.protocols);
1336
- socket.binaryType = "arraybuffer";
1337
1482
  this.socket = socket;
1338
1483
  socket.addEventListener("open", () => {
1339
1484
  if (this.socket !== socket) return;
1340
1485
  this.setState("connected");
1341
- if (this.joinData) (this.rejoinHandler ?? (() => this.performJoin().then(() => void 0)))().catch(() => socket.close(4008, "session rejoin failed"));
1486
+ if (this.joinData && (this.rejoinHandler || this.stateValue === "connected")) (this.rejoinHandler ?? (() => this.performJoin().then(() => void 0)))().catch(() => socket.close(REALTIME_CLIENT_CLOSE_CODES.sessionJoinFailed, "session rejoin failed"));
1342
1487
  });
1343
1488
  socket.addEventListener("message", (event) => {
1344
- this.messageChain = this.messageChain.then(() => this.handleMessage(event.data)).catch(() => socket.close(4007, "malformed realtime frame"));
1345
- });
1346
- socket.addEventListener("error", () => {
1347
- if (this.socket === socket) socket.close();
1489
+ try {
1490
+ this.handleMessage(event.data, socket);
1491
+ } catch {
1492
+ socket.close(REALTIME_CLIENT_CLOSE_CODES.malformedFrame, "malformed realtime frame");
1493
+ }
1348
1494
  });
1349
- socket.addEventListener("close", () => {
1495
+ socket.addEventListener("close", (event) => {
1350
1496
  if (this.socket !== socket) return;
1351
1497
  this.socket = null;
1498
+ if (event.code !== void 0 && SERVER_CLOSE_CODES.has(event.code)) this.reconnectNotBeforeMs = Math.round(this.random() * REALTIME_SERVER_CLOSE_RECONNECT_WINDOW_MS);
1352
1499
  this.clearHeartbeat();
1500
+ this.pingSupported = false;
1353
1501
  this.rejectPending(/* @__PURE__ */ new Error("Realtime connection closed"));
1354
1502
  this.setState("disconnected");
1355
1503
  if (!this.manuallyClosed) this.scheduleReconnect();
1356
1504
  });
1357
1505
  }
1358
- async performJoin() {
1359
- if (!this.joinData || !this.connected) return void 0;
1506
+ performJoin(data = this.joinData) {
1507
+ const socket = this.socket;
1508
+ if (!data || !socket || !this.connected) return Promise.resolve(void 0);
1509
+ const key = JSON.stringify(data);
1510
+ const pending = this.joinAttempt;
1511
+ if (pending?.socket === socket) {
1512
+ if (pending.key === key) return pending.promise;
1513
+ return pending.promise.then(() => {
1514
+ if (this.socket !== socket || !this.connected) throw new Error("Realtime connection changed before queued join");
1515
+ return this.performJoin(data);
1516
+ });
1517
+ }
1518
+ const promise = this.restoreSession(socket, this.request("session.join", data));
1519
+ this.joinAttempt = {
1520
+ socket,
1521
+ key,
1522
+ promise
1523
+ };
1360
1524
  this.setState("joining");
1525
+ return promise;
1526
+ }
1527
+ async restoreSession(socket, joined) {
1361
1528
  try {
1362
- const result = await this.request("session.join", this.joinData);
1529
+ const result = await joined;
1530
+ if (this.socket !== socket) return result;
1531
+ this.pingSupported = hasRealtimeCapabilities(result) && result.capabilities.includes("connection.ping");
1363
1532
  await Promise.all([...this.subscriptions.values()].map((scope) => this.request("subscription.subscribe", scope)));
1533
+ if (this.socket !== socket) return result;
1364
1534
  this.reconnectAttempt = 0;
1365
1535
  this.setState("ready");
1366
1536
  return result;
1367
1537
  } catch (error) {
1368
- if (this.connected) this.socket?.close(4008, "session join failed");
1538
+ if (this.socket === socket) {
1539
+ if (error instanceof RealtimeRequestError && !error.retryable) {
1540
+ this.manuallyClosed = true;
1541
+ this.clearReconnect();
1542
+ }
1543
+ if (error instanceof RealtimeRequestError && error.retryable) this.reconnectNotBeforeMs = error.retryAfterMs ?? 0;
1544
+ socket.close(REALTIME_CLIENT_CLOSE_CODES.sessionJoinFailed, "session join failed");
1545
+ }
1369
1546
  throw error;
1547
+ } finally {
1548
+ if (this.joinAttempt?.socket === socket) this.joinAttempt = null;
1370
1549
  }
1371
1550
  }
1372
- async handleMessage(data) {
1373
- const frame = await this.decodeFrame(data);
1551
+ handleMessage(data, socket) {
1552
+ if (this.socket !== socket) return;
1553
+ if (!Predicate.isString(data)) throw new Error("Gateway returned a non-text realtime frame");
1554
+ const frame = decodeRealtimeFrame(data);
1374
1555
  if ("status" in frame) {
1375
1556
  const pending = this.pending.get(frame.requestId);
1376
1557
  if (!pending) return;
@@ -1379,6 +1560,7 @@ var RealtimeClient = class {
1379
1560
  if (frame.status === "success") {
1380
1561
  if (pending.type === "presence.publish") {
1381
1562
  this.presenceSessionId = getPresenceSessionId(frame.data);
1563
+ this.presenceRefreshedAt = pending.startedAt;
1382
1564
  this.scheduleHeartbeat();
1383
1565
  }
1384
1566
  pending.resolve(frame.data);
@@ -1386,35 +1568,64 @@ var RealtimeClient = class {
1386
1568
  return;
1387
1569
  }
1388
1570
  if (!isServerEventFrame(frame)) return;
1389
- for (const listener of this.eventListeners) listener(frame);
1390
- }
1391
- encodeFrame(frame) {
1392
- return this.frameEncoding === "json" ? JSON.stringify(frame) : encodeRealtimeFrame(frame);
1393
- }
1394
- async decodeFrame(data) {
1395
- if (this.frameEncoding === "json") {
1396
- if (typeof data !== "string") throw new Error("Gateway returned a non-text realtime frame");
1397
- return decodeRealtimeFrame(JSON.parse(data));
1398
- }
1399
- const decoded = tryDecodeRealtimeFrame(await toBytes(data));
1400
- if (Result.isFailure(decoded)) throw decoded.failure;
1401
- return decoded.success;
1571
+ notifyListeners(this.eventListeners, (listener) => listener(frame), `server event ${frame.type}`);
1402
1572
  }
1403
1573
  scheduleReconnect() {
1404
1574
  this.reconnectAttempt += 1;
1405
1575
  const exponential = Math.min(this.reconnectMaxDelayMs, this.reconnectBaseDelayMs * 2 ** (this.reconnectAttempt - 1));
1406
- const jittered = Math.round(exponential * (.5 + this.random()));
1576
+ const jittered = Math.max(Math.round(exponential * (.5 + this.random())), this.reconnectNotBeforeMs);
1577
+ this.reconnectNotBeforeMs = 0;
1407
1578
  this.setState("reconnecting");
1408
1579
  this.reconnectTimeout = setTimeout(() => this.open("reconnecting"), jittered);
1409
1580
  }
1410
- scheduleHeartbeat() {
1581
+ scheduleHeartbeat(delayMs = PRESENCE_HEARTBEAT_INTERVAL_MS, retryAttempt = 0) {
1411
1582
  this.clearHeartbeat();
1412
- if (!this.presenceSessionId || !this.connected) return;
1583
+ if (!this.presenceSessionId || !this.connected) {
1584
+ this.setHeartbeatLatency(null);
1585
+ return;
1586
+ }
1413
1587
  this.heartbeatTimeout = setTimeout(() => {
1414
1588
  const sessionId = this.presenceSessionId;
1415
1589
  if (!sessionId) return;
1416
- this.request("presence.heartbeat", { sessionId }).then(() => this.scheduleHeartbeat()).catch(() => this.socket?.close());
1417
- }, PRESENCE_HEARTBEAT_INTERVAL_MS);
1590
+ const startedAt = performance.now();
1591
+ const socket = this.socket;
1592
+ const refreshedAt = this.presenceRefreshedAt;
1593
+ const expiry = refreshedAt + PRESENCE_EXPIRY_MS;
1594
+ const remaining = expiry - startedAt;
1595
+ if (remaining <= 0 || retryAttempt > 0 && remaining < MIN_HEARTBEAT_RETRY_BUDGET_MS) {
1596
+ this.setHeartbeatLatency(null);
1597
+ socket?.close(REALTIME_CLIENT_CLOSE_CODES.heartbeatUnavailable, "heartbeat unavailable");
1598
+ return;
1599
+ }
1600
+ const isStaleHeartbeat = () => this.socket !== socket || this.presenceSessionId !== sessionId || this.presenceRefreshedAt !== refreshedAt;
1601
+ this.requestWithTimeout("presence.heartbeat", { sessionId }, Math.min(this.requestTimeoutMs, remaining)).then(() => {
1602
+ if (isStaleHeartbeat()) return;
1603
+ this.presenceRefreshedAt = startedAt;
1604
+ this.setHeartbeatLatency(Math.round(performance.now() - startedAt));
1605
+ this.scheduleHeartbeat();
1606
+ }).catch((error) => {
1607
+ if (isStaleHeartbeat()) return;
1608
+ this.setHeartbeatLatency(null);
1609
+ if (error instanceof RealtimeRequestError && error.retryable) {
1610
+ const retryDelay = Math.max(Math.round(Math.min(5e3, 1e3 * 2 ** retryAttempt) * (.5 + this.random())), error.retryAfterMs ?? 0);
1611
+ if (expiry - performance.now() - retryDelay >= MIN_HEARTBEAT_RETRY_BUDGET_MS) {
1612
+ this.scheduleHeartbeat(retryDelay, retryAttempt + 1);
1613
+ return;
1614
+ }
1615
+ socket?.close(REALTIME_CLIENT_CLOSE_CODES.heartbeatUnavailable, "heartbeat unavailable");
1616
+ return;
1617
+ }
1618
+ if (error instanceof RealtimeRequestTimeout) {
1619
+ socket?.close(REALTIME_CLIENT_CLOSE_CODES.heartbeatTimeout, "heartbeat timeout");
1620
+ return;
1621
+ }
1622
+ if (error instanceof RealtimeRequestError) {
1623
+ socket?.close(REALTIME_CLIENT_CLOSE_CODES.heartbeatRejected, "heartbeat rejected");
1624
+ return;
1625
+ }
1626
+ socket?.close();
1627
+ });
1628
+ }, delayMs);
1418
1629
  }
1419
1630
  rejectPending(error) {
1420
1631
  for (const pending of this.pending.values()) {
@@ -1426,7 +1637,8 @@ var RealtimeClient = class {
1426
1637
  setState(state) {
1427
1638
  if (this.stateValue === state) return;
1428
1639
  this.stateValue = state;
1429
- for (const listener of this.stateListeners) listener(state);
1640
+ if (state === "disconnected") this.setHeartbeatLatency(null);
1641
+ notifyListeners(this.stateListeners, (listener) => listener(state), `connection state ${state}`);
1430
1642
  }
1431
1643
  clearReconnect() {
1432
1644
  if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
@@ -1438,6 +1650,6 @@ var RealtimeClient = class {
1438
1650
  }
1439
1651
  };
1440
1652
  //#endregion
1441
- export { REALTIME_JSON_SUBPROTOCOL, REALTIME_SUBPROTOCOL, RealtimeClient, RealtimeRequestError };
1653
+ export { REALTIME_JSON_SUBPROTOCOL, RealtimeClient, RealtimeRequestError };
1442
1654
 
1443
1655
  //# sourceMappingURL=realtime.js.map