@crowdedkingdomstudios/crowdyjs 1.0.7 → 2.1.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 (68) hide show
  1. package/README.md +439 -12
  2. package/dist/auth-state.d.ts +21 -0
  3. package/dist/auth-state.d.ts.map +1 -0
  4. package/dist/auth-state.js +30 -0
  5. package/dist/client.d.ts +27 -21
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +27 -196
  8. package/dist/crowdy-client.d.ts +44 -24
  9. package/dist/crowdy-client.d.ts.map +1 -1
  10. package/dist/crowdy-client.js +42 -81
  11. package/dist/domains/actors.d.ts +22 -0
  12. package/dist/domains/actors.d.ts.map +1 -0
  13. package/dist/domains/actors.js +42 -0
  14. package/dist/domains/appAccess.d.ts +23 -0
  15. package/dist/domains/appAccess.d.ts.map +1 -0
  16. package/dist/domains/appAccess.js +42 -0
  17. package/dist/domains/apps.d.ts +27 -0
  18. package/dist/domains/apps.d.ts.map +1 -0
  19. package/dist/domains/apps.js +49 -0
  20. package/dist/domains/auth.d.ts +32 -0
  21. package/dist/domains/auth.d.ts.map +1 -0
  22. package/dist/domains/auth.js +70 -0
  23. package/dist/domains/billing.d.ts +17 -0
  24. package/dist/domains/billing.d.ts.map +1 -0
  25. package/dist/domains/billing.js +31 -0
  26. package/dist/domains/chunks.d.ts +20 -0
  27. package/dist/domains/chunks.d.ts.map +1 -0
  28. package/dist/domains/chunks.js +40 -0
  29. package/dist/domains/organizations.d.ts +33 -0
  30. package/dist/domains/organizations.d.ts.map +1 -0
  31. package/dist/domains/organizations.js +90 -0
  32. package/dist/domains/payments.d.ts +20 -0
  33. package/dist/domains/payments.d.ts.map +1 -0
  34. package/dist/domains/payments.js +28 -0
  35. package/dist/domains/quotas.d.ts +20 -0
  36. package/dist/domains/quotas.d.ts.map +1 -0
  37. package/dist/domains/quotas.js +34 -0
  38. package/dist/domains/serverStatus.d.ts +21 -0
  39. package/dist/domains/serverStatus.d.ts.map +1 -0
  40. package/dist/domains/serverStatus.js +32 -0
  41. package/dist/domains/state.d.ts +16 -0
  42. package/dist/domains/state.d.ts.map +1 -0
  43. package/dist/domains/state.js +27 -0
  44. package/dist/domains/teleport.d.ts +13 -0
  45. package/dist/domains/teleport.d.ts.map +1 -0
  46. package/dist/domains/teleport.js +15 -0
  47. package/dist/domains/udp.d.ts +32 -0
  48. package/dist/domains/udp.d.ts.map +1 -0
  49. package/dist/domains/udp.js +55 -0
  50. package/dist/domains/users.d.ts +24 -0
  51. package/dist/domains/users.d.ts.map +1 -0
  52. package/dist/domains/users.js +53 -0
  53. package/dist/domains/voxels.d.ts +17 -0
  54. package/dist/domains/voxels.d.ts.map +1 -0
  55. package/dist/domains/voxels.js +31 -0
  56. package/dist/generated/graphql.d.ts +3571 -0
  57. package/dist/generated/graphql.d.ts.map +1 -0
  58. package/dist/generated/graphql.js +181 -0
  59. package/dist/index.d.ts +33 -3
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +34 -1
  62. package/dist/subscriptions.d.ts +36 -18
  63. package/dist/subscriptions.d.ts.map +1 -1
  64. package/dist/subscriptions.js +95 -181
  65. package/dist/types.d.ts +20 -17
  66. package/dist/types.d.ts.map +1 -1
  67. package/dist/types.js +3 -3
  68. package/package.json +12 -1
package/README.md CHANGED
@@ -1,9 +1,16 @@
1
1
  # CrowdyJS SDK
2
2
 
3
3
  Client SDK for the Crowded Kingdoms GraphQL API with UDP proxy support.
4
- Handles authentication, real-time subscriptions, and all repilcation-server
5
- communication through a single `CrowdyClient` instance. Users should still
6
- directly access the CK GraphQL API for other functions beyond replication.
4
+ Handles authentication, real-time subscriptions, and all replication-server
5
+ communication through a single `CrowdyClient` instance.
6
+
7
+ ## Links
8
+
9
+ - [NPM](https://www.npmjs.com/package/@crowdedkingdomstudios/crowdyjs)
10
+ - [GitHub](https://github.com/CrowdedKingdoms/CrowdyJS)
11
+ - [Discord](https://discord.gg/crowdedkingdoms)
12
+ - [YouTube](https://www.youtube.com/@CrowdedKingdoms)
13
+
7
14
 
8
15
  ## Installation
9
16
 
@@ -116,7 +123,7 @@ const MY_UUID = Array.from(crypto.getRandomValues(new Uint8Array(16)))
116
123
 
117
124
  ```javascript
118
125
  await client.sendActorUpdate({
119
- mapId: 0,
126
+ appId: 0,
120
127
  chunk: { x: 0, y: 0, z: 0 },
121
128
  distance: 8,
122
129
  uuid: MY_UUID,
@@ -142,7 +149,7 @@ view.setFloat32(8, posZ, true);
142
149
  const base64State = btoa(String.fromCharCode(...new Uint8Array(stateBuffer)));
143
150
 
144
151
  await client.sendActorUpdate({
145
- mapId: 0,
152
+ appId: 0,
146
153
  chunk: { x: 0, y: 0, z: 0 },
147
154
  distance: 8,
148
155
  decayRate: 0,
@@ -163,13 +170,40 @@ Unsubscribing from notifications stops delivery but does **not** release
163
170
  the UDP session. Call `disconnectUdpProxy()` explicitly, or the server
164
171
  will release it after 30 seconds of inactivity.
165
172
 
173
+ ## Request / Notification Mapping
174
+
175
+ Every accepted spatial request is fanned out by the game server as a
176
+ notification to all clients in range, **including the sender**. Treat
177
+ your own arrival as the round-trip echo for that send (correlate by
178
+ `uuid` + `sequenceNumber`).
179
+
180
+ | Mutation (you send) | Notification (everyone in range receives) |
181
+ |-----------------------------|-------------------------------------------|
182
+ | `sendActorUpdate(...)` | `ActorUpdateNotification` |
183
+ | `sendVoxelUpdate(...)` | `VoxelUpdateNotification` |
184
+ | `sendAudioPacket(...)` | `ClientAudioNotification` |
185
+ | `sendTextPacket(...)` | `ClientTextNotification` |
186
+ | `sendClientEvent(...)` | `ClientEventNotification` |
187
+ | *(generic spatial)* | *(generic spatial)* |
188
+
189
+ `ServerEventNotification` is server-originated (no matching client
190
+ mutation). `GenericErrorResponse` carries failures for any of the above
191
+ — correlate to the offending send via `sequenceNumber`. The legacy
192
+ `ActorUpdateResponse` and `VoxelUpdateResponse` ack types are retired
193
+ on the wire; rely on the self-fanout notification (and
194
+ `GenericErrorResponse` for failures) instead.
195
+
196
+ > *Generic spatial* refers to wire type `GENERIC_SPATIAL_1` (an arbitrary
197
+ > client-defined payload routed through the same spatial fan-out as the
198
+ > typed messages above). It is not currently exposed by this SDK.
199
+
166
200
  ## Subscription Handlers
167
201
 
168
202
  All spatial notification types share a uniform header:
169
203
 
170
204
  | Field | Type | Description |
171
205
  |-------|------|-------------|
172
- | `mapId` | `string` | Map / chunk-W coordinate |
206
+ | `appId` | `string` | App ID (tenant / world identifier) |
173
207
  | `chunkX` | `string` | Chunk X coordinate |
174
208
  | `chunkY` | `string` | Chunk Y coordinate |
175
209
  | `chunkZ` | `string` | Chunk Z coordinate |
@@ -204,7 +238,7 @@ All mutation inputs share these fields:
204
238
 
205
239
  | Field | Type | Required | Default | Description |
206
240
  |-------|------|----------|---------|-------------|
207
- | `mapId` | `number` | yes | -- | Map / chunk-W coordinate |
241
+ | `appId` | `number` | yes | -- | App ID (tenant / world identifier) |
208
242
  | `chunk` | `{ x, y, z }` | yes | -- | Chunk coordinates (numbers) |
209
243
  | `uuid` | `string` | yes | -- | Random 32-byte UTF-8 string (see [Generating a UUID](#generating-a-uuid)) |
210
244
  | `distance` | `number` | no | `8` | Replication range (0-8 chunks, Chebyshev) |
@@ -285,7 +319,7 @@ const unsubErrors = client.onGenericError((e) => {
285
319
 
286
320
  // 3. Register in chunk
287
321
  await client.sendActorUpdate({
288
- mapId: 0,
322
+ appId: 0,
289
323
  chunk: { x: 0, y: 0, z: 0 },
290
324
  distance: 8,
291
325
  uuid: MY_UUID,
@@ -301,7 +335,7 @@ const interval = setInterval(async () => {
301
335
  const state = btoa(String.fromCharCode(...buf));
302
336
 
303
337
  await client.sendActorUpdate({
304
- mapId: 0,
338
+ appId: 0,
305
339
  chunk: { x: 0, y: 0, z: 0 },
306
340
  distance: 8,
307
341
  uuid: MY_UUID,
@@ -318,6 +352,212 @@ await client.disconnectUdpProxy();
318
352
  client.close();
319
353
  ```
320
354
 
355
+ ## Building a Real-Time Game
356
+
357
+ The Quick Start above is enough to send and receive packets. Once you wire
358
+ the SDK into an actual game these patterns will save you a lot of debugging
359
+ time.
360
+
361
+ ### Decouple the network from the render loop
362
+
363
+ Browsers throttle `requestAnimationFrame` aggressively when the tab is
364
+ backgrounded — often to ~1 fps or fully paused. If your `sendActorUpdate`
365
+ cadence is driven by the render loop, your character will appear frozen
366
+ to other players the moment a user switches tabs. Drive sends with
367
+ `setInterval` (which keeps firing in background tabs):
368
+
369
+ ```javascript
370
+ const SEND_INTERVAL_MS = 100; // 10 Hz
371
+ const sendId = setInterval(async () => {
372
+ await client.sendActorUpdate({
373
+ appId, chunk: getCurrentChunk(),
374
+ uuid: MY_UUID,
375
+ state: encodeLocalPose(),
376
+ sequenceNumber: nextSeq(),
377
+ });
378
+ }, SEND_INTERVAL_MS);
379
+ ```
380
+
381
+ Apply incoming notifications **inside the WS callback**, not buffered for
382
+ the next frame. That keeps your data model in sync with real network
383
+ arrivals even while the render loop is paused:
384
+
385
+ ```javascript
386
+ client.onActorUpdate((notification) => {
387
+ registry.applyRemote(notification, performance.now()); // synchronous
388
+ });
389
+ ```
390
+
391
+ The renderer should treat the registry as a *view source* — sprites
392
+ interpolate toward registry-held targets each frame, but the registry
393
+ itself is mutated only by the network layer.
394
+
395
+ ### Verify the round-trip with self-fanout
396
+
397
+ The game server fans out every accepted actor update to all clients in
398
+ the chunk, **including the sender**. Use your own arrival as the WS
399
+ round-trip echo (correlate by `uuid` + `sequenceNumber`):
400
+
401
+ ```javascript
402
+ let lastSendAt = null, lastSendSeq = null;
403
+ let lastEchoAt = null, lastEchoSeq = null;
404
+
405
+ async function sendOnce() {
406
+ const seq = nextSeq();
407
+ const ok = await client.sendActorUpdate({ /* ... */, sequenceNumber: seq });
408
+ if (ok) { lastSendAt = performance.now(); lastSendSeq = seq; }
409
+ }
410
+
411
+ client.onActorUpdate((n) => {
412
+ if (n.uuid === MY_UUID) {
413
+ lastEchoAt = performance.now();
414
+ lastEchoSeq = n.sequenceNumber;
415
+ return; // don't render ourselves as a remote
416
+ }
417
+ registry.applyRemote(n);
418
+ });
419
+ ```
420
+
421
+ `ActorUpdateResponse` is retired on the wire — the self-uuid notification
422
+ is the canonical ack. `GenericErrorResponse` carries failures (correlate
423
+ via `sequenceNumber`).
424
+
425
+ ### Keep the UDP session alive
426
+
427
+ The server times the UDP proxy session out after **30 s** of inactivity in
428
+ either direction. A 10 Hz send loop trivially keeps it warm. If your
429
+ sender pauses (e.g. no local input → no diff to send), the next mutation
430
+ will lazily re-establish via `ensureUdpProxyConnection` — `connectUdpProxy()`
431
+ is idempotent. There is no need for a separate keepalive.
432
+
433
+ ### Detect and recover from connection drops
434
+
435
+ The SDK does not auto-reconnect a dropped WebSocket: `onclose` only nulls
436
+ the internal client. Track wall-clock arrival time of *any* notification
437
+ and force a fresh subscription if it goes silent:
438
+
439
+ ```javascript
440
+ let lastNotificationAt = performance.now();
441
+ function bumpActivity() { lastNotificationAt = performance.now(); }
442
+
443
+ // Wire bumpActivity into every handler you register (actor, voxel, text,
444
+ // audio, event, server event, error). Or wrap your subscribe call.
445
+ client.onActorUpdate((n) => { bumpActivity(); /* ... */ });
446
+ client.onGenericError((e) => { bumpActivity(); /* ... */ });
447
+
448
+ setInterval(() => {
449
+ if (performance.now() - lastNotificationAt > 8000) {
450
+ forceResubscribe(); // see below
451
+ }
452
+ }, 1000);
453
+ ```
454
+
455
+ To force a fresh subscription, call your unsubscribe handles and
456
+ `subscribe()` again — the SDK opens a new WebSocket on a fresh subscribe.
457
+ Then send one `sendActorUpdate` to re-register your chunk position so
458
+ others see you immediately.
459
+
460
+ ```javascript
461
+ function forceResubscribe() {
462
+ unsubAll();
463
+ unsubAll = installSubscriptions(); // re-registers handlers
464
+ void client.sendActorUpdate({ /* re-register */ });
465
+ }
466
+ ```
467
+
468
+ ### Visibility-driven heal
469
+
470
+ Browsers sometimes kill the WebSocket while a tab is hidden. Listen for
471
+ `visibilitychange` and trigger a heal on return. Pause your stale-actor
472
+ reaper (next section) while hidden so you don't drop remotes during a
473
+ period when you couldn't have heard from them anyway.
474
+
475
+ ```javascript
476
+ document.addEventListener('visibilitychange', () => {
477
+ if (document.hidden) {
478
+ pauseReaper('tab_hidden');
479
+ return;
480
+ }
481
+ forceResubscribe();
482
+ resumeReaperWithGrace('tab_hidden', 5000);
483
+ });
484
+ ```
485
+
486
+ ### Track presence with timestamps
487
+
488
+ There is no live "who is in this chunk" query — `client.actors.list({ chunk })`
489
+ returns the persisted DB roster, not the live UDP set. Each remote actor's
490
+ presence is inferred from the last notification you saw from them:
491
+
492
+ ```javascript
493
+ const remotes = new Map(); // uuid -> { firstSeenAt, lastUpdateAt, pose }
494
+
495
+ client.onActorUpdate((n) => {
496
+ if (n.uuid === MY_UUID) return;
497
+ const now = performance.now();
498
+ const existing = remotes.get(n.uuid);
499
+ if (!existing) {
500
+ remotes.set(n.uuid, { firstSeenAt: now, lastUpdateAt: now, pose: decode(n.state) });
501
+ } else {
502
+ existing.lastUpdateAt = now;
503
+ existing.pose = decode(n.state);
504
+ }
505
+ });
506
+ ```
507
+
508
+ Periodically reap remotes that stopped sending. **Suspend reaping while
509
+ the tab is hidden or the WS is unhealthy** — otherwise you will evict
510
+ remotes simply because you couldn't have heard from them.
511
+
512
+ ```javascript
513
+ const STALE_TIMEOUT_MS = 10_000;
514
+ setInterval(() => {
515
+ if (document.hidden || isWsStale()) return;
516
+ const now = performance.now();
517
+ for (const [uuid, r] of remotes) {
518
+ if (now - r.lastUpdateAt > STALE_TIMEOUT_MS) remotes.delete(uuid);
519
+ }
520
+ }, 3000);
521
+ ```
522
+
523
+ Grant a grace window after every heal (visibility return, force-resubscribe)
524
+ before reaping resumes — that lets remotes re-broadcast on their own
525
+ cadence first.
526
+
527
+ ### Suggested module layout
528
+
529
+ A clean separation makes the rest much easier:
530
+
531
+ ```
532
+ GameSession (singleton, lives above your scenes / views)
533
+ ├── ActorRegistry data + emitter; mutated only in WS callbacks
534
+ ├── ActorSender setInterval sends; reads pose from your game
535
+ ├── NetworkHealth visibility / WS-stale monitor + heal
536
+ └── (uses) CrowdyClient
537
+ ```
538
+
539
+ Sprites and HUD become *views* over `ActorRegistry`. Scene shutdown only
540
+ destroys sprites; the singleton session and registry persist, so revisiting
541
+ the scene (or returning from a hidden tab) does not lose presence state.
542
+ Recreating sprites on scene re-entry walks the existing registry entries.
543
+
544
+ ### Diagnostics worth logging
545
+
546
+ When something goes wrong, these are the values you'll wish you had:
547
+
548
+ - `sendCount`, `lastSendAt`, `lastSendSeq` — your local sender's history.
549
+ - `echoCount`, `lastEchoAt`, `lastEchoSeq`, `msSinceLastEcho` — the WS
550
+ round-trip via self-fanout. If `echoCount` flatlines while `sendCount`
551
+ climbs, the WS path is wedged.
552
+ - `notificationsByType` — map of `__typename` to arrival count. Useful for
553
+ confirming what the server is actually emitting.
554
+ - Per-remote `lastUpdateAt` / `msSinceUpdate`.
555
+ - `visibilityState`, `hidden`, `lastWsResubscribeAt`, `reaperPaused`.
556
+
557
+ Expose them on a global handle (e.g. `window.__MYGAME_NET__.snapshot()`)
558
+ during development — a structured JSON snapshot is far easier to triage
559
+ than a wall of streaming logs.
560
+
321
561
  ## API Reference
322
562
 
323
563
  ### Constructor
@@ -344,8 +584,8 @@ new CrowdyClient(config?: CrowdyClientConfig)
344
584
 
345
585
  | Method | Returns | Description |
346
586
  |--------|---------|-------------|
347
- | `connectUdpProxy()` | `Promise<UdpProxyConnectionStatus>` | Explicitly open a UDP session (optional) |
348
- | `disconnectUdpProxy()` | `Promise<boolean>` | Release the UDP session |
587
+ | `connectUdpProxy()` | `Promise<UdpProxyConnectionStatus>` | Open a UDP session (idempotent: returns the existing status if one is open). Optional — `udpNotifications` and any `send*` mutation also create a session lazily. |
588
+ | `disconnectUdpProxy()` | `Promise<boolean>` | Release the UDP session and complete the current `udpNotifications` stream. To force a fresh socket: call `disconnectUdpProxy()` then `connectUdpProxy()`. |
349
589
  | `getConnectionStatus()` | `Promise<UdpProxyConnectionStatus>` | Check if a UDP session is active |
350
590
 
351
591
  ### Mutations
@@ -381,11 +621,198 @@ the handler.
381
621
  |--------|-------------|
382
622
  | `close()` | Close the WebSocket, remove all handlers, clear auth state |
383
623
 
624
+ ## Domain APIs
625
+
626
+ In addition to the replication-focused methods documented above, the
627
+ client exposes a set of typed *domain wrappers* that cover the rest of
628
+ the Crowded Kingdoms GraphQL API. Each wrapper is mounted on the
629
+ `CrowdyClient` instance and uses the same auth token / base URL as the
630
+ replication API. Operation inputs and return shapes are generated from
631
+ the live `schema.gql` via `graphql-codegen`, so they stay in lockstep
632
+ with the server.
633
+
634
+ ```javascript
635
+ const client = new CrowdyClient({ graphqlEndpoint: '...' });
636
+ await client.auth.login({ email, password });
637
+ ```
638
+
639
+ ### `client.auth` -- authentication & session lifecycle
640
+
641
+ ```javascript
642
+ await client.auth.register({ email, password, gamertag });
643
+ await client.auth.login({ email, password });
644
+ await client.auth.confirmEmail('...token...');
645
+ await client.auth.requestPasswordReset('user@example.com');
646
+ await client.auth.resetPassword({ token: '...', newPassword: '...' });
647
+ await client.auth.resendConfirmationEmail('user@example.com');
648
+ await client.auth.changePassword('old', 'new');
649
+ await client.auth.logout();
650
+ await client.auth.logoutAllDevices();
651
+ ```
652
+
653
+ The legacy shortcuts `client.login(email, password)` and
654
+ `client.register(email, password, gamertag?)` are kept for backwards
655
+ compatibility and delegate to `client.auth`.
656
+
657
+ ### `client.users` -- user directory & profile
658
+
659
+ ```javascript
660
+ const me = await client.users.me();
661
+ const u = await client.users.byId('123');
662
+ const all = await client.users.list();
663
+ const list = await client.users.byGamertag('Gandalf');
664
+ const list2 = await client.users.byEmail('a@b.c');
665
+
666
+ await client.users.updateGamertag({ gamertag: 'Newname', disambiguation: '0001' });
667
+ await client.users.updateUserState({ state: 'base64...' });
668
+ ```
669
+
670
+ ### `client.orgs` -- organizations / studios
671
+
672
+ ```javascript
673
+ const org = await client.orgs.create({ name: 'Acme', slug: 'acme' });
674
+ const byId = await client.orgs.byId(org.orgId);
675
+ const bySlug = await client.orgs.bySlug('acme');
676
+ const members = await client.orgs.members(org.orgId);
677
+
678
+ await client.orgs.inviteMember({ orgId: org.orgId, userId: '42' });
679
+ const token = await client.orgs.createOrgToken({ orgId: org.orgId, label: 'CI' });
680
+ ```
681
+
682
+ ### `client.appAccess` -- app access tiers and grants
683
+
684
+ ```javascript
685
+ const tiers = await client.appAccess.listTiers(appId);
686
+ const mine = await client.appAccess.myAccess(appId);
687
+
688
+ await client.appAccess.createTier({ appId, name: 'Pro', isFree: false, tierOrder: 2 });
689
+ await client.appAccess.grant({ appId, userId: '42', tierId });
690
+ await client.appAccess.revoke(appId, '42');
691
+ ```
692
+
693
+ ### `client.billing` -- wallets & app budgets
694
+
695
+ ```javascript
696
+ const wallet = await client.billing.balance(orgId);
697
+ const txns = await client.billing.transactions({ orgId, limit: 50, offset: 0 });
698
+ const budget = await client.billing.appBudget(orgId, appId);
699
+
700
+ await client.billing.setAppBudget({ orgId, appId, monthlyLimitCents: '5000' });
701
+ ```
702
+
703
+ ### `client.quotas` -- per-org / per-app service quotas
704
+
705
+ ```javascript
706
+ const orgQ = await client.quotas.byOrg(orgId);
707
+ const appQ = await client.quotas.byApp(appId);
708
+
709
+ const q = await client.quotas.set({
710
+ orgId,
711
+ appId,
712
+ metric: 'voxel_updates',
713
+ limitValue: '1000000',
714
+ period: 'month',
715
+ });
716
+ await client.quotas.delete(q.quotaId);
717
+ ```
718
+
719
+ ### `client.chunks` -- chunk + LOD queries and admin mutations
720
+
721
+ ```javascript
722
+ const chunk = await client.chunks.get({ appId, coordinates: { x: '0', y: '0', z: '0' } });
723
+ const lods = await client.chunks.getLods({ appId, coordinates, lodLevels: [0, 1, 2] });
724
+ const nearby = await client.chunks.byDistance({ appId, centerCoordinate: coordinates, maxDistance: 4 });
725
+ const voxels = await client.chunks.voxelList({ appId, coordinates });
726
+
727
+ await client.chunks.update({ appId, coordinates, voxels: '...base64...' });
728
+ await client.chunks.updateState({ appId, coordinates, chunkState: '...' });
729
+ await client.chunks.updateLods({ appId, coordinates, lods: [{ level: 0, data: '...' }] });
730
+ ```
731
+
732
+ ### `client.voxels` -- voxel queries, history, rollback
733
+
734
+ ```javascript
735
+ await client.voxels.list({ appId, coordinates });
736
+ await client.voxels.listByDistance({ appId, centerCoordinate: coordinates, maxDistance: 2 });
737
+ await client.voxels.update({ appId, coordinates, location, voxelType: 5, state: '...' });
738
+ await client.voxels.history({ appId, userId: '42', limit: 100 });
739
+ await client.voxels.rollback({ appId, userId: '42', from, to, dryRun: true });
740
+ ```
741
+
742
+ ### `client.actors` -- persisted actors (CRUD)
743
+
744
+ ```javascript
745
+ const a = await client.actors.create({
746
+ appId, uuid: '...', chunk: { x: '0', y: '0', z: '0' },
747
+ });
748
+ await client.actors.get(a.uuid);
749
+ await client.actors.list({ appId });
750
+ await client.actors.batchLookup({ uuids: [a.uuid] });
751
+ await client.actors.update(a.uuid, { publicState: '...' });
752
+ await client.actors.updateState(a.uuid, { publicState: '...' });
753
+ await client.actors.delete(a.uuid);
754
+ ```
755
+
756
+ For high-frequency replication, continue to use the existing
757
+ `client.sendActorUpdate(...)` UDP path -- it is unchanged.
758
+
759
+ ### `client.teleport`
760
+
761
+ ```javascript
762
+ const result = await client.teleport.request({
763
+ appId,
764
+ chunkAddress: { x: '0', y: '0', z: '0' },
765
+ voxelAddress: { x: 0, y: 0, z: 0 },
766
+ UUID: '...',
767
+ });
768
+ ```
769
+
770
+ ### `client.state` -- per-user, per-app state
771
+
772
+ ```javascript
773
+ await client.state.update({ appId, state: 'base64...' });
774
+ await client.state.getOne(appId);
775
+ await client.state.getAll();
776
+ await client.state.delete(appId);
777
+ ```
778
+
779
+ ### `client.serverStatus`
780
+
781
+ ```javascript
782
+ await client.serverStatus.serverWithLeastClients();
783
+ await client.serverStatus.listAll();
784
+ await client.serverStatus.listActiveGraphqlServers();
785
+ await client.serverStatus.versionInfo();
786
+ ```
787
+
788
+ ### `client.apps`
789
+
790
+ The current schema does not expose direct CRUD for the App entity --
791
+ apps are provisioned out of band. `client.apps` exists as a stable
792
+ namespace for future additions; in the meantime use `client.appAccess`,
793
+ `client.state`, `client.billing`, `client.chunks`, and `client.voxels`
794
+ for app-scoped functionality.
795
+
796
+ ## Codegen
797
+
798
+ Operations live as `.graphql` files under `src/operations/<domain>/`.
799
+ After editing them or pulling a new schema, regenerate the typed
800
+ documents:
801
+
802
+ ```bash
803
+ npm run codegen # one-shot
804
+ npm run codegen:watch # watch mode
805
+ ```
806
+
807
+ `npm run build` runs codegen automatically via `prebuild`.
808
+
384
809
  ## TypeScript
385
810
 
386
811
  The SDK is written in TypeScript and ships type declarations. All
387
812
  notification interfaces, input types, and handler signatures are fully
388
- typed for IDE autocomplete and compile-time safety.
813
+ typed for IDE autocomplete and compile-time safety. Schema-derived
814
+ input/output types (e.g. `CreateOrganizationInput`, `Organization`,
815
+ `AppBudget`) are re-exported from the package root.
389
816
 
390
817
  ## License
391
818
 
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Single source of truth for the auth token within one CrowdyClient instance.
3
+ *
4
+ * Both the HTTP client (`GraphQLClient`) and the WebSocket subscription
5
+ * manager observe this. Anything that mutates auth state - the typed
6
+ * `client.auth.login()` / `register()` / `logout()` family, or external
7
+ * callers - flows through `setToken()` so subscription requests can never
8
+ * silently fail with "Must be authenticated to subscribe".
9
+ *
10
+ * (Replaces the previous arrangement where GraphQLClient and
11
+ * SubscriptionManager each held their own copy of the token.)
12
+ */
13
+ export type AuthStateListener = (token: string | null) => void;
14
+ export declare class AuthState {
15
+ private token;
16
+ private listeners;
17
+ getToken(): string | null;
18
+ setToken(token: string | null): void;
19
+ subscribe(listener: AuthStateListener): () => void;
20
+ }
21
+ //# sourceMappingURL=auth-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-state.d.ts","sourceRoot":"","sources":["../src/auth-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;AAE/D,qBAAa,SAAS;IACpB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,SAAS,CAAgC;IAEjD,QAAQ,IAAI,MAAM,GAAG,IAAI;IAIzB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAYpC,SAAS,CAAC,QAAQ,EAAE,iBAAiB,GAAG,MAAM,IAAI;CAQnD"}
@@ -0,0 +1,30 @@
1
+ export class AuthState {
2
+ constructor() {
3
+ this.token = null;
4
+ this.listeners = new Set();
5
+ }
6
+ getToken() {
7
+ return this.token;
8
+ }
9
+ setToken(token) {
10
+ if (token === this.token)
11
+ return;
12
+ this.token = token;
13
+ for (const listener of this.listeners) {
14
+ try {
15
+ listener(token);
16
+ }
17
+ catch (error) {
18
+ console.error('AuthState listener threw:', error);
19
+ }
20
+ }
21
+ }
22
+ subscribe(listener) {
23
+ this.listeners.add(listener);
24
+ // Replay current value immediately so listeners initialize correctly.
25
+ listener(this.token);
26
+ return () => {
27
+ this.listeners.delete(listener);
28
+ };
29
+ }
30
+ }
package/dist/client.d.ts CHANGED
@@ -1,27 +1,33 @@
1
1
  /**
2
- * Core GraphQL client for HTTP queries and mutations
2
+ * Minimal HTTP GraphQL client. Reads its bearer token from `AuthState` so the
3
+ * WebSocket subscription manager and HTTP client always agree on who's
4
+ * authenticated. The `login` / `register` / `connectUdpProxy` /
5
+ * `sendActorUpdate` / etc. shortcuts that used to live here are gone -
6
+ * everything goes through the typed sub-clients on `CrowdyClient` (e.g.
7
+ * `client.auth.login`, `client.udp.sendActorUpdate`).
3
8
  */
4
- import type { AuthResponse, UdpProxyConnectionStatus, ActorUpdateRequestInput, VoxelUpdateRequestInput, ClientAudioPacketInput, ClientTextPacketInput, ClientEventNotificationInput } from './types.js';
9
+ import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
10
+ import { AuthState } from './auth-state.js';
11
+ export interface GraphQLClientConfig {
12
+ graphqlEndpoint?: string;
13
+ timeout?: number;
14
+ }
5
15
  export declare class GraphQLClient {
6
- private token;
7
- private graphqlEndpoint;
8
- private timeout;
9
- constructor(config?: {
10
- graphqlEndpoint?: string;
11
- timeout?: number;
12
- });
13
- setAuthToken(token: string | null): void;
14
- getAuthToken(): string | null;
16
+ private readonly graphqlEndpoint;
17
+ private readonly timeout;
18
+ private readonly authState;
19
+ constructor(config: GraphQLClientConfig | undefined, authState: AuthState);
20
+ getEndpoint(): string;
21
+ /**
22
+ * Execute a typed GraphQL operation produced by codegen and return the
23
+ * `data` payload. Throws on transport errors, GraphQL errors, or timeouts.
24
+ */
25
+ request<TResult, TVariables>(document: TypedDocumentNode<TResult, TVariables>, variables?: TVariables): Promise<TResult>;
26
+ /**
27
+ * Internal escape hatch for raw query strings (used by hand-written
28
+ * adapters that haven't migrated to typed documents yet). Prefer
29
+ * `request()` with a `TypedDocumentNode`.
30
+ */
15
31
  query<T = any>(query: string, variables?: Record<string, any>): Promise<T>;
16
- login(email: string, password: string): Promise<AuthResponse>;
17
- register(email: string, password: string, gamertag?: string): Promise<AuthResponse>;
18
- connectUdpProxy(): Promise<UdpProxyConnectionStatus>;
19
- disconnectUdpProxy(): Promise<boolean>;
20
- getConnectionStatus(): Promise<UdpProxyConnectionStatus>;
21
- sendActorUpdate(input: ActorUpdateRequestInput): Promise<boolean>;
22
- sendVoxelUpdate(input: VoxelUpdateRequestInput): Promise<boolean>;
23
- sendAudioPacket(input: ClientAudioPacketInput): Promise<boolean>;
24
- sendTextPacket(input: ClientTextPacketInput): Promise<boolean>;
25
- sendClientEvent(input: ClientEventNotificationInput): Promise<boolean>;
26
32
  }
27
33
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,4BAA4B,EAC7B,MAAM,YAAY,CAAC;AAEpB,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,OAAO,CAAS;gBAEZ,MAAM,GAAE;QAAE,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAO;IAKvE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAIxC,YAAY,IAAI,MAAM,GAAG,IAAI;IAIvB,KAAK,CAAC,CAAC,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IA+C9E,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IA2B7D,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IA2BnF,eAAe,IAAI,OAAO,CAAC,wBAAwB,CAAC;IAgBpD,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAC;IAWtC,mBAAmB,IAAI,OAAO,CAAC,wBAAwB,CAAC;IAgBxD,eAAe,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IA0BjE,eAAe,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;IA4BjE,eAAe,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC;IA0BhE,cAAc,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,OAAO,CAAC;IA0B9D,eAAe,CAAC,KAAK,EAAE,4BAA4B,GAAG,OAAO,CAAC,OAAO,CAAC;CA0B7E"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,WAAW,mBAAmB;IAClC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;gBAE1B,MAAM,EAAE,mBAAmB,YAAK,EAAE,SAAS,EAAE,SAAS;IAOlE,WAAW,IAAI,MAAM;IAIrB;;;OAGG;IACG,OAAO,CAAC,OAAO,EAAE,UAAU,EAC/B,QAAQ,EAAE,iBAAiB,CAAC,OAAO,EAAE,UAAU,CAAC,EAChD,SAAS,CAAC,EAAE,UAAU,GACrB,OAAO,CAAC,OAAO,CAAC;IAKnB;;;;OAIG;IACG,KAAK,CAAC,CAAC,GAAG,GAAG,EACjB,KAAK,EAAE,MAAM,EACb,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAClC,OAAO,CAAC,CAAC,CAAC;CAgDd"}