@crowdedkingdomstudios/crowdyjs 2.1.2 → 3.0.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/MIGRATION.md +26 -0
- package/README.md +110 -772
- package/dist/auth-state.d.ts +6 -16
- package/dist/auth-state.d.ts.map +1 -1
- package/dist/auth-state.js +9 -26
- package/dist/client.d.ts +14 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +22 -17
- package/dist/crowdy-client.d.ts +21 -3
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +37 -20
- package/dist/domains/serverStatus.d.ts +2 -1
- package/dist/domains/serverStatus.d.ts.map +1 -1
- package/dist/domains/serverStatus.js +5 -1
- package/dist/domains/udp.d.ts +19 -2
- package/dist/domains/udp.d.ts.map +1 -1
- package/dist/domains/udp.js +38 -0
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/generated/graphql.d.ts +404 -12
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +2 -0
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +1 -0
- package/dist/realtime.d.ts +86 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +270 -0
- package/dist/session.d.ts +27 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +61 -0
- package/dist/subscriptions.d.ts +1 -48
- package/dist/subscriptions.d.ts.map +1 -1
- package/dist/subscriptions.js +1 -192
- package/dist/utils.d.ts +12 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +50 -0
- package/dist/world.d.ts +35 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +88 -0
- package/package.json +11 -3
package/README.md
CHANGED
|
@@ -1,819 +1,157 @@
|
|
|
1
1
|
# CrowdyJS SDK
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Browser-first SDK for Crowded Kingdoms game clients. CrowdyJS wraps the GraphQL
|
|
4
|
+
API, the UDP proxy subscription, auth/session state, and spatial send helpers in
|
|
5
|
+
one typed client.
|
|
6
6
|
|
|
7
|
-
##
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
7
|
+
## Install
|
|
16
8
|
|
|
17
9
|
```bash
|
|
18
10
|
npm install @crowdedkingdomstudios/crowdyjs
|
|
19
11
|
```
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
polyfill such as the `ws` package:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npm install ws
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
```javascript
|
|
31
|
-
import WebSocket from 'ws';
|
|
32
|
-
globalThis.WebSocket = WebSocket;
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Place this **before** importing `CrowdyClient`.
|
|
36
|
-
|
|
37
|
-
### Browser
|
|
38
|
-
|
|
39
|
-
No extra setup needed -- the SDK uses the built-in `WebSocket`.
|
|
13
|
+
CrowdyJS v3 targets browsers by default and uses native `fetch`, `WebSocket`,
|
|
14
|
+
`crypto`, `btoa`, and `atob`. Node tools can still use the SDK, but must provide
|
|
15
|
+
browser-compatible globals when they open realtime connections.
|
|
40
16
|
|
|
41
17
|
## Quick Start
|
|
42
18
|
|
|
43
|
-
```
|
|
44
|
-
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
19
|
+
```ts
|
|
20
|
+
import {
|
|
21
|
+
BrowserLocalStorageTokenStore,
|
|
22
|
+
createCrowdyClient,
|
|
23
|
+
} from '@crowdedkingdomstudios/crowdyjs';
|
|
24
|
+
|
|
25
|
+
const client = createCrowdyClient({
|
|
26
|
+
httpUrl: 'https://api.example.com/graphql',
|
|
27
|
+
wsUrl: 'wss://api.example.com/graphql',
|
|
28
|
+
tokenStore: new BrowserLocalStorageTokenStore(),
|
|
29
|
+
realtime: {
|
|
30
|
+
retryAttempts: 8,
|
|
31
|
+
waitTimeoutMs: 5000,
|
|
32
|
+
},
|
|
49
33
|
});
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
If omitted, both endpoints default to `localhost:3000/graphql`.
|
|
53
|
-
|
|
54
|
-
## Connection Lifecycle
|
|
55
|
-
|
|
56
|
-
The SDK follows a four-step lifecycle that matches the server protocol:
|
|
57
34
|
|
|
58
|
-
|
|
59
|
-
1. Login --> obtain a game token
|
|
60
|
-
2. Subscribe --> auto-opens UDP proxy session
|
|
61
|
-
3. Register --> tell the game server your chunk position
|
|
62
|
-
4. Send updates --> replicated to other clients in range
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### 1. Login
|
|
66
|
-
|
|
67
|
-
```javascript
|
|
68
|
-
const auth = await client.login('user@example.com', 'password');
|
|
69
|
-
// auth.token -- 64-char hex game token (set automatically)
|
|
70
|
-
// auth.user.email -- logged-in user
|
|
71
|
-
```
|
|
35
|
+
await client.session.restore();
|
|
72
36
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const auth = await client.register('user@example.com', 'password', 'MyGamertag');
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 2. Subscribe to Notifications
|
|
80
|
-
|
|
81
|
-
Register one or more notification handlers. The first handler automatically
|
|
82
|
-
opens a WebSocket subscription and a UDP proxy session to the game server --
|
|
83
|
-
no explicit `connectUdpProxy()` call is needed.
|
|
84
|
-
|
|
85
|
-
```javascript
|
|
86
|
-
const unsub = client.onActorUpdate((notification) => {
|
|
87
|
-
console.log('Actor:', notification.uuid);
|
|
88
|
-
console.log('State:', notification.state);
|
|
89
|
-
console.log('Chunk:', notification.chunkX, notification.chunkY, notification.chunkZ);
|
|
90
|
-
console.log('Time:', notification.epochMillis);
|
|
91
|
-
});
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Handlers are unsubscribed by calling the returned function:
|
|
95
|
-
|
|
96
|
-
```javascript
|
|
97
|
-
unsub(); // stop receiving ActorUpdateNotification
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
When all handlers are removed the WebSocket is closed automatically.
|
|
101
|
-
|
|
102
|
-
### 3. Register in a Chunk
|
|
103
|
-
|
|
104
|
-
Before other clients can see you, send an initial actor update so the game
|
|
105
|
-
server knows which chunk you occupy. Use a minimal base64 payload (the
|
|
106
|
-
server requires a non-empty `state`).
|
|
107
|
-
|
|
108
|
-
#### Generating a UUID
|
|
109
|
-
|
|
110
|
-
Every client must create its own UUID as a **random 32-byte UTF-8 string**.
|
|
111
|
-
This ensures each client produces a globally unique identifier without
|
|
112
|
-
relying on a central registry.
|
|
113
|
-
|
|
114
|
-
```javascript
|
|
115
|
-
// Node.js
|
|
116
|
-
const MY_UUID = crypto.randomBytes(32).toString('hex').slice(0, 32);
|
|
117
|
-
|
|
118
|
-
// Browser
|
|
119
|
-
const MY_UUID = Array.from(crypto.getRandomValues(new Uint8Array(16)))
|
|
120
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
121
|
-
.join('');
|
|
122
|
-
```
|
|
37
|
+
if (!client.session.getToken()) {
|
|
38
|
+
await client.auth.login({ email: 'player@example.com', password: 'secret' });
|
|
39
|
+
}
|
|
123
40
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
41
|
+
const bootstrap = await client.serverStatus.gameClientBootstrap('1');
|
|
42
|
+
console.log(bootstrap.versionInfo.minimumClientVersion);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Game Loop Lifecycle
|
|
46
|
+
|
|
47
|
+
1. Authenticate with `client.auth.login()` or restore a previous token through
|
|
48
|
+
`client.session.restore()`.
|
|
49
|
+
2. Subscribe to UDP proxy notifications with `client.udp.subscribe()` or
|
|
50
|
+
`client.realtime.connect()`.
|
|
51
|
+
3. Join a chunk by sending an initial actor update.
|
|
52
|
+
4. Send actor, voxel, text, audio, and client-event updates through `client.udp`
|
|
53
|
+
or the higher-level `client.world(appId)` helpers.
|
|
54
|
+
5. Call `client.udp.disconnect()` when leaving the world, then `client.close()`
|
|
55
|
+
when disposing the SDK instance.
|
|
56
|
+
|
|
57
|
+
## Realtime Notifications
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const unsubscribe = client.udp.subscribe({
|
|
61
|
+
actorUpdate: (event) => {
|
|
62
|
+
console.log(event.uuid, event.state);
|
|
63
|
+
},
|
|
64
|
+
genericError: (event) => {
|
|
65
|
+
console.warn(event.sequenceNumber, event.errorCode);
|
|
66
|
+
},
|
|
67
|
+
connectionEvent: (event) => {
|
|
68
|
+
console.warn(event.code, event.message);
|
|
69
|
+
},
|
|
70
|
+
error: (error) => {
|
|
71
|
+
console.error(error.code, error.message);
|
|
72
|
+
},
|
|
132
73
|
});
|
|
133
|
-
```
|
|
134
74
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
### 4. Send Actor Updates
|
|
139
|
-
|
|
140
|
-
```javascript
|
|
141
|
-
// Build your binary state and base64-encode it
|
|
142
|
-
const stateBuffer = new ArrayBuffer(96);
|
|
143
|
-
const view = new DataView(stateBuffer);
|
|
144
|
-
view.setFloat32(0, posX, true);
|
|
145
|
-
view.setFloat32(4, posY, true);
|
|
146
|
-
view.setFloat32(8, posZ, true);
|
|
147
|
-
// ... fill remaining fields
|
|
148
|
-
|
|
149
|
-
const base64State = btoa(String.fromCharCode(...new Uint8Array(stateBuffer)));
|
|
150
|
-
|
|
151
|
-
await client.sendActorUpdate({
|
|
152
|
-
appId: 0,
|
|
153
|
-
chunk: { x: 0, y: 0, z: 0 },
|
|
154
|
-
distance: 8,
|
|
155
|
-
decayRate: 0,
|
|
156
|
-
uuid: MY_UUID,
|
|
157
|
-
state: base64State,
|
|
158
|
-
sequenceNumber: 2,
|
|
75
|
+
client.realtime.onStatus((status) => {
|
|
76
|
+
console.log('realtime:', status);
|
|
159
77
|
});
|
|
160
78
|
```
|
|
161
79
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
await client.disconnectUdpProxy(); // release the UDP session
|
|
166
|
-
client.close(); // close WebSocket + clear state
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
Unsubscribing from notifications stops delivery but does **not** release
|
|
170
|
-
the UDP session. Call `disconnectUdpProxy()` explicitly, or the server
|
|
171
|
-
will release it after 30 seconds of inactivity.
|
|
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
|
-
|
|
200
|
-
## Subscription Handlers
|
|
201
|
-
|
|
202
|
-
All spatial notification types share a uniform header:
|
|
203
|
-
|
|
204
|
-
| Field | Type | Description |
|
|
205
|
-
|-------|------|-------------|
|
|
206
|
-
| `appId` | `string` | App ID (tenant / world identifier) |
|
|
207
|
-
| `chunkX` | `string` | Chunk X coordinate |
|
|
208
|
-
| `chunkY` | `string` | Chunk Y coordinate |
|
|
209
|
-
| `chunkZ` | `string` | Chunk Z coordinate |
|
|
210
|
-
| `distance` | `number` | Replication distance (0-8) |
|
|
211
|
-
| `decayRate` | `number` | Delivery decay (0-5) |
|
|
212
|
-
| `uuid` | `string` | Random 32-byte UTF-8 sender UUID |
|
|
213
|
-
| `sequenceNumber` | `number` | uint8 (0-255), wraps |
|
|
214
|
-
| `epochMillis` | `string` | Server UTC timestamp in ms |
|
|
215
|
-
|
|
216
|
-
Each handler receives a fully-typed notification object:
|
|
217
|
-
|
|
218
|
-
```javascript
|
|
219
|
-
client.onActorUpdate((n) => { /* n: ActorUpdateNotification -- adds: state */ });
|
|
220
|
-
client.onActorUpdateResponse((n) => { /* n: ActorUpdateResponse */ });
|
|
221
|
-
client.onVoxelUpdate((n) => { /* n: VoxelUpdateNotification -- adds: voxelX/Y/Z, voxelType, voxelState */ });
|
|
222
|
-
client.onVoxelUpdateResponse((n) => { /* n: VoxelUpdateResponse */ });
|
|
223
|
-
client.onClientAudio((n) => { /* n: ClientAudioNotification -- adds: audioData */ });
|
|
224
|
-
client.onClientText((n) => { /* n: ClientTextNotification -- adds: text */ });
|
|
225
|
-
client.onClientEvent((n) => { /* n: ClientEventNotification -- adds: eventType, state */ });
|
|
226
|
-
client.onServerEvent((n) => { /* n: ServerEventNotification -- adds: eventType, state */ });
|
|
227
|
-
client.onGenericError((e) => { /* e: GenericErrorResponse -- sequenceNumber, errorCode only */ });
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
`GenericErrorResponse` is the only type without the spatial header; it has
|
|
231
|
-
just `sequenceNumber` and `errorCode`.
|
|
232
|
-
|
|
233
|
-
## Input Parameters
|
|
234
|
-
|
|
235
|
-
### Common fields
|
|
236
|
-
|
|
237
|
-
All mutation inputs share these fields:
|
|
238
|
-
|
|
239
|
-
| Field | Type | Required | Default | Description |
|
|
240
|
-
|-------|------|----------|---------|-------------|
|
|
241
|
-
| `appId` | `number` | yes | -- | App ID (tenant / world identifier) |
|
|
242
|
-
| `chunk` | `{ x, y, z }` | yes | -- | Chunk coordinates (numbers) |
|
|
243
|
-
| `uuid` | `string` | yes | -- | Random 32-byte UTF-8 string (see [Generating a UUID](#generating-a-uuid)) |
|
|
244
|
-
| `distance` | `number` | no | `8` | Replication range (0-8 chunks, Chebyshev) |
|
|
245
|
-
| `decayRate` | `number` | no | `0` | Delivery decay (see table below) |
|
|
246
|
-
| `sequenceNumber` | `number` | no | `0` | uint8 (0-255) for correlation |
|
|
247
|
-
|
|
248
|
-
### `decayRate` values
|
|
249
|
-
|
|
250
|
-
| Value | Name | Behavior |
|
|
251
|
-
|-------|------|----------|
|
|
252
|
-
| 0 | None | All clients within `distance` receive every message |
|
|
253
|
-
| 1 | Exponential | Each ring receives half the messages of the previous ring |
|
|
254
|
-
| 2 | Linear 50% | Furthest ring receives 50% of messages |
|
|
255
|
-
| 3 | Linear 25% | Furthest ring receives 25% of messages |
|
|
256
|
-
| 4 | Linear 10% | Furthest ring receives 10% of messages |
|
|
257
|
-
| 5 | Linear 5% | Furthest ring receives 5% of messages |
|
|
258
|
-
|
|
259
|
-
### `sendActorUpdate`
|
|
260
|
-
|
|
261
|
-
| Field | Type | Description |
|
|
262
|
-
|-------|------|-------------|
|
|
263
|
-
| `state` | `string` | Base64-encoded binary state (must be non-empty) |
|
|
264
|
-
|
|
265
|
-
### `sendVoxelUpdate`
|
|
266
|
-
|
|
267
|
-
| Field | Type | Description |
|
|
268
|
-
|-------|------|-------------|
|
|
269
|
-
| `voxel` | `{ x, y, z }` | Voxel position within the chunk |
|
|
270
|
-
| `voxelType` | `number` | Voxel type ID |
|
|
271
|
-
| `voxelState` | `string` | Base64-encoded voxel state |
|
|
272
|
-
|
|
273
|
-
### `sendAudioPacket`
|
|
274
|
-
|
|
275
|
-
| Field | Type | Description |
|
|
276
|
-
|-------|------|-------------|
|
|
277
|
-
| `audioData` | `string` | Base64-encoded compressed audio |
|
|
80
|
+
The SDK uses the `graphql-transport-ws` protocol through `graphql-ws`, reconnects
|
|
81
|
+
with backoff, re-reads the current token before reconnecting, and resubscribes to
|
|
82
|
+
the generated `UdpNotifications` document.
|
|
278
83
|
|
|
279
|
-
|
|
84
|
+
## Raw UDP Sends
|
|
280
85
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
| Field | Type | Description |
|
|
288
|
-
|-------|------|-------------|
|
|
289
|
-
| `eventType` | `number` | Custom event type ID |
|
|
290
|
-
| `state` | `string` | Base64-encoded event state |
|
|
291
|
-
|
|
292
|
-
## Complete Example
|
|
293
|
-
|
|
294
|
-
```javascript
|
|
295
|
-
import WebSocket from 'ws';
|
|
296
|
-
globalThis.WebSocket = WebSocket;
|
|
297
|
-
|
|
298
|
-
import { CrowdyClient } from '@crowdedkingdomstudios/crowdyjs';
|
|
299
|
-
|
|
300
|
-
const client = new CrowdyClient({
|
|
301
|
-
graphqlEndpoint: 'https://your-server.com/graphql',
|
|
302
|
-
wsEndpoint: 'wss://your-server.com/graphql',
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
const MY_UUID = crypto.randomBytes(32).toString('hex').slice(0, 32);
|
|
306
|
-
|
|
307
|
-
// 1. Login
|
|
308
|
-
await client.login('user@example.com', 'password');
|
|
309
|
-
|
|
310
|
-
// 2. Subscribe (auto-opens UDP proxy session)
|
|
311
|
-
const unsubActors = client.onActorUpdate((n) => {
|
|
312
|
-
console.log(`Actor ${n.uuid} at chunk (${n.chunkX},${n.chunkY},${n.chunkZ})`);
|
|
313
|
-
console.log(` state=${n.state} seq=${n.sequenceNumber} t=${n.epochMillis}`);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
const unsubErrors = client.onGenericError((e) => {
|
|
317
|
-
console.error(`Error: ${e.errorCode} (seq ${e.sequenceNumber})`);
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// 3. Register in chunk
|
|
321
|
-
await client.sendActorUpdate({
|
|
322
|
-
appId: 0,
|
|
323
|
-
chunk: { x: 0, y: 0, z: 0 },
|
|
324
|
-
distance: 8,
|
|
325
|
-
uuid: MY_UUID,
|
|
86
|
+
```ts
|
|
87
|
+
const response = await client.udp.sendActorUpdateAndWait({
|
|
88
|
+
appId: '1',
|
|
89
|
+
chunk: { x: '0', y: '0', z: '0' },
|
|
90
|
+
uuid: '0123456789abcdef0123456789abcdef',
|
|
326
91
|
state: 'AA==',
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
// 4. Send updates in a loop
|
|
331
|
-
let seq = 2;
|
|
332
|
-
const interval = setInterval(async () => {
|
|
333
|
-
const buf = new Uint8Array(96);
|
|
334
|
-
crypto.getRandomValues(buf);
|
|
335
|
-
const state = btoa(String.fromCharCode(...buf));
|
|
336
|
-
|
|
337
|
-
await client.sendActorUpdate({
|
|
338
|
-
appId: 0,
|
|
339
|
-
chunk: { x: 0, y: 0, z: 0 },
|
|
340
|
-
distance: 8,
|
|
341
|
-
uuid: MY_UUID,
|
|
342
|
-
state,
|
|
343
|
-
sequenceNumber: seq++ % 256,
|
|
344
|
-
});
|
|
345
|
-
}, 100);
|
|
346
|
-
|
|
347
|
-
// 5. Cleanup
|
|
348
|
-
clearInterval(interval);
|
|
349
|
-
unsubActors();
|
|
350
|
-
unsubErrors();
|
|
351
|
-
await client.disconnectUdpProxy();
|
|
352
|
-
client.close();
|
|
353
|
-
```
|
|
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
|
-
}
|
|
92
|
+
distance: 8,
|
|
93
|
+
decayRate: 1,
|
|
505
94
|
});
|
|
506
|
-
```
|
|
507
95
|
|
|
508
|
-
|
|
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);
|
|
96
|
+
console.log(response.__typename, response.sequenceNumber);
|
|
521
97
|
```
|
|
522
98
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
99
|
+
The plain `sendActorUpdate`, `sendVoxelUpdate`, `sendAudioPacket`,
|
|
100
|
+
`sendTextPacket`, and `sendClientEvent` methods return the GraphQL mutation
|
|
101
|
+
result immediately. The `AndWait` variants allocate a `sequenceNumber` when one
|
|
102
|
+
is missing and wait for either a matching notification or `GenericErrorResponse`.
|
|
526
103
|
|
|
527
|
-
|
|
104
|
+
## World Helpers
|
|
528
105
|
|
|
529
|
-
|
|
106
|
+
```ts
|
|
107
|
+
const world = client.world('1');
|
|
108
|
+
const actor = world.actor();
|
|
530
109
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
├── ActorSender setInterval sends; reads pose from your game
|
|
535
|
-
├── NetworkHealth visibility / WS-stale monitor + heal
|
|
536
|
-
└── (uses) CrowdyClient
|
|
110
|
+
await actor.join({ x: '0', y: '0', z: '0' });
|
|
111
|
+
await actor.sendState('AA==');
|
|
112
|
+
await actor.sendText('hello nearby players');
|
|
537
113
|
```
|
|
538
114
|
|
|
539
|
-
|
|
540
|
-
|
|
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.
|
|
115
|
+
The world helpers are convenience wrappers for browser games. Advanced callers
|
|
116
|
+
can always use `client.udp.*` with generated GraphQL input types.
|
|
543
117
|
|
|
544
|
-
|
|
118
|
+
## Errors
|
|
545
119
|
|
|
546
|
-
|
|
120
|
+
Transport and protocol failures use structured error classes:
|
|
547
121
|
|
|
548
|
-
- `
|
|
549
|
-
- `
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
- `
|
|
553
|
-
|
|
554
|
-
- Per-remote `lastUpdateAt` / `msSinceUpdate`.
|
|
555
|
-
- `visibilityState`, `hidden`, `lastWsResubscribeAt`, `reaperPaused`.
|
|
122
|
+
- `CrowdyHttpError`
|
|
123
|
+
- `CrowdyGraphQLError`
|
|
124
|
+
- `CrowdyNetworkError`
|
|
125
|
+
- `CrowdyTimeoutError`
|
|
126
|
+
- `CrowdyRealtimeError`
|
|
127
|
+
- `CrowdyProtocolError`
|
|
556
128
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
than a wall of streaming logs.
|
|
129
|
+
`CrowdyGraphQLError` preserves every GraphQL error, including `path` and
|
|
130
|
+
`extensions.code`.
|
|
560
131
|
|
|
561
|
-
##
|
|
132
|
+
## Low-Level GraphQL Access
|
|
562
133
|
|
|
563
|
-
|
|
134
|
+
Game-client methods are first-class, but generated operations are still
|
|
135
|
+
available through the transport escape hatch:
|
|
564
136
|
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
```
|
|
137
|
+
```ts
|
|
138
|
+
import { VersionInfoDocument } from '@crowdedkingdomstudios/crowdyjs/generated';
|
|
568
139
|
|
|
569
|
-
|
|
570
|
-
|--------|------|---------|-------------|
|
|
571
|
-
| `graphqlEndpoint` | `string` | `http://localhost:3000/graphql` | HTTP endpoint for mutations/queries |
|
|
572
|
-
| `wsEndpoint` | `string` | `ws://localhost:3000/graphql` | WebSocket endpoint for subscriptions |
|
|
573
|
-
| `timeout` | `number` | `60000` | HTTP request timeout in ms |
|
|
574
|
-
|
|
575
|
-
### Authentication
|
|
576
|
-
|
|
577
|
-
| Method | Returns | Description |
|
|
578
|
-
|--------|---------|-------------|
|
|
579
|
-
| `login(email, password)` | `Promise<AuthResponse>` | Login and store the game token |
|
|
580
|
-
| `register(email, password, gamertag?)` | `Promise<AuthResponse>` | Register and store the game token |
|
|
581
|
-
| `getAuthToken()` | `string \| null` | Get the current game token |
|
|
582
|
-
|
|
583
|
-
### UDP Proxy
|
|
584
|
-
|
|
585
|
-
| Method | Returns | Description |
|
|
586
|
-
|--------|---------|-------------|
|
|
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()`. |
|
|
589
|
-
| `getConnectionStatus()` | `Promise<UdpProxyConnectionStatus>` | Check if a UDP session is active |
|
|
590
|
-
|
|
591
|
-
### Mutations
|
|
592
|
-
|
|
593
|
-
| Method | Returns | Description |
|
|
594
|
-
|--------|---------|-------------|
|
|
595
|
-
| `sendActorUpdate(input)` | `Promise<boolean>` | Send an actor state update |
|
|
596
|
-
| `sendVoxelUpdate(input)` | `Promise<boolean>` | Modify a voxel in a chunk |
|
|
597
|
-
| `sendAudioPacket(input)` | `Promise<boolean>` | Send voice audio data |
|
|
598
|
-
| `sendTextPacket(input)` | `Promise<boolean>` | Send chat text |
|
|
599
|
-
| `sendClientEvent(input)` | `Promise<boolean>` | Send a custom event |
|
|
600
|
-
|
|
601
|
-
### Subscriptions
|
|
602
|
-
|
|
603
|
-
| Method | Handler receives | Description |
|
|
604
|
-
|--------|-----------------|-------------|
|
|
605
|
-
| `onActorUpdate(handler)` | `ActorUpdateNotification` | Another client's actor state |
|
|
606
|
-
| `onActorUpdateResponse(handler)` | `ActorUpdateResponse` | Server ack for your actor update |
|
|
607
|
-
| `onVoxelUpdate(handler)` | `VoxelUpdateNotification` | A voxel was modified |
|
|
608
|
-
| `onVoxelUpdateResponse(handler)` | `VoxelUpdateResponse` | Server ack for your voxel update |
|
|
609
|
-
| `onClientAudio(handler)` | `ClientAudioNotification` | Voice audio from another client |
|
|
610
|
-
| `onClientText(handler)` | `ClientTextNotification` | Chat text from another client |
|
|
611
|
-
| `onClientEvent(handler)` | `ClientEventNotification` | Custom event from another client |
|
|
612
|
-
| `onServerEvent(handler)` | `ServerEventNotification` | Event from the game server |
|
|
613
|
-
| `onGenericError(handler)` | `GenericErrorResponse` | Error from the server |
|
|
614
|
-
|
|
615
|
-
All subscription methods return an `UnsubscribeFn` -- call it to remove
|
|
616
|
-
the handler.
|
|
617
|
-
|
|
618
|
-
### Cleanup
|
|
619
|
-
|
|
620
|
-
| Method | Description |
|
|
621
|
-
|--------|-------------|
|
|
622
|
-
| `close()` | Close the WebSocket, remove all handlers, clear auth state |
|
|
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 });
|
|
140
|
+
const data = await client.graphql.request(VersionInfoDocument);
|
|
637
141
|
```
|
|
638
142
|
|
|
639
|
-
|
|
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
|
-
```
|
|
143
|
+
Most consumers should prefer the methods on `client.auth`, `client.udp`,
|
|
144
|
+
`client.serverStatus`, `client.users`, `client.apps`, and `client.world()`.
|
|
681
145
|
|
|
682
|
-
|
|
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:
|
|
146
|
+
## Development
|
|
801
147
|
|
|
802
148
|
```bash
|
|
803
|
-
npm
|
|
804
|
-
npm run codegen
|
|
149
|
+
npm install
|
|
150
|
+
npm run codegen
|
|
151
|
+
npm run build
|
|
152
|
+
npm test
|
|
805
153
|
```
|
|
806
154
|
|
|
807
|
-
`npm run
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
The SDK is written in TypeScript and ships type declarations. All
|
|
812
|
-
notification interfaces, input types, and handler signatures are fully
|
|
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.
|
|
816
|
-
|
|
817
|
-
## License
|
|
818
|
-
|
|
819
|
-
MIT
|
|
155
|
+
`npm run codegen` syncs `../cks-graphql-api/schema.gql` when the API repo is
|
|
156
|
+
checked out beside this SDK. `npm run check:schema` fails if the committed SDL or
|
|
157
|
+
generated types drift from the API schema.
|