@crowdedkingdomstudios/crowdyjs 2.0.0 → 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.
- package/README.md +433 -6
- package/dist/auth-state.d.ts +21 -0
- package/dist/auth-state.d.ts.map +1 -0
- package/dist/auth-state.js +30 -0
- package/dist/client.d.ts +27 -21
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +27 -196
- package/dist/crowdy-client.d.ts +44 -24
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +42 -81
- package/dist/domains/actors.d.ts +22 -0
- package/dist/domains/actors.d.ts.map +1 -0
- package/dist/domains/actors.js +42 -0
- package/dist/domains/appAccess.d.ts +23 -0
- package/dist/domains/appAccess.d.ts.map +1 -0
- package/dist/domains/appAccess.js +42 -0
- package/dist/domains/apps.d.ts +27 -0
- package/dist/domains/apps.d.ts.map +1 -0
- package/dist/domains/apps.js +49 -0
- package/dist/domains/auth.d.ts +32 -0
- package/dist/domains/auth.d.ts.map +1 -0
- package/dist/domains/auth.js +70 -0
- package/dist/domains/billing.d.ts +17 -0
- package/dist/domains/billing.d.ts.map +1 -0
- package/dist/domains/billing.js +31 -0
- package/dist/domains/chunks.d.ts +20 -0
- package/dist/domains/chunks.d.ts.map +1 -0
- package/dist/domains/chunks.js +40 -0
- package/dist/domains/organizations.d.ts +33 -0
- package/dist/domains/organizations.d.ts.map +1 -0
- package/dist/domains/organizations.js +90 -0
- package/dist/domains/payments.d.ts +20 -0
- package/dist/domains/payments.d.ts.map +1 -0
- package/dist/domains/payments.js +28 -0
- package/dist/domains/quotas.d.ts +20 -0
- package/dist/domains/quotas.d.ts.map +1 -0
- package/dist/domains/quotas.js +34 -0
- package/dist/domains/serverStatus.d.ts +21 -0
- package/dist/domains/serverStatus.d.ts.map +1 -0
- package/dist/domains/serverStatus.js +32 -0
- package/dist/domains/state.d.ts +16 -0
- package/dist/domains/state.d.ts.map +1 -0
- package/dist/domains/state.js +27 -0
- package/dist/domains/teleport.d.ts +13 -0
- package/dist/domains/teleport.d.ts.map +1 -0
- package/dist/domains/teleport.js +15 -0
- package/dist/domains/udp.d.ts +32 -0
- package/dist/domains/udp.d.ts.map +1 -0
- package/dist/domains/udp.js +55 -0
- package/dist/domains/users.d.ts +24 -0
- package/dist/domains/users.d.ts.map +1 -0
- package/dist/domains/users.js +53 -0
- package/dist/domains/voxels.d.ts +17 -0
- package/dist/domains/voxels.d.ts.map +1 -0
- package/dist/domains/voxels.js +31 -0
- package/dist/generated/graphql.d.ts +3571 -0
- package/dist/generated/graphql.d.ts.map +1 -0
- package/dist/generated/graphql.js +181 -0
- package/dist/index.d.ts +33 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -1
- package/dist/subscriptions.d.ts +36 -18
- package/dist/subscriptions.d.ts.map +1 -1
- package/dist/subscriptions.js +95 -181
- 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
|
|
5
|
-
communication through a single `CrowdyClient` instance.
|
|
6
|
-
|
|
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
|
|
|
@@ -163,6 +170,33 @@ 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:
|
|
@@ -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>` |
|
|
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
|
-
*
|
|
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 {
|
|
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
|
|
7
|
-
private
|
|
8
|
-
private
|
|
9
|
-
constructor(config
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|