@lootlog/sdk 0.1.0 → 0.2.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 (38) hide show
  1. package/README.md +261 -40
  2. package/dist/activity.d.ts +9 -2
  3. package/dist/activity.d.ts.map +1 -1
  4. package/dist/activity.js +2 -2
  5. package/dist/activity.js.map +1 -1
  6. package/dist/battlelog.d.ts +280 -5
  7. package/dist/battlelog.d.ts.map +1 -1
  8. package/dist/battlelog.js +11 -6
  9. package/dist/battlelog.js.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/main.d.ts +1457 -918
  13. package/dist/main.d.ts.map +1 -1
  14. package/dist/main.js +400 -284
  15. package/dist/main.js.map +1 -1
  16. package/dist/{mutators-Da_vMUva.d.ts → mutators-BaZDDfwh.d.ts} +2 -2
  17. package/dist/{mutators-Da_vMUva.d.ts.map → mutators-BaZDDfwh.d.ts.map} +1 -1
  18. package/dist/{mutators-CaBTxpi-.js → mutators-Dj5qBH3y.js} +2 -2
  19. package/dist/{mutators-CaBTxpi-.js.map → mutators-Dj5qBH3y.js.map} +1 -1
  20. package/dist/realtime.d.ts +427 -12
  21. package/dist/realtime.d.ts.map +1 -1
  22. package/dist/realtime.js +768 -471
  23. package/dist/realtime.js.map +1 -1
  24. package/dist/search.d.ts +247 -5
  25. package/dist/search.d.ts.map +1 -1
  26. package/dist/search.js +115 -2
  27. package/dist/search.js.map +1 -1
  28. package/dist/{transport-CIfewyM7.js → transport-Cn1V6y-o.js} +46 -8
  29. package/dist/transport-Cn1V6y-o.js.map +1 -0
  30. package/dist/{transport-BHbtsYo_.d.ts → transport-CoTymLxF.d.ts} +3 -1
  31. package/dist/transport-CoTymLxF.d.ts.map +1 -0
  32. package/openapi/activity.json +16 -1
  33. package/openapi/battlelog.json +805 -14
  34. package/openapi/main.json +5752 -4060
  35. package/openapi/search.json +721 -53
  36. package/package.json +4 -4
  37. package/dist/transport-BHbtsYo_.d.ts.map +0 -1
  38. package/dist/transport-CIfewyM7.js.map +0 -1
package/dist/realtime.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Result, Schema, SchemaTransformation } from "effect";
2
2
  import * as Schema$1 from "effect/Schema";
3
3
  import { TaggedError } from "effect/Schema";
4
+ import * as SchemaTransformation$1 from "effect/SchemaTransformation";
4
5
  import { decode, encode } from "@msgpack/msgpack";
5
6
  //#region ../schema/src/npc-type.ts
6
7
  const NpcTypeEnum = {
@@ -12,6 +13,7 @@ const NpcTypeEnum = {
12
13
  EVENT_HERO: "EVENT_HERO",
13
14
  COLOSSUS: "COLOSSUS",
14
15
  TITAN: "TITAN",
16
+ HERO_TRACKER: "HERO_TRACKER",
15
17
  NPC: "NPC"
16
18
  };
17
19
  const NpcTypeSchema = Schema.Literals([
@@ -23,6 +25,7 @@ const NpcTypeSchema = Schema.Literals([
23
25
  NpcTypeEnum.EVENT_HERO,
24
26
  NpcTypeEnum.COLOSSUS,
25
27
  NpcTypeEnum.TITAN,
28
+ NpcTypeEnum.HERO_TRACKER,
26
29
  NpcTypeEnum.NPC
27
30
  ]);
28
31
  //#endregion
@@ -139,7 +142,7 @@ const NPC_PERMISSIONS = {
139
142
  Object.values(NPC_PERMISSIONS).flat();
140
143
  new Set(NPC_PERMISSIONS.loots);
141
144
  Object.values(Permission).sort();
142
- new Set(Object.values(NpcTypeEnum));
145
+ Schema.is(NpcTypeSchema);
143
146
  //#endregion
144
147
  //#region ../schema/src/item-rarity.ts
145
148
  const ItemRarityEnum = {
@@ -161,7 +164,7 @@ const LootSourceEnum = {
161
164
  DIALOG: "DIALOG",
162
165
  FIGHT: "FIGHT"
163
166
  };
164
- Schema.Literals([
167
+ const LootSourceSchema = Schema.Literals([
165
168
  LootSourceEnum.LOOTBOX,
166
169
  LootSourceEnum.DIALOG,
167
170
  LootSourceEnum.FIGHT
@@ -263,18 +266,53 @@ Schema.Literals([
263
266
  //#endregion
264
267
  //#region ../schema/src/http-scalars.ts
265
268
  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" }));
269
+ 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}` }));
270
+ 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}))$`;
271
+ /** Strict ISO-8601 UTC date-time pattern shared by HTTP and domain schemas. */
272
+ const DATE_TIME_STRING_PATTERN = dateTimePattern("Z");
273
+ const DateTimeString = dateTimeString(DATE_TIME_STRING_PATTERN);
274
+ dateTimeString(dateTimePattern("Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)"));
275
+ const NonEmptyString = Schema$1.String.check(Schema$1.isMinLength(1).annotate({ expected: "a value with a length of at least 1" }));
270
276
  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" }));
277
+ 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}` }));
278
+ boundedInteger(-9007199254740991, 9007199254740991);
279
+ const PositiveSafeInteger = boundedInteger(1, 9007199254740991);
280
+ const NonNegativeSafeInteger = boundedInteger(0, 9007199254740991);
281
+ const LevelFilter = boundedInteger(0, 500);
282
+ boundedInteger(1, 100);
283
+ /** Rejects a level filter whose lower bound exceeds its upper bound. */
284
+ const levelRangeCheck = (minKey, maxKey) => Schema$1.makeFilter((data) => {
285
+ const min = data[minKey];
286
+ const max = data[maxKey];
287
+ return min === void 0 || max === void 0 || min <= max ? void 0 : {
288
+ path: [minKey],
289
+ issue: `${minKey} must be <= ${maxKey}`
290
+ };
291
+ });
292
+ Schema$1.optionalKey(LevelFilter), Schema$1.optionalKey(LevelFilter), levelRangeCheck("npcLevelMin", "npcLevelMax");
293
+ Schema$1.Array(NpcTypeSchema);
294
+ Schema$1.Literals([
295
+ "24h",
296
+ "3d",
297
+ "7d",
298
+ "14d",
299
+ "30d",
300
+ "90d",
301
+ "180d",
302
+ "all"
303
+ ]).annotate({ default: "7d" });
304
+ 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" }));
305
+ Schema$1.String.check(Schema$1.isMaxLength(100)).pipe(Schema$1.decode(SchemaTransformation$1.trim()));
306
+ Schema$1.String.check(Schema$1.isMinLength(1), Schema$1.isMaxLength(512));
276
307
  //#endregion
277
308
  //#region ../protocol/src/loot-summary.ts
309
+ const MapPlayersSnapshot = Schema.Array(Schema.Struct({
310
+ accountId: PositiveSafeInteger,
311
+ characterId: PositiveSafeInteger,
312
+ name: NonEmptyString,
313
+ prof: Schema.NullOr(ProfessionSchema),
314
+ icon: Schema.NullOr(Schema.String)
315
+ })).check(Schema.isMinLength(1));
278
316
  const LootItemResponse = Schema.Struct({
279
317
  id: FiniteNumber,
280
318
  hid: Schema.String,
@@ -314,6 +352,37 @@ const LootSummary = Schema.Struct({
314
352
  lootShare: LootShareResponse,
315
353
  location: Schema.String
316
354
  });
355
+ /** Same shape as the HTTP `MemberSummary`. */
356
+ const LootMember = Schema.Struct({
357
+ id: Schema.Number,
358
+ name: Schema.String,
359
+ avatar: Schema.NullOr(Schema.String),
360
+ userId: Schema.String
361
+ });
362
+ const LootSubmission = Schema.Struct({
363
+ guildId: Schema.String,
364
+ memberId: Schema.Number,
365
+ lootId: Schema.Number,
366
+ member: LootMember
367
+ });
368
+ /** One Organization's view of a loot, as its HTTP list and detail responses encode it. */
369
+ const LootSnapshot = Schema.Struct({
370
+ id: Schema.Number,
371
+ uniqueId: Schema.String,
372
+ world: Schema.String,
373
+ source: LootSourceSchema,
374
+ location: Schema.String,
375
+ items: Schema.Array(LootItemResponse),
376
+ players: Schema.Array(LootPlayerResponse),
377
+ mapPlayersSnapshot: Schema.NullOr(MapPlayersSnapshot),
378
+ npcs: Schema.Array(LootNpcResponse),
379
+ lootShare: LootShareResponse,
380
+ createdAt: Schema.String,
381
+ updatedAt: Schema.String,
382
+ submissions: Schema.optionalKey(Schema.Array(LootSubmission)),
383
+ commentsCount: Schema.Number,
384
+ likes: Schema.optionalKey(Schema.Array(LootMember))
385
+ });
317
386
  //#endregion
318
387
  //#region ../protocol/src/feed.ts
319
388
  const FeedNpc = Schema.Struct({
@@ -360,9 +429,204 @@ const UserFeedItem = Schema.Union([Schema.Struct({
360
429
  })]);
361
430
  //#endregion
362
431
  //#region ../schema/src/primitives.ts
363
- const NonEmptyString = Schema.NonEmptyString;
364
432
  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));
433
+ const isoDateTimePattern = new RegExp(DATE_TIME_STRING_PATTERN, "u");
434
+ Schema.String.annotate({ format: "date-time" }).pipe(Schema.check(Schema.isPattern(isoDateTimePattern)), Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromString));
435
+ //#endregion
436
+ //#region ../schema/src/party-ready-room.ts
437
+ const PARTY_READY_ROOM_STATUSES = ["ACTIVE", "CANCELLED"];
438
+ const PARTY_READY_ROOM_PARTY_PRESENCE_STATES = ["OUTSIDE", "IN_PARTY"];
439
+ const PartyReadyRoomClanSchema = Schema.Struct({
440
+ id: Schema.optionalKey(Schema.Number),
441
+ name: Schema.optionalKey(Schema.String)
442
+ });
443
+ const PartyReadyRoomCharacterSchema = Schema.Struct({
444
+ accountId: Schema.String,
445
+ characterId: Schema.String,
446
+ icon: Schema.String,
447
+ lvl: Schema.Number,
448
+ nick: Schema.String,
449
+ prof: Schema.String,
450
+ clan: Schema.optionalKey(PartyReadyRoomClanSchema)
451
+ });
452
+ const PartyReadyRoomParticipantSchema = Schema.Struct({
453
+ participantId: Schema.String,
454
+ discordId: Schema.String,
455
+ character: PartyReadyRoomCharacterSchema,
456
+ partyPresence: Schema.Literals(PARTY_READY_ROOM_PARTY_PRESENCE_STATES),
457
+ createdAt: Schema.String,
458
+ updatedAt: Schema.String
459
+ });
460
+ const PartyGatheringNpcSchema = Schema.Struct({
461
+ prof: Schema.optionalKey(Schema.String),
462
+ icon: Schema.optionalKey(Schema.String),
463
+ name: Schema.String,
464
+ location: Schema.String,
465
+ lvl: Schema.Number,
466
+ type: Schema.String,
467
+ x: Schema.optionalKey(Schema.Number),
468
+ y: Schema.optionalKey(Schema.Number)
469
+ });
470
+ const PartyGatheringCharacterSchema = Schema.Struct({
471
+ characterId: NonEmptyString.check(Schema.isMaxLength(255)),
472
+ nick: Schema.String.check(Schema.isMaxLength(255)),
473
+ icon: Schema.String.check(Schema.isMaxLength(2048)),
474
+ lvl: FiniteNumber,
475
+ prof: Schema.String.check(Schema.isMaxLength(100))
476
+ });
477
+ const PartyGatheringPartyMemberSchema = Schema.Struct({
478
+ characterId: PartyGatheringCharacterSchema.fields.characterId,
479
+ nick: Schema.optionalKey(PartyGatheringCharacterSchema.fields.nick),
480
+ icon: Schema.optionalKey(PartyGatheringCharacterSchema.fields.icon),
481
+ lvl: Schema.optionalKey(PartyGatheringCharacterSchema.fields.lvl),
482
+ prof: Schema.optionalKey(PartyGatheringCharacterSchema.fields.prof)
483
+ });
484
+ const PartyGatheringPartyStateSchema = Schema.Union([Schema.Struct({ status: Schema.Literal("UNKNOWN") }), Schema.Struct({
485
+ status: Schema.Literal("OBSERVED"),
486
+ observedAt: DateTimeString,
487
+ members: Schema.Array(PartyGatheringPartyMemberSchema).check(Schema.isMaxLength(20))
488
+ })]);
489
+ const PartyGatheringVolunteerSchema = Schema.Struct({
490
+ ...PartyGatheringCharacterSchema.fields,
491
+ partyPresence: Schema.Literals(PARTY_READY_ROOM_PARTY_PRESENCE_STATES)
492
+ });
493
+ const PartyGatheringSummarySchema = Schema.Struct({
494
+ notificationId: Schema.String,
495
+ organizerName: Schema.String,
496
+ organizerDiscordId: Schema.optionalKey(Schema.String),
497
+ organizerLvl: Schema.optionalKey(FiniteNumber),
498
+ organizerProf: Schema.optionalKey(Schema.String),
499
+ applicantCount: NonNegativeSafeInteger,
500
+ inPartyCount: NonNegativeSafeInteger,
501
+ partyMemberCount: Schema.optionalKey(NonNegativeSafeInteger),
502
+ revision: Schema.optionalKey(PositiveSafeInteger),
503
+ volunteers: Schema.optionalKey(Schema.Array(PartyGatheringVolunteerSchema)),
504
+ partyState: Schema.optionalKey(PartyGatheringPartyStateSchema),
505
+ guildIds: Schema.Array(Schema.String),
506
+ world: Schema.String,
507
+ description: Schema.optionalKey(Schema.String),
508
+ minLvl: Schema.optionalKey(FiniteNumber),
509
+ maxLvl: Schema.optionalKey(FiniteNumber),
510
+ npc: Schema.optionalKey(Schema.Struct({
511
+ ...PartyGatheringNpcSchema.fields,
512
+ type: Schema.optionalKey(Schema.String)
513
+ })),
514
+ createdAt: DateTimeString,
515
+ expiresAt: DateTimeString
516
+ });
517
+ const PartyGatheringClientUpdateSchema = Schema.Union([Schema.Struct({
518
+ type: Schema.Literal("UPSERT"),
519
+ gathering: PartyGatheringSummarySchema
520
+ }), Schema.Struct({
521
+ type: Schema.Literal("REMOVE"),
522
+ notificationId: Schema.String,
523
+ revision: PositiveSafeInteger
524
+ })]);
525
+ Schema.Struct({
526
+ guildId: Schema.String,
527
+ guildIds: Schema.Array(Schema.String),
528
+ organizerDiscordId: Schema.String,
529
+ npc: Schema.optionalKey(PartyGatheringNpcSchema),
530
+ world: Schema.String,
531
+ notificationId: Schema.String,
532
+ revision: PositiveSafeInteger,
533
+ update: PartyGatheringClientUpdateSchema
534
+ });
535
+ const PartyReadyRoomAggregateSchema = Schema.Struct({
536
+ schemaVersion: Schema.Literal(3),
537
+ npc: Schema.optionalKey(PartyGatheringNpcSchema),
538
+ notificationId: Schema.String,
539
+ organizerDiscordId: Schema.String,
540
+ organizerCharacter: PartyReadyRoomCharacterSchema,
541
+ guildIds: Schema.mutable(Schema.Array(Schema.String)),
542
+ world: Schema.String,
543
+ description: Schema.optionalKey(Schema.String),
544
+ minLvl: Schema.optionalKey(Schema.Number),
545
+ maxLvl: Schema.optionalKey(Schema.Number),
546
+ partyMemberCount: Schema.optionalKey(NonNegativeSafeInteger),
547
+ partyState: Schema.optionalKey(PartyGatheringPartyStateSchema),
548
+ status: Schema.Literals(PARTY_READY_ROOM_STATUSES),
549
+ revision: Schema.Number,
550
+ createdAt: Schema.String,
551
+ updatedAt: Schema.String,
552
+ expiresAt: Schema.String,
553
+ participants: Schema.Record(Schema.String, PartyReadyRoomParticipantSchema)
554
+ });
555
+ const activeProjectionFields = {
556
+ volunteers: Schema.optionalKey(Schema.Array(PartyGatheringVolunteerSchema)),
557
+ ...PartyReadyRoomAggregateSchema.fields,
558
+ participants: Schema.Record(Schema.String, Schema.mutableKey(PartyReadyRoomParticipantSchema)),
559
+ status: Schema.Literal("ACTIVE")
560
+ };
561
+ const PartyReadyRoomOrganizerProjectionSchema = Schema.Struct({
562
+ ...activeProjectionFields,
563
+ viewer: Schema.Literal("ORGANIZER"),
564
+ ownedParticipantIds: Schema.mutable(Schema.Array(Schema.String))
565
+ });
566
+ const PartyReadyRoomParticipantProjectionSchema = Schema.Struct({
567
+ ...activeProjectionFields,
568
+ viewer: Schema.Literal("PARTICIPANT")
569
+ });
570
+ const PartyReadyRoomProjectionSchema = Schema.Union([PartyReadyRoomOrganizerProjectionSchema, PartyReadyRoomParticipantProjectionSchema]);
571
+ Schema.decodeUnknownSync(PartyReadyRoomProjectionSchema);
572
+ const PartyReadyRoomUpsertUpdateSchema = Schema.Struct({
573
+ schemaVersion: Schema.Literal(3),
574
+ type: Schema.Literal("UPSERT"),
575
+ projection: PartyReadyRoomProjectionSchema
576
+ });
577
+ const PartyReadyRoomRemoveUpdateSchema = Schema.Struct({
578
+ schemaVersion: Schema.Literal(3),
579
+ type: Schema.Literal("REMOVE"),
580
+ notificationId: Schema.String,
581
+ revision: Schema.Number
582
+ });
583
+ const PartyReadyRoomClientUpdateSchema = Schema.Union([PartyReadyRoomUpsertUpdateSchema, PartyReadyRoomRemoveUpdateSchema]);
584
+ Schema.decodeUnknownSync(PartyReadyRoomClientUpdateSchema);
585
+ const GlobalChatMessageText = Schema.String.check(Schema.isTrimmed(), Schema.isMinLength(1), Schema.isMaxLength(128));
586
+ /**
587
+ * A Margonem world with its own global chat channel. Wherever a channel is
588
+ * named, an absent world means the channel every world shares.
589
+ */
590
+ const GlobalChatWorld = Schema.NonEmptyString.check(Schema.isMaxLength(64));
591
+ /**
592
+ * A global chat message as every Member sees it. It crosses Organization
593
+ * boundaries, so it names the sender only by their Discord display name and
594
+ * never carries a User, Discord, Organization or character identifier.
595
+ *
596
+ * `isAdmin` and the worlds are optional only for messages published before
597
+ * channels existed; the API sets `isAdmin` on every message it stores.
598
+ */
599
+ const GlobalChatMessageSchema = Schema.Struct({
600
+ id: Schema.NonEmptyString,
601
+ displayName: Schema.NonEmptyString,
602
+ message: GlobalChatMessageText,
603
+ timestamp: DateTimeString,
604
+ /** The channel's world; absent in the channel every world shares. */
605
+ world: Schema.optionalKey(GlobalChatWorld),
606
+ /** In the shared channel only: the world the sender wrote from. */
607
+ originWorld: Schema.optionalKey(GlobalChatWorld),
608
+ isAdmin: Schema.optionalKey(Schema.Boolean)
609
+ });
610
+ Schema.Union([Schema.Struct({
611
+ type: Schema.Literal("deleted"),
612
+ world: Schema.optionalKey(GlobalChatWorld),
613
+ id: Schema.NonEmptyString
614
+ }), Schema.Struct({
615
+ type: Schema.Literal("pinned"),
616
+ world: Schema.optionalKey(GlobalChatWorld),
617
+ message: Schema.NullOr(GlobalChatMessageSchema)
618
+ })]);
619
+ /** Live counts the gateway sends to a channel's subscribers. */
620
+ const GlobalChatStatsSchema = Schema.Struct({
621
+ world: Schema.optionalKey(GlobalChatWorld),
622
+ /**
623
+ * Unique Users with a live Game client across every Organization: on the
624
+ * channel's world, or on any world in the shared channel.
625
+ */
626
+ online: NonNegativeInt,
627
+ /** Unique Users following this channel. */
628
+ listeners: NonNegativeInt
629
+ });
366
630
  //#endregion
367
631
  //#region ../schema/src/air-tag.ts
368
632
  const AIR_TAG_RELATIONS = [
@@ -386,6 +650,11 @@ const Coordinate = Schema.Int.check(Schema.isBetween({
386
650
  minimum: 0,
387
651
  maximum: AIR_TAG_MAX_COORDINATE
388
652
  }));
653
+ const Level = Schema.Int.check(Schema.isBetween({
654
+ minimum: 0,
655
+ maximum: 1e4
656
+ }));
657
+ const Profession = Schema.NonEmptyString.check(Schema.isMaxLength(1));
389
658
  const AirTagRelationSchema = Schema.Literals(AIR_TAG_RELATIONS);
390
659
  const AirTagClanSchema = Schema.Struct({
391
660
  id: SafeNatural,
@@ -397,24 +666,22 @@ const AirTagObservationSchema = Schema.Struct({
397
666
  clan: Schema.optionalKey(AirTagClanSchema),
398
667
  relation: AirTagRelationSchema,
399
668
  x: Coordinate,
400
- y: Coordinate
669
+ y: Coordinate,
670
+ lvl: Schema.optionalKey(Level),
671
+ prof: Schema.optionalKey(Profession),
672
+ stasis: Schema.optionalKey(Schema.Boolean)
673
+ });
674
+ const AirTagDepartureSchema = Schema.Struct({
675
+ targetId: ShortString,
676
+ reason: Schema.Literals(["left-map", "out-of-sight"])
401
677
  });
402
678
  const AirTagObservationBatchSchema = Schema.Struct({
403
679
  expectedMapId: Coordinate,
404
- observations: Schema.Array(AirTagObservationSchema)
405
- });
406
- Schema.Struct({
407
- requestId: ShortString,
408
- enabled: Schema.Boolean,
409
- expectedMapId: Schema.optionalKey(Coordinate)
680
+ observations: Schema.Array(AirTagObservationSchema),
681
+ departures: Schema.optionalKey(Schema.Array(AirTagDepartureSchema))
410
682
  });
411
683
  const AirTagTargetSchema = Schema.Struct({
412
- targetId: ShortString,
413
- nickname: ShortString,
414
- clan: Schema.optionalKey(AirTagClanSchema),
415
- relation: AirTagRelationSchema,
416
- x: Coordinate,
417
- y: Coordinate,
684
+ ...AirTagObservationSchema.fields,
418
685
  observedAt: SafeNatural,
419
686
  enemyObservedAt: Schema.optionalKey(SafeNatural),
420
687
  clanEnemyObservedAt: Schema.optionalKey(SafeNatural)
@@ -429,23 +696,220 @@ const AirTagScopeIdentityFields = {
429
696
  };
430
697
  const AirTagScopeSnapshotSchema = Schema.Struct({
431
698
  ...AirTagScopeIdentityFields,
432
- targets: Schema.Array(AirTagTargetSchema)
699
+ targets: Schema.Array(AirTagTargetSchema),
700
+ serverTime: Schema.optionalKey(SafeNatural)
433
701
  });
434
702
  const AirTagUpdateEventSchema = Schema.Struct({
435
703
  ...AirTagScopeIdentityFields,
436
704
  target: AirTagTargetSchema
437
705
  });
438
- Schema.is(AirTagRelationSchema);
706
+ const AirTagScopeUpdateEventSchema = Schema.Struct({
707
+ ...AirTagScopeIdentityFields,
708
+ targets: Schema.Array(AirTagTargetSchema),
709
+ removedTargetIds: Schema.Array(ShortString)
710
+ });
711
+ const AirTagMapThreatEventSchema = Schema.Struct({
712
+ guildId: GuildId,
713
+ world: ShortString,
714
+ mapId: Coordinate,
715
+ mapName: Schema.NonEmptyString.check(Schema.isMaxLength(128)),
716
+ revision: SafeNatural,
717
+ enemies: Schema.Array(Schema.Struct({
718
+ targetId: AirTagObservationSchema.fields.targetId,
719
+ nickname: AirTagObservationSchema.fields.nickname,
720
+ clan: AirTagObservationSchema.fields.clan,
721
+ lvl: AirTagObservationSchema.fields.lvl,
722
+ prof: AirTagObservationSchema.fields.prof,
723
+ stasis: AirTagObservationSchema.fields.stasis,
724
+ ageMs: SafeNatural
725
+ }))
726
+ });
439
727
  Schema.is(AirTagObservationSchema);
440
- Schema.is(AirTagTargetSchema);
728
+ Schema.is(AirTagRelationSchema);
441
729
  Schema.is(AirTagScopeSnapshotSchema);
442
730
  Schema.is(AirTagUpdateEventSchema);
731
+ Schema.is(AirTagScopeUpdateEventSchema);
732
+ Schema.is(AirTagDepartureSchema);
733
+ //#endregion
734
+ //#region ../schema/src/battle-ping.ts
735
+ /** Pings placed on an enemy warrior. `attack` is the team's shared target. */
736
+ const BATTLE_ENEMY_PING_TYPES = [
737
+ "attack",
738
+ "caution",
739
+ "spare"
740
+ ];
741
+ /** Requests placed on an allied warrior (or on the sender's own warrior). */
742
+ const BATTLE_REQUEST_PING_TYPES = [
743
+ "attack-speed-aura",
744
+ "atmo",
745
+ "cleanse",
746
+ "emanating-arrow",
747
+ "fury",
748
+ "heal",
749
+ "healing-wave",
750
+ "need-heal",
751
+ "poison",
752
+ "protection-aura",
753
+ "rime",
754
+ "stigma",
755
+ "taunt"
756
+ ];
757
+ /**
758
+ * Calls to the whole battle team, sent from the sender's own warrior. Only
759
+ * sockets that negotiated `lootlog.battle-ping.team.v1` send or receive them.
760
+ */
761
+ const BATTLE_TEAM_PING_TYPES = ["quick-fight"];
762
+ const BATTLE_PING_TYPES = [
763
+ ...BATTLE_ENEMY_PING_TYPES,
764
+ ...BATTLE_REQUEST_PING_TYPES,
765
+ ...BATTLE_TEAM_PING_TYPES
766
+ ];
767
+ /** A Margonem party holds at most ten characters, so nine other recipients. */
768
+ const MAX_BATTLE_PING_RECIPIENTS = 9;
769
+ /** Margonem battle warrior ids: positive for characters, negative for NPCs. */
770
+ const MAX_BATTLE_WARRIOR_ID = 2147483647;
771
+ const BattlePingTypeSchema = Schema.Literals(BATTLE_PING_TYPES);
772
+ const BattleWarriorIdSchema = Schema.Int.check(Schema.isBetween({
773
+ minimum: -2147483647,
774
+ maximum: MAX_BATTLE_WARRIOR_ID
775
+ }), Schema.makeFilter((id) => id !== 0 || "warrior id must not be 0"));
776
+ const CharacterIdSchema$1 = Schema.String.check(Schema.isPattern(/^[1-9]\d{0,9}$/u));
777
+ Schema.is(BattlePingTypeSchema);
778
+ Schema.is(Schema.Literals(BATTLE_ENEMY_PING_TYPES));
779
+ Schema.is(Schema.Literals(BATTLE_TEAM_PING_TYPES));
780
+ const BattlePingSendPayloadSchema = Schema.Struct({
781
+ expectedMapId: NonNegativeInt,
782
+ type: BattlePingTypeSchema,
783
+ warriorId: BattleWarriorIdSchema,
784
+ recipientCharacterIds: Schema.Array(CharacterIdSchema$1).check(Schema.isMinLength(1), Schema.isMaxLength(MAX_BATTLE_PING_RECIPIENTS))
785
+ });
786
+ const BattlePingEventSchema = Schema.Struct({
787
+ pingId: Schema.NonEmptyString,
788
+ world: Schema.NonEmptyString,
789
+ mapId: NonNegativeInt,
790
+ type: BattlePingTypeSchema,
791
+ warriorId: BattleWarriorIdSchema,
792
+ sender: Schema.Struct({
793
+ characterId: Schema.NonEmptyString,
794
+ name: Schema.NonEmptyString
795
+ }),
796
+ createdAt: NonNegativeInt
797
+ });
798
+ //#endregion
799
+ //#region ../schema/src/loot-split.ts
800
+ /**
801
+ * A participant's choice for one item of a group loot split, as Margonem's
802
+ * `loot.states` reports it: 0 "nie chcę", 1 "chcę", 2 "bardzo chcę".
803
+ */
804
+ const LOOT_SPLIT_CHOICES = [
805
+ 0,
806
+ 1,
807
+ 2
808
+ ];
809
+ /** A Margonem party holds at most ten characters, so nine other recipients. */
810
+ const MAX_LOOT_SPLIT_RECIPIENTS = 9;
811
+ /** Bounds a frame; one group loot holds far fewer items. */
812
+ const MAX_LOOT_SPLIT_ITEMS = 64;
813
+ const ITEM_ID_PATTERN = /^[1-9]\d{0,17}$/u;
814
+ const ItemIdSchema = Schema.String.check(Schema.isPattern(ITEM_ID_PATTERN));
815
+ const CharacterIdSchema = Schema.String.check(Schema.isPattern(/^[1-9]\d{0,9}$/u));
816
+ const RecipientCharacterIdsSchema = Schema.Array(CharacterIdSchema).check(Schema.isMinLength(1), Schema.isMaxLength(MAX_LOOT_SPLIT_RECIPIENTS));
817
+ const SenderSchema = Schema.Struct({
818
+ characterId: Schema.NonEmptyString,
819
+ name: Schema.NonEmptyString
820
+ });
821
+ const LootSplitChoicesSchema = Schema.Record(Schema.String, Schema.Literals(LOOT_SPLIT_CHOICES)).check(Schema.makeFilter((choices) => {
822
+ const ids = Object.keys(choices);
823
+ return ids.length <= MAX_LOOT_SPLIT_ITEMS && ids.every((id) => ITEM_ID_PATTERN.test(id)) || "choices must map at most 64 item ids";
824
+ }));
825
+ const LootSplitSendPayloadSchema = Schema.Struct({
826
+ splitId: ItemIdSchema,
827
+ choices: LootSplitChoicesSchema,
828
+ accepted: Schema.Boolean,
829
+ recipientCharacterIds: RecipientCharacterIdsSchema
830
+ });
831
+ const LootSplitEventSchema = Schema.Struct({
832
+ splitId: ItemIdSchema,
833
+ world: Schema.NonEmptyString,
834
+ sender: SenderSchema,
835
+ choices: LootSplitChoicesSchema,
836
+ accepted: Schema.Boolean,
837
+ sentAt: NonNegativeInt
838
+ });
839
+ const LootSplitAutoAcceptPayloadSchema = Schema.Struct({ recipientCharacterIds: RecipientCharacterIdsSchema });
840
+ const LootSplitAutoAcceptedEventSchema = Schema.Struct({
841
+ world: Schema.NonEmptyString,
842
+ sender: SenderSchema,
843
+ sentAt: NonNegativeInt
844
+ });
845
+ const LootSplitAckSchema = Schema.Union([Schema.Struct({ status: Schema.Literal("accepted") }), Schema.Struct({
846
+ status: Schema.Literal("rejected"),
847
+ code: Schema.Literals([
848
+ "forbidden",
849
+ "invalid-context",
850
+ "invalid-payload",
851
+ "rate-limited",
852
+ "temporarily-unavailable"
853
+ ]),
854
+ retryAfterMs: Schema.optionalKey(NonNegativeInt)
855
+ })]);
856
+ /** The fields organization NPC visibility needs, keyed by the NPC's runtime id. */
857
+ const NpcPresenceNpcSchema = Schema.Struct({
858
+ id: NonNegativeInt,
859
+ lvl: NonNegativeInt,
860
+ wt: NonNegativeInt,
861
+ prof: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(8))),
862
+ type: Schema.optionalKey(NonNegativeInt)
863
+ });
864
+ /** Every NPC a character currently sees on its map; each report replaces the previous one. */
865
+ const NpcPresenceReportSchema = Schema.Struct({
866
+ world: Schema.NonEmptyString.check(Schema.isMaxLength(64)),
867
+ organizationIds: Schema.Array(Schema.NonEmptyString).check(Schema.isMaxLength(20)),
868
+ npcs: Schema.Array(NpcPresenceNpcSchema).check(Schema.isMaxLength(50))
869
+ });
870
+ /** Sent only when an NPC gains its first reporter or loses its last one. */
871
+ const NpcPresenceEventSchema = Schema.Struct({
872
+ organizationId: Schema.NonEmptyString,
873
+ world: Schema.NonEmptyString,
874
+ /** Orders the updates of one Organization and world across gateway replicas. */
875
+ revision: NonNegativeInt,
876
+ npc: NpcPresenceNpcSchema,
877
+ standing: Schema.Boolean,
878
+ /** Gateway time the NPC was first reported. */
879
+ since: NonNegativeInt
880
+ });
881
+ const NpcPresenceSnapshotSchema = Schema.Struct({
882
+ revision: NonNegativeInt,
883
+ npcs: Schema.Array(Schema.Struct({
884
+ npc: NpcPresenceNpcSchema,
885
+ since: NonNegativeInt
886
+ }))
887
+ });
888
+ const NpcPresenceRejectCode = Schema.Literals([
889
+ "forbidden",
890
+ "invalid-context",
891
+ "invalid-payload",
892
+ "rate-limited",
893
+ "temporarily-unavailable"
894
+ ]);
895
+ const NpcPresenceReportAckSchema = Schema.Union([Schema.Struct({ status: Schema.Literal("accepted") }), Schema.Struct({
896
+ status: Schema.Literal("rejected"),
897
+ code: NpcPresenceRejectCode,
898
+ retryAfterMs: Schema.optional(NonNegativeInt)
899
+ })]);
900
+ //#endregion
901
+ //#region ../schema/src/map-ping.ts
443
902
  const MapPingTypeSchema = Schema.Literals([
444
903
  "attention",
445
904
  "enemy",
446
905
  "regroup",
447
906
  "avoid"
448
907
  ]);
908
+ /** Margonem NPC and character ids are positive 32-bit integers. */
909
+ const MapPingCharacterIdSchema = Schema.Int.check(Schema.isBetween({
910
+ minimum: 1,
911
+ maximum: 2147483647
912
+ }));
449
913
  const MapPingRejectCodeSchema = Schema.Literals([
450
914
  "forbidden",
451
915
  "invalid-context",
@@ -454,11 +918,16 @@ const MapPingRejectCodeSchema = Schema.Literals([
454
918
  "temporarily-unavailable"
455
919
  ]);
456
920
  Schema.is(MapPingTypeSchema);
457
- const MapPingSendPayloadSchema = Schema.Struct({
458
- expectedMapId: NonNegativeInt,
921
+ const mapPingFields = {
459
922
  type: MapPingTypeSchema,
460
923
  x: NonNegativeInt,
461
- y: NonNegativeInt
924
+ y: NonNegativeInt,
925
+ npcId: Schema.optionalKey(MapPingCharacterIdSchema),
926
+ playerId: Schema.optionalKey(MapPingCharacterIdSchema)
927
+ };
928
+ const MapPingSendPayloadSchema = Schema.Struct({
929
+ expectedMapId: NonNegativeInt,
930
+ ...mapPingFields
462
931
  });
463
932
  const MapPingAckSchema = Schema.Union([Schema.Struct({
464
933
  status: Schema.Literal("accepted"),
@@ -472,9 +941,7 @@ const MapPingEventSchema = Schema.Struct({
472
941
  pingId: Schema.NonEmptyString,
473
942
  world: Schema.NonEmptyString,
474
943
  mapId: NonNegativeInt,
475
- type: MapPingTypeSchema,
476
- x: NonNegativeInt,
477
- y: NonNegativeInt,
944
+ ...mapPingFields,
478
945
  sender: Schema.Struct({
479
946
  characterId: Schema.NonEmptyString,
480
947
  name: Schema.NonEmptyString
@@ -482,302 +949,24 @@ const MapPingEventSchema = Schema.Struct({
482
949
  createdAt: NonNegativeInt
483
950
  });
484
951
  //#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
- });
952
+ //#region ../protocol/src/loot-events.ts
754
953
  const GuildLootEventNpc = Schema.Struct({
755
- lvl: Schema.optional(NullableNumber),
756
- prof: Schema.optional(NullableString),
954
+ lvl: Schema.optional(Schema.NullOr(Schema.Number)),
955
+ prof: Schema.optional(Schema.NullOr(Schema.String)),
757
956
  type: Schema.optional(Schema.NullOr(Schema.Union([Schema.Number, Schema.String]))),
758
957
  wt: Schema.optional(Schema.NullOr(Schema.Union([Schema.Number, Schema.String])))
759
958
  });
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
959
  const GuildLootCreatedEventV2 = Schema.Struct({
775
960
  feedEntry: Schema.optional(UserFeedItem),
776
961
  version: Schema.Literal(2),
777
- guildId: NonEmptyString,
962
+ guildId: Schema.NonEmptyString,
778
963
  lootId: NonNegativeInt,
779
964
  npcs: Schema.Array(GuildLootEventNpc)
780
965
  });
966
+ const GuildLootCreatedMessageV2 = Schema.Struct({
967
+ ...GuildLootCreatedEventV2.fields,
968
+ loot: Schema.optional(LootSnapshot)
969
+ });
781
970
  const GuildLootShareUpdatedEventV2 = Schema.Struct({
782
971
  ...GuildLootCreatedEventV2.fields,
783
972
  lootShare: Schema.Record(Schema.String, Schema.Array(Schema.String))
@@ -790,105 +979,30 @@ const ReservationChangedEventV2 = Schema.Struct({
790
979
  "deleted",
791
980
  "sharing-changed"
792
981
  ]),
793
- sourceGuildId: NonEmptyString,
794
- audienceGuildIds: Schema.Array(NonEmptyString),
982
+ sourceGuildId: Schema.NonEmptyString,
983
+ audienceGuildIds: Schema.Array(Schema.NonEmptyString),
795
984
  reservationId: Schema.NullOr(NonNegativeInt),
796
985
  spotId: Schema.NullOr(Schema.String)
797
986
  });
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;
987
+ const { feedEntry: _feedEntry, ...lootCreatedFields } = GuildLootCreatedMessageV2.fields;
988
+ const LootSnapshotEvent = Schema.Struct(lootCreatedFields);
888
989
  const REALTIME_SUBPROTOCOL = "lootlog.realtime.v1";
889
990
  const REALTIME_JSON_SUBPROTOCOL = "lootlog.realtime.json.v1";
991
+ const REALTIME_CLIENT_CLOSE_CODES = {
992
+ heartbeatTimeout: 4001,
993
+ heartbeatRejected: 4002,
994
+ heartbeatUnavailable: 4003,
995
+ malformedFrame: 4007,
996
+ sessionJoinFailed: 4008
997
+ };
998
+ const REALTIME_SERVER_CLOSE_CODES = {
999
+ restarting: 1012,
1000
+ tryAgainLater: 1013
1001
+ };
1002
+ const REALTIME_SERVER_CLOSE_RECONNECT_WINDOW_MS = 3e4;
890
1003
  const PRESENCE_HEARTBEAT_INTERVAL_MS = 25e3;
891
- const RequestId = NonEmptyString;
1004
+ const PRESENCE_EXPIRY_MS = 6e4;
1005
+ const RequestId = Schema.NonEmptyString;
892
1006
  const Revision = NonNegativeInt;
893
1007
  const Timestamp = NonNegativeInt;
894
1008
  const RealtimeLogicalTopic = Schema.Literals([
@@ -903,13 +1017,15 @@ const RealtimeLogicalTopic = Schema.Literals([
903
1017
  "event.coordination",
904
1018
  "map.air-tags",
905
1019
  "map.pings",
906
- "party.ready-room"
1020
+ "party.ready-room",
1021
+ "global.chat",
1022
+ "global.chat.world"
907
1023
  ]);
908
1024
  const SubscriptionScope = Schema.Struct({
909
1025
  topic: RealtimeLogicalTopic,
910
- organizationId: Schema.optional(NonEmptyString),
911
- eventId: Schema.optional(NonEmptyString),
912
- world: Schema.optional(NonEmptyString),
1026
+ organizationId: Schema.optional(Schema.NonEmptyString),
1027
+ eventId: Schema.optional(Schema.NonEmptyString),
1028
+ world: Schema.optional(Schema.NonEmptyString),
913
1029
  mapId: Schema.optional(NonNegativeInt)
914
1030
  });
915
1031
  const PresencePlatform = Schema.Literals(["game", "web-app"]);
@@ -921,26 +1037,26 @@ const PresenceClan = Schema.Struct({
921
1037
  rank: Schema.optional(Schema.Int)
922
1038
  });
923
1039
  const PresenceCharacter = Schema.Struct({
924
- world: NonEmptyString,
925
- name: NonEmptyString,
1040
+ world: Schema.NonEmptyString,
1041
+ name: Schema.NonEmptyString,
926
1042
  lvl: NonNegativeInt,
927
1043
  icon: Schema.String,
928
- characterId: NonEmptyString,
929
- accountId: NonEmptyString,
1044
+ characterId: Schema.NonEmptyString,
1045
+ accountId: Schema.NonEmptyString,
930
1046
  prof: Schema.String,
931
1047
  clan: Schema.optional(PresenceClan)
932
1048
  });
933
1049
  const PrecisePresenceLocation = Schema.Struct({
934
1050
  mapId: Schema.optional(NonNegativeInt),
935
- map: NonEmptyString,
1051
+ map: Schema.NonEmptyString,
936
1052
  x: Schema.optional(NonNegativeInt),
937
1053
  y: Schema.optional(NonNegativeInt)
938
1054
  });
939
1055
  const BasicPresence = Schema.Struct({
940
- userId: NonEmptyString,
941
- discordId: Schema.optional(NonEmptyString),
942
- sessionId: NonEmptyString,
943
- organizationIds: Schema.Array(NonEmptyString),
1056
+ userId: Schema.NonEmptyString,
1057
+ discordId: Schema.optional(Schema.NonEmptyString),
1058
+ sessionId: Schema.NonEmptyString,
1059
+ organizationIds: Schema.Array(Schema.NonEmptyString),
944
1060
  platform: PresencePlatform,
945
1061
  status: PresenceStatus,
946
1062
  confidence: PresenceConfidence,
@@ -953,29 +1069,29 @@ const PresenceWithLocation = Schema.Struct({
953
1069
  location: PrecisePresenceLocation
954
1070
  });
955
1071
  const PublishedPresence = Schema.Struct({
956
- organizationIds: Schema.Array(NonEmptyString),
1072
+ organizationIds: Schema.Array(Schema.NonEmptyString),
957
1073
  isAfk: Schema.optional(Schema.Boolean),
958
1074
  character: Schema.optional(PresenceCharacter),
959
1075
  location: Schema.optional(PrecisePresenceLocation),
960
1076
  clientObservedAt: Schema.optional(Timestamp)
961
1077
  });
962
1078
  const PresenceSnapshot = Schema.Struct({
963
- organizationId: NonEmptyString,
964
- world: Schema.optional(NonEmptyString),
1079
+ organizationId: Schema.NonEmptyString,
1080
+ world: Schema.optional(Schema.NonEmptyString),
965
1081
  revision: Revision,
966
1082
  presences: Schema.Array(Schema.Union([PresenceWithLocation, BasicPresence]))
967
1083
  });
968
1084
  const PresenceDelta = Schema.Struct({
969
- organizationId: NonEmptyString,
1085
+ organizationId: Schema.NonEmptyString,
970
1086
  revision: Revision,
971
1087
  changes: Schema.Array(Schema.Union([Schema.Struct({
972
1088
  action: Schema.Literal("upsert"),
973
1089
  presence: Schema.Union([PresenceWithLocation, BasicPresence])
974
1090
  }), Schema.Struct({
975
1091
  action: Schema.Literal("remove"),
976
- userId: NonEmptyString,
977
- discordId: Schema.optional(NonEmptyString),
978
- sessionId: NonEmptyString
1092
+ userId: Schema.NonEmptyString,
1093
+ discordId: Schema.optional(Schema.NonEmptyString),
1094
+ sessionId: Schema.NonEmptyString
979
1095
  })]))
980
1096
  });
981
1097
  const command = (type, data) => Schema.Struct({
@@ -985,25 +1101,40 @@ const command = (type, data) => Schema.Struct({
985
1101
  data
986
1102
  });
987
1103
  const SessionJoinCommand = command("session.join", Schema.Struct({
988
- world: Schema.optional(NonEmptyString),
1104
+ world: Schema.optional(Schema.NonEmptyString),
989
1105
  character: Schema.optional(PresenceCharacter),
990
1106
  margonemAccountProof: Schema.optional(Schema.Unknown)
991
1107
  }));
992
- const HeartbeatCommand = command("presence.heartbeat", Schema.Struct({ sessionId: NonEmptyString }));
1108
+ const HeartbeatCommand = command("presence.heartbeat", Schema.Struct({ sessionId: Schema.NonEmptyString }));
993
1109
  const PresencePublishCommand = command("presence.publish", PublishedPresence);
1110
+ const PingCommand = command("connection.ping", Schema.Struct({}));
994
1111
  const PresenceFetchCommand = command("presence.fetch", Schema.Struct({
995
- organizationId: NonEmptyString,
996
- world: Schema.optional(NonEmptyString)
1112
+ organizationId: Schema.NonEmptyString,
1113
+ world: Schema.optional(Schema.NonEmptyString),
1114
+ delivery: Schema.optional(Schema.Literal("response"))
997
1115
  }));
998
1116
  const SubscribeCommand = command("subscription.subscribe", SubscriptionScope);
999
1117
  const UnsubscribeCommand = command("subscription.unsubscribe", SubscriptionScope);
1000
1118
  const MapPingCommand = command("map-ping.send", MapPingSendPayloadSchema);
1119
+ const BattlePingCommand = command("battle-ping.send", BattlePingSendPayloadSchema);
1120
+ const LootSplitCommand = command("loot-split.send", LootSplitSendPayloadSchema);
1121
+ const LootSplitAutoAcceptCommand = command("loot-split.auto-accept", LootSplitAutoAcceptPayloadSchema);
1001
1122
  const AirTagSubscriptionCommand = command("air-tag.subscription", Schema.Struct({
1002
1123
  requestId: RequestId,
1003
1124
  enabled: Schema.Boolean,
1004
1125
  expectedMapId: Schema.optional(NonNegativeInt)
1005
1126
  }));
1006
1127
  const AirTagObservationCommand = command("air-tag.observation", AirTagObservationBatchSchema);
1128
+ const AirTagMapThreatsFetchCommand = command("air-tag.map-threats.fetch", Schema.Struct({
1129
+ organizationId: Schema.NonEmptyString,
1130
+ world: Schema.NonEmptyString
1131
+ }));
1132
+ const AirTagMapThreatsFetchResponse = Schema.Struct({ threats: Schema.Array(AirTagMapThreatEventSchema) });
1133
+ const NpcPresenceReportCommand = command("npc-presence.report", NpcPresenceReportSchema);
1134
+ const NpcPresenceFetchCommand = command("npc-presence.fetch", Schema.Struct({
1135
+ organizationId: Schema.NonEmptyString,
1136
+ world: Schema.NonEmptyString
1137
+ }));
1007
1138
  const AirTagRejectCode = Schema.Literals([
1008
1139
  "forbidden",
1009
1140
  "invalid-context",
@@ -1032,17 +1163,24 @@ const AirTagObservationAck = Schema.Union([Schema.Struct({
1032
1163
  const ClientCommand = Schema.Union([
1033
1164
  SessionJoinCommand,
1034
1165
  HeartbeatCommand,
1166
+ PingCommand,
1035
1167
  PresencePublishCommand,
1036
1168
  PresenceFetchCommand,
1037
1169
  SubscribeCommand,
1038
1170
  UnsubscribeCommand,
1039
1171
  MapPingCommand,
1172
+ BattlePingCommand,
1173
+ LootSplitCommand,
1174
+ LootSplitAutoAcceptCommand,
1040
1175
  AirTagSubscriptionCommand,
1041
- AirTagObservationCommand
1176
+ AirTagObservationCommand,
1177
+ AirTagMapThreatsFetchCommand,
1178
+ NpcPresenceReportCommand,
1179
+ NpcPresenceFetchCommand
1042
1180
  ]);
1043
1181
  const RealtimeError = Schema.Struct({
1044
- code: NonEmptyString,
1045
- message: NonEmptyString,
1182
+ code: Schema.NonEmptyString,
1183
+ message: Schema.NonEmptyString,
1046
1184
  retryable: Schema.Boolean,
1047
1185
  retryAfterMs: Schema.optional(NonNegativeInt),
1048
1186
  details: Schema.optional(Schema.Record(Schema.String, Schema.Unknown))
@@ -1065,19 +1203,21 @@ const serverEvent = (type, data) => Schema.Struct({
1065
1203
  data
1066
1204
  });
1067
1205
  const OrganizationEvent = Schema.Struct({
1068
- organizationId: NonEmptyString,
1206
+ organizationId: Schema.NonEmptyString,
1069
1207
  payload: Schema.Unknown
1070
1208
  });
1071
1209
  const ServerEvent = Schema.Union([
1210
+ serverEvent("session.hello", Schema.Struct({ connectionId: Schema.NonEmptyString })),
1072
1211
  serverEvent("session.joined", Schema.Struct({
1073
- connectionId: NonEmptyString,
1074
- organizationIds: Schema.Array(NonEmptyString),
1212
+ connectionId: Schema.NonEmptyString,
1213
+ organizationIds: Schema.Array(Schema.NonEmptyString),
1075
1214
  subscriptionScopes: Schema.Array(SubscriptionScope),
1076
- accessPolicy: Schema.optional(AccessPolicySnapshot)
1215
+ accessPolicy: Schema.optional(AccessPolicySnapshot),
1216
+ capabilities: Schema.optional(Schema.Array(Schema.NonEmptyString))
1077
1217
  })),
1078
1218
  serverEvent("permissions.updated", Schema.Struct({
1079
1219
  changes: Schema.optional(Schema.Array(AccessPolicyChange)),
1080
- organizationIds: Schema.Array(NonEmptyString),
1220
+ organizationIds: Schema.Array(Schema.NonEmptyString),
1081
1221
  subscriptionScopes: Schema.Array(SubscriptionScope),
1082
1222
  accessPolicy: Schema.optional(AccessPolicySnapshot)
1083
1223
  })),
@@ -1087,12 +1227,24 @@ const ServerEvent = Schema.Union([
1087
1227
  serverEvent("chat.updated", OrganizationEvent),
1088
1228
  serverEvent("chat.deleted", OrganizationEvent),
1089
1229
  serverEvent("chat.cleared", OrganizationEvent),
1230
+ serverEvent("global-chat.created", GlobalChatMessageSchema),
1231
+ serverEvent("global-chat.deleted", Schema.Struct({
1232
+ world: Schema.optionalKey(GlobalChatWorld),
1233
+ id: Schema.NonEmptyString
1234
+ })),
1235
+ serverEvent("global-chat.pinned", Schema.Struct({
1236
+ world: Schema.optionalKey(GlobalChatWorld),
1237
+ message: Schema.NullOr(GlobalChatMessageSchema)
1238
+ })),
1239
+ serverEvent("global-chat.stats", GlobalChatStatsSchema),
1090
1240
  serverEvent("feed.entry", UserFeedItem),
1091
- serverEvent("kills.changed", Schema.Struct({ guildId: NonEmptyString })),
1241
+ serverEvent("kills.changed", Schema.Struct({ guildId: Schema.NonEmptyString })),
1092
1242
  serverEvent("loot.created", GuildLootCreatedEventV2),
1093
1243
  serverEvent("loot.share-updated", GuildLootShareUpdatedEventV2),
1244
+ serverEvent("loot.snapshot", LootSnapshotEvent),
1094
1245
  serverEvent("timer.created", OrganizationEvent),
1095
1246
  serverEvent("timer.deleted", OrganizationEvent),
1247
+ serverEvent("hero-sighting.updated", OrganizationEvent),
1096
1248
  serverEvent("reservation.created", OrganizationEvent),
1097
1249
  serverEvent("reservation.deleted", OrganizationEvent),
1098
1250
  serverEvent("reservation.changed", ReservationChangedEventV2),
@@ -1100,19 +1252,29 @@ const ServerEvent = Schema.Union([
1100
1252
  serverEvent("member-refresh.updated", OrganizationEvent),
1101
1253
  serverEvent("party-gathering.updated", OrganizationEvent),
1102
1254
  serverEvent("party-gathering.cancelled", OrganizationEvent),
1255
+ serverEvent("party-gathering.state-updated", Schema.Struct({
1256
+ organizationId: Schema.NonEmptyString,
1257
+ payload: PartyGatheringClientUpdateSchema
1258
+ })),
1103
1259
  serverEvent("party-ready-room.updated", OrganizationEvent),
1104
1260
  serverEvent("map-ping.received", MapPingEventSchema),
1261
+ serverEvent("battle-ping.received", BattlePingEventSchema),
1262
+ serverEvent("loot-split.received", LootSplitEventSchema),
1263
+ serverEvent("loot-split.auto-accepted", LootSplitAutoAcceptedEventSchema),
1105
1264
  serverEvent("air-tag.updated", AirTagUpdateEventSchema),
1265
+ serverEvent("air-tag.scope-updated", AirTagScopeUpdateEventSchema),
1266
+ serverEvent("air-tag.map-threat-updated", AirTagMapThreatEventSchema),
1267
+ serverEvent("npc-presence.updated", NpcPresenceEventSchema),
1106
1268
  serverEvent("event.map-status-updated", OrganizationEvent),
1107
1269
  serverEvent("event.hero-killed", OrganizationEvent),
1108
1270
  serverEvent("event.ranking-updated", OrganizationEvent),
1109
1271
  serverEvent("event.respawn-window-opened", OrganizationEvent),
1110
1272
  serverEvent("event.respawn-window-closed", OrganizationEvent),
1111
1273
  serverEvent("notification.volunteer", Schema.Struct({
1112
- notificationId: NonEmptyString,
1274
+ notificationId: Schema.NonEmptyString,
1113
1275
  volunteer: Schema.StructWithRest(Schema.Struct({
1114
- discordId: NonEmptyString,
1115
- world: NonEmptyString
1276
+ discordId: Schema.NonEmptyString,
1277
+ world: Schema.NonEmptyString
1116
1278
  }), [Schema.Record(Schema.String, Schema.Unknown)])
1117
1279
  }))
1118
1280
  ]);
@@ -1122,7 +1284,6 @@ const RealtimeFrame = Schema.Union([
1122
1284
  ServerEvent
1123
1285
  ]);
1124
1286
  Schema.decodeUnknownSync(ClientCommand);
1125
- Schema.decodeUnknownSync(Response);
1126
1287
  Schema.decodeUnknownSync(ServerEvent);
1127
1288
  const decodeRealtimeFrame = Schema.decodeUnknownSync(RealtimeFrame);
1128
1289
  const serverEventTypes = new Set(ServerEvent.members.map((member) => member.fields.type.literal));
@@ -1164,11 +1325,34 @@ const tryDecodeRealtimeFrame = (bytes) => Result.try({
1164
1325
  });
1165
1326
  Schema.decodeUnknownSync(PresenceSnapshot);
1166
1327
  Schema.is(MapPingAckSchema);
1328
+ Schema.is(LootSplitAckSchema);
1167
1329
  Schema.is(AirTagSubscriptionAck);
1168
1330
  Schema.is(AirTagObservationAck);
1331
+ Schema.is(AirTagMapThreatsFetchResponse);
1332
+ Schema.is(NpcPresenceReportAckSchema);
1333
+ Schema.is(NpcPresenceSnapshotSchema);
1334
+ const hasRealtimeCapabilities = Schema.is(Schema.Struct({ capabilities: Schema.Array(Schema.NonEmptyString) }));
1169
1335
  Schema.is(Schema.Struct({ presences: Schema.optional(PresenceSnapshot.fields.presences) }));
1170
1336
  //#endregion
1337
+ //#region ../client/src/realtime/notify-listeners.ts
1338
+ const reportListenerError = ({ source, error }) => {
1339
+ try {
1340
+ console.warn(`[Realtime] ${source} listener failed:`, error);
1341
+ } catch {}
1342
+ };
1343
+ const notifyListeners = (listeners, notify, source) => {
1344
+ for (const listener of listeners) try {
1345
+ notify(listener);
1346
+ } catch (error) {
1347
+ reportListenerError({
1348
+ source,
1349
+ error
1350
+ });
1351
+ }
1352
+ };
1353
+ //#endregion
1171
1354
  //#region ../client/src/realtime/realtime-client.ts
1355
+ var RealtimeRequestTimeout = class extends Error {};
1172
1356
  var RealtimeRequestError = class extends Error {
1173
1357
  code;
1174
1358
  retryable;
@@ -1193,6 +1377,7 @@ const normalizeUrl = (baseUrl, path) => {
1193
1377
  };
1194
1378
  const nativeWebSocketFactory = (url, protocols) => new WebSocket(url, protocols);
1195
1379
  const WEBSOCKET_OPEN = 1;
1380
+ const MIN_HEARTBEAT_RETRY_BUDGET_MS = 2e3;
1196
1381
  const scopeKey = (scope) => JSON.stringify([
1197
1382
  scope.topic,
1198
1383
  scope.organizationId ?? null,
@@ -1206,6 +1391,7 @@ const toBytes = async (data) => {
1206
1391
  if (data instanceof Blob) return new Uint8Array(await data.arrayBuffer());
1207
1392
  throw new Error("Gateway returned a non-binary realtime frame");
1208
1393
  };
1394
+ const SERVER_CLOSE_CODES = new Set(Object.values(REALTIME_SERVER_CLOSE_CODES));
1209
1395
  const getPresenceSessionId = (data) => {
1210
1396
  if (!data || typeof data !== "object" || !("sessionId" in data)) return null;
1211
1397
  return typeof data.sessionId === "string" ? data.sessionId : null;
@@ -1221,15 +1407,22 @@ var RealtimeClient = class {
1221
1407
  frameEncoding;
1222
1408
  eventListeners = /* @__PURE__ */ new Set();
1223
1409
  stateListeners = /* @__PURE__ */ new Set();
1410
+ heartbeatLatencyMs = null;
1411
+ heartbeatLatencyListeners = /* @__PURE__ */ new Set();
1224
1412
  pending = /* @__PURE__ */ new Map();
1225
1413
  subscriptions = /* @__PURE__ */ new Map();
1226
1414
  socket = null;
1227
1415
  stateValue = "disconnected";
1228
1416
  joinData = null;
1417
+ joinAttempt = null;
1229
1418
  reconnectAttempt = 0;
1419
+ reconnectNotBeforeMs = 0;
1230
1420
  reconnectTimeout = null;
1231
1421
  heartbeatTimeout = null;
1232
1422
  presenceSessionId = null;
1423
+ presenceRefreshedAt = 0;
1424
+ pingSupported = false;
1425
+ latencyProbeInFlight = false;
1233
1426
  manuallyClosed = false;
1234
1427
  messageChain = Promise.resolve();
1235
1428
  rejoinHandler = null;
@@ -1259,6 +1452,7 @@ var RealtimeClient = class {
1259
1452
  this.manuallyClosed = true;
1260
1453
  this.clearReconnect();
1261
1454
  this.clearHeartbeat();
1455
+ this.pingSupported = false;
1262
1456
  this.rejectPending(/* @__PURE__ */ new Error("Realtime client disconnected"));
1263
1457
  const activeSocket = this.socket;
1264
1458
  this.socket = null;
@@ -1274,6 +1468,30 @@ var RealtimeClient = class {
1274
1468
  listener(this.stateValue);
1275
1469
  return () => this.stateListeners.delete(listener);
1276
1470
  }
1471
+ subscribeHeartbeatLatency(listener) {
1472
+ this.heartbeatLatencyListeners.add(listener);
1473
+ listener(this.heartbeatLatencyMs);
1474
+ return () => this.heartbeatLatencyListeners.delete(listener);
1475
+ }
1476
+ /**
1477
+ * Measures the round trip now instead of at the next heartbeat. A gateway
1478
+ * that does not advertise `connection.ping` keeps the heartbeat measurement.
1479
+ */
1480
+ probeLatency() {
1481
+ if (!this.pingSupported || this.stateValue !== "ready" || this.latencyProbeInFlight) return;
1482
+ const socket = this.socket;
1483
+ const startedAt = performance.now();
1484
+ this.latencyProbeInFlight = true;
1485
+ this.request("connection.ping", {}).then(() => {
1486
+ if (this.socket === socket) this.setHeartbeatLatency(Math.round(performance.now() - startedAt));
1487
+ }).catch(() => {}).finally(() => {
1488
+ this.latencyProbeInFlight = false;
1489
+ });
1490
+ }
1491
+ setHeartbeatLatency(latencyMs) {
1492
+ this.heartbeatLatencyMs = latencyMs;
1493
+ notifyListeners(this.heartbeatLatencyListeners, (listener) => listener(latencyMs), "heartbeat latency");
1494
+ }
1277
1495
  setReconnectHandler(handler) {
1278
1496
  this.rejoinHandler = handler;
1279
1497
  }
@@ -1292,16 +1510,20 @@ var RealtimeClient = class {
1292
1510
  return this.request("subscription.unsubscribe", scope);
1293
1511
  }
1294
1512
  request(type, data) {
1513
+ return this.requestWithTimeout(type, data, this.requestTimeoutMs);
1514
+ }
1515
+ requestWithTimeout(type, data, timeoutMs) {
1295
1516
  const activeSocket = this.socket;
1296
1517
  if (!activeSocket || activeSocket.readyState !== WEBSOCKET_OPEN) return Promise.reject(/* @__PURE__ */ new Error("Realtime connection is not open"));
1297
1518
  const requestId = crypto.randomUUID();
1298
1519
  return new Promise((resolve, reject) => {
1299
1520
  const timeout = setTimeout(() => {
1300
1521
  this.pending.delete(requestId);
1301
- reject(/* @__PURE__ */ new Error(`Realtime request timed out: ${type}`));
1302
- }, this.requestTimeoutMs);
1522
+ reject(new RealtimeRequestTimeout(`Realtime request timed out: ${type}`));
1523
+ }, timeoutMs);
1303
1524
  this.pending.set(requestId, {
1304
1525
  type,
1526
+ startedAt: performance.now(),
1305
1527
  resolve,
1306
1528
  reject,
1307
1529
  timeout
@@ -1338,39 +1560,71 @@ var RealtimeClient = class {
1338
1560
  socket.addEventListener("open", () => {
1339
1561
  if (this.socket !== socket) return;
1340
1562
  this.setState("connected");
1341
- if (this.joinData) (this.rejoinHandler ?? (() => this.performJoin().then(() => void 0)))().catch(() => socket.close(4008, "session rejoin failed"));
1563
+ 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
1564
  });
1343
1565
  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();
1566
+ this.messageChain = this.messageChain.then(() => this.handleMessage(event.data, socket)).catch(() => socket.close(REALTIME_CLIENT_CLOSE_CODES.malformedFrame, "malformed realtime frame"));
1348
1567
  });
1349
- socket.addEventListener("close", () => {
1568
+ socket.addEventListener("close", (event) => {
1350
1569
  if (this.socket !== socket) return;
1351
1570
  this.socket = null;
1571
+ if (event.code !== void 0 && SERVER_CLOSE_CODES.has(event.code)) this.reconnectNotBeforeMs = Math.round(this.random() * REALTIME_SERVER_CLOSE_RECONNECT_WINDOW_MS);
1352
1572
  this.clearHeartbeat();
1573
+ this.pingSupported = false;
1353
1574
  this.rejectPending(/* @__PURE__ */ new Error("Realtime connection closed"));
1354
1575
  this.setState("disconnected");
1355
1576
  if (!this.manuallyClosed) this.scheduleReconnect();
1356
1577
  });
1357
1578
  }
1358
- async performJoin() {
1359
- if (!this.joinData || !this.connected) return void 0;
1579
+ performJoin(data = this.joinData) {
1580
+ const socket = this.socket;
1581
+ if (!data || !socket || !this.connected) return Promise.resolve(void 0);
1582
+ const key = JSON.stringify(data);
1583
+ const pending = this.joinAttempt;
1584
+ if (pending?.socket === socket) {
1585
+ if (pending.key === key) return pending.promise;
1586
+ return pending.promise.then(() => {
1587
+ if (this.socket !== socket || !this.connected) throw new Error("Realtime connection changed before queued join");
1588
+ return this.performJoin(data);
1589
+ });
1590
+ }
1591
+ const promise = this.restoreSession(socket, this.request("session.join", data));
1592
+ this.joinAttempt = {
1593
+ socket,
1594
+ key,
1595
+ promise
1596
+ };
1360
1597
  this.setState("joining");
1598
+ return promise;
1599
+ }
1600
+ async restoreSession(socket, joined) {
1361
1601
  try {
1362
- const result = await this.request("session.join", this.joinData);
1602
+ const result = await joined;
1603
+ if (this.socket !== socket) return result;
1604
+ this.pingSupported = hasRealtimeCapabilities(result) && result.capabilities.includes("connection.ping");
1363
1605
  await Promise.all([...this.subscriptions.values()].map((scope) => this.request("subscription.subscribe", scope)));
1606
+ if (this.socket !== socket) return result;
1364
1607
  this.reconnectAttempt = 0;
1365
1608
  this.setState("ready");
1366
1609
  return result;
1367
1610
  } catch (error) {
1368
- if (this.connected) this.socket?.close(4008, "session join failed");
1611
+ if (this.socket === socket) {
1612
+ if (error instanceof RealtimeRequestError && !error.retryable) {
1613
+ this.manuallyClosed = true;
1614
+ this.clearReconnect();
1615
+ }
1616
+ if (error instanceof RealtimeRequestError && error.retryable) this.reconnectNotBeforeMs = error.retryAfterMs ?? 0;
1617
+ socket.close(REALTIME_CLIENT_CLOSE_CODES.sessionJoinFailed, "session join failed");
1618
+ }
1369
1619
  throw error;
1620
+ } finally {
1621
+ if (this.joinAttempt?.socket === socket) this.joinAttempt = null;
1370
1622
  }
1371
1623
  }
1372
- async handleMessage(data) {
1624
+ async handleMessage(data, socket) {
1625
+ if (this.socket !== socket) return;
1373
1626
  const frame = await this.decodeFrame(data);
1627
+ if (this.socket !== socket) return;
1374
1628
  if ("status" in frame) {
1375
1629
  const pending = this.pending.get(frame.requestId);
1376
1630
  if (!pending) return;
@@ -1379,6 +1633,7 @@ var RealtimeClient = class {
1379
1633
  if (frame.status === "success") {
1380
1634
  if (pending.type === "presence.publish") {
1381
1635
  this.presenceSessionId = getPresenceSessionId(frame.data);
1636
+ this.presenceRefreshedAt = pending.startedAt;
1382
1637
  this.scheduleHeartbeat();
1383
1638
  }
1384
1639
  pending.resolve(frame.data);
@@ -1386,7 +1641,7 @@ var RealtimeClient = class {
1386
1641
  return;
1387
1642
  }
1388
1643
  if (!isServerEventFrame(frame)) return;
1389
- for (const listener of this.eventListeners) listener(frame);
1644
+ notifyListeners(this.eventListeners, (listener) => listener(frame), `server event ${frame.type}`);
1390
1645
  }
1391
1646
  encodeFrame(frame) {
1392
1647
  return this.frameEncoding === "json" ? JSON.stringify(frame) : encodeRealtimeFrame(frame);
@@ -1403,18 +1658,59 @@ var RealtimeClient = class {
1403
1658
  scheduleReconnect() {
1404
1659
  this.reconnectAttempt += 1;
1405
1660
  const exponential = Math.min(this.reconnectMaxDelayMs, this.reconnectBaseDelayMs * 2 ** (this.reconnectAttempt - 1));
1406
- const jittered = Math.round(exponential * (.5 + this.random()));
1661
+ const jittered = Math.max(Math.round(exponential * (.5 + this.random())), this.reconnectNotBeforeMs);
1662
+ this.reconnectNotBeforeMs = 0;
1407
1663
  this.setState("reconnecting");
1408
1664
  this.reconnectTimeout = setTimeout(() => this.open("reconnecting"), jittered);
1409
1665
  }
1410
- scheduleHeartbeat() {
1666
+ scheduleHeartbeat(delayMs = PRESENCE_HEARTBEAT_INTERVAL_MS, retryAttempt = 0) {
1411
1667
  this.clearHeartbeat();
1412
- if (!this.presenceSessionId || !this.connected) return;
1668
+ if (!this.presenceSessionId || !this.connected) {
1669
+ this.setHeartbeatLatency(null);
1670
+ return;
1671
+ }
1413
1672
  this.heartbeatTimeout = setTimeout(() => {
1414
1673
  const sessionId = this.presenceSessionId;
1415
1674
  if (!sessionId) return;
1416
- this.request("presence.heartbeat", { sessionId }).then(() => this.scheduleHeartbeat()).catch(() => this.socket?.close());
1417
- }, PRESENCE_HEARTBEAT_INTERVAL_MS);
1675
+ const startedAt = performance.now();
1676
+ const socket = this.socket;
1677
+ const refreshedAt = this.presenceRefreshedAt;
1678
+ const expiry = refreshedAt + PRESENCE_EXPIRY_MS;
1679
+ const remaining = expiry - startedAt;
1680
+ if (remaining <= 0 || retryAttempt > 0 && remaining < MIN_HEARTBEAT_RETRY_BUDGET_MS) {
1681
+ this.setHeartbeatLatency(null);
1682
+ socket?.close(REALTIME_CLIENT_CLOSE_CODES.heartbeatUnavailable, "heartbeat unavailable");
1683
+ return;
1684
+ }
1685
+ const isStaleHeartbeat = () => this.socket !== socket || this.presenceSessionId !== sessionId || this.presenceRefreshedAt !== refreshedAt;
1686
+ this.requestWithTimeout("presence.heartbeat", { sessionId }, Math.min(this.requestTimeoutMs, remaining)).then(() => {
1687
+ if (isStaleHeartbeat()) return;
1688
+ this.presenceRefreshedAt = startedAt;
1689
+ this.setHeartbeatLatency(Math.round(performance.now() - startedAt));
1690
+ this.scheduleHeartbeat();
1691
+ }).catch((error) => {
1692
+ if (isStaleHeartbeat()) return;
1693
+ this.setHeartbeatLatency(null);
1694
+ if (error instanceof RealtimeRequestError && error.retryable) {
1695
+ const retryDelay = Math.max(Math.round(Math.min(5e3, 1e3 * 2 ** retryAttempt) * (.5 + this.random())), error.retryAfterMs ?? 0);
1696
+ if (expiry - performance.now() - retryDelay >= MIN_HEARTBEAT_RETRY_BUDGET_MS) {
1697
+ this.scheduleHeartbeat(retryDelay, retryAttempt + 1);
1698
+ return;
1699
+ }
1700
+ socket?.close(REALTIME_CLIENT_CLOSE_CODES.heartbeatUnavailable, "heartbeat unavailable");
1701
+ return;
1702
+ }
1703
+ if (error instanceof RealtimeRequestTimeout) {
1704
+ socket?.close(REALTIME_CLIENT_CLOSE_CODES.heartbeatTimeout, "heartbeat timeout");
1705
+ return;
1706
+ }
1707
+ if (error instanceof RealtimeRequestError) {
1708
+ socket?.close(REALTIME_CLIENT_CLOSE_CODES.heartbeatRejected, "heartbeat rejected");
1709
+ return;
1710
+ }
1711
+ socket?.close();
1712
+ });
1713
+ }, delayMs);
1418
1714
  }
1419
1715
  rejectPending(error) {
1420
1716
  for (const pending of this.pending.values()) {
@@ -1426,7 +1722,8 @@ var RealtimeClient = class {
1426
1722
  setState(state) {
1427
1723
  if (this.stateValue === state) return;
1428
1724
  this.stateValue = state;
1429
- for (const listener of this.stateListeners) listener(state);
1725
+ if (state === "disconnected") this.setHeartbeatLatency(null);
1726
+ notifyListeners(this.stateListeners, (listener) => listener(state), `connection state ${state}`);
1430
1727
  }
1431
1728
  clearReconnect() {
1432
1729
  if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);