castle-web-sdk 0.4.4 → 0.4.5

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/src/storage.ts CHANGED
@@ -1,42 +1,15 @@
1
- import { getAuth, requireAuthToken } from "./auth";
2
- import { getDeckContext, requireDeckId } from "./context";
1
+ import {
2
+ type SharedScope,
3
+ type StorageBlob,
4
+ type StorageUpdate,
5
+ } from "./commands";
3
6
  import { CastleError } from "./errors";
4
- import { graphqlRequest } from "./graphql";
7
+ import { hostRequest } from "./transport";
5
8
  import type { Json } from "./types";
6
9
 
7
10
  const FLUSH_INTERVAL_MS = 2000;
8
11
 
9
12
  type EncodedValue = string | null;
10
- type StorageBlob = Record<string, string>;
11
- type SharedScope = "deck" | "user";
12
-
13
- interface DeckStorageData {
14
- deckStorage: StorageBlob;
15
- }
16
-
17
- interface UpdateDeckStorageData {
18
- updateDeckStorage: StorageBlob;
19
- }
20
-
21
- interface SharedDeckStorageData {
22
- sharedDeckStorage: StorageBlob;
23
- }
24
-
25
- interface UpdateSharedDeckStorageData {
26
- updateSharedDeckStorage: boolean;
27
- }
28
-
29
- interface StorageUpdate {
30
- [key: string]: Json;
31
- key: string;
32
- value: EncodedValue;
33
- }
34
-
35
- interface Bucket {
36
- scope: SharedScope;
37
- userId?: string;
38
- sessionId?: string;
39
- }
40
13
 
41
14
  interface PendingRead {
42
15
  resolve: (value: Json | null) => void;
@@ -44,7 +17,9 @@ interface PendingRead {
44
17
  }
45
18
 
46
19
  interface ReadBatch {
47
- bucket: Bucket;
20
+ bucketKey: string;
21
+ scope: SharedScope;
22
+ userId?: string;
48
23
  reads: Map<string, PendingRead[]>;
49
24
  scheduled: boolean;
50
25
  }
@@ -67,22 +42,24 @@ export interface SharedStorageApi {
67
42
  remove(scope: SharedScope, key: string): void;
68
43
  }
69
44
 
45
+ // Per-player private storage. The host stamps deckId/sessionId, so one deck
46
+ // session = one stable blob: load once, then serve reads from cache and batch
47
+ // writes on a 2s flush. Dirty writes overlay the server blob so an in-flight
48
+ // write isn't clobbered by the flush response.
70
49
  class PrivateStorageImpl implements StorageApi {
71
50
  private cache = new Map<string, Json>();
72
51
  private dirty = new Map<string, EncodedValue>();
73
52
  private loadPromise: Promise<void> | null = null;
74
- private loadedContextKey: string | null = null;
75
53
  private flushTimer: ReturnType<typeof setTimeout> | null = null;
76
54
 
77
55
  async get<T extends Json = Json>(key: string): Promise<T | null> {
78
- await this.ensureLoaded("Storage.get");
56
+ await this.ensureLoaded();
79
57
  return (this.cache.get(key) ?? null) as T | null;
80
58
  }
81
59
 
82
60
  set(key: string, value: Json): void {
83
- const encoded = encodeStorageValue(value, "Storage.set");
84
61
  this.cache.set(key, value);
85
- this.dirty.set(key, encoded);
62
+ this.dirty.set(key, encodeStorageValue(value, "Storage.set"));
86
63
  this.scheduleFlush();
87
64
  }
88
65
 
@@ -92,30 +69,9 @@ class PrivateStorageImpl implements StorageApi {
92
69
  this.scheduleFlush();
93
70
  }
94
71
 
95
- private async flush(): Promise<void> {
96
- if (this.dirty.size === 0) return;
97
- const context = await currentContext("Storage.flush");
98
- const snapshot = new Map(this.dirty);
99
- const data = await storageGraphql<UpdateDeckStorageData>(
100
- UPDATE_DECK_STORAGE_MUTATION,
101
- {
102
- deckId: context.deckId,
103
- sessionId: context.sessionId,
104
- updates: updatesFromDirty(snapshot),
105
- },
106
- "updateDeckStorage",
107
- );
108
- this.applyServerBlob(
109
- data.updateDeckStorage,
110
- contextKey(context.deckId, context.sessionId),
111
- );
112
- clearAcknowledgedDirty(this.dirty, snapshot);
113
- this.overlayDirty();
114
- }
115
-
116
- private ensureLoaded(operation: string): Promise<void> {
72
+ private ensureLoaded(): Promise<void> {
117
73
  if (!this.loadPromise) {
118
- this.loadPromise = this.load(operation).catch((error: unknown) => {
74
+ this.loadPromise = this.load().catch((error: unknown) => {
119
75
  this.loadPromise = null;
120
76
  throw error;
121
77
  });
@@ -123,32 +79,27 @@ class PrivateStorageImpl implements StorageApi {
123
79
  return this.loadPromise;
124
80
  }
125
81
 
126
- private async load(operation: string): Promise<void> {
127
- const context = await currentContext(operation);
128
- const key = contextKey(context.deckId, context.sessionId);
129
- if (this.loadedContextKey === key) return;
130
- const data = await storageGraphql<DeckStorageData>(
131
- DECK_STORAGE_QUERY,
132
- { deckId: context.deckId, sessionId: context.sessionId },
133
- "deckStorage",
134
- );
135
- this.cache = decodeStorageBlob(data.deckStorage, operation);
136
- this.loadedContextKey = key;
82
+ private async load(): Promise<void> {
83
+ const { blob } = await hostRequest("deckStorage.load", {});
84
+ this.cache = decodeStorageBlob(blob, "Storage.get");
137
85
  this.overlayDirty();
138
86
  }
139
87
 
140
- private applyServerBlob(blob: StorageBlob, loadedContextKey: string): void {
88
+ private async flush(): Promise<void> {
89
+ if (this.dirty.size === 0) return;
90
+ const snapshot = new Map(this.dirty);
91
+ const { blob } = await hostRequest("deckStorage.update", {
92
+ updates: updatesFromDirty(snapshot),
93
+ });
141
94
  this.cache = decodeStorageBlob(blob, "Storage.flush");
142
- this.loadedContextKey = loadedContextKey;
95
+ clearAcknowledgedDirty(this.dirty, snapshot);
96
+ this.overlayDirty();
143
97
  }
144
98
 
145
99
  private overlayDirty(): void {
146
100
  for (const [key, encoded] of this.dirty) {
147
- if (encoded === null) {
148
- this.cache.delete(key);
149
- } else {
150
- this.cache.set(key, decodeStorageValue(encoded, "Storage.dirty"));
151
- }
101
+ if (encoded === null) this.cache.delete(key);
102
+ else this.cache.set(key, decodeStorageValue(encoded, "Storage.dirty"));
152
103
  }
153
104
  }
154
105
 
@@ -161,12 +112,14 @@ class PrivateStorageImpl implements StorageApi {
161
112
  }
162
113
  }
163
114
 
115
+ // Cross-player storage. Writes always target the current player (the host
116
+ // forces 'user'-scope writes to whoever is signed in), so dirty writes bucket
117
+ // under a fixed key; reads of another player's 'user' bucket take an explicit
118
+ // userId and never have pending writes. Reads coalesce per microtask into one
119
+ // command with a keys[] array.
164
120
  class SharedStorageImpl implements SharedStorageApi {
165
121
  private dirtyBuckets = new Map<string, Map<string, EncodedValue>>();
166
- private buckets = new Map<string, Bucket>();
167
122
  private readBatches = new Map<string, ReadBatch>();
168
- private pendingWrites = new Set<Promise<void>>();
169
- private pendingWriteErrors: unknown[] = [];
170
123
  private flushTimer: ReturnType<typeof setTimeout> | null = null;
171
124
 
172
125
  async get<T extends Json = Json>(
@@ -174,54 +127,30 @@ class SharedStorageImpl implements SharedStorageApi {
174
127
  userOrKey: string,
175
128
  maybeKey?: string,
176
129
  ): Promise<T | null> {
177
- const bucket = await readBucket(
178
- scope,
179
- userOrKey,
180
- maybeKey,
181
- "SharedStorage.get",
182
- );
130
+ assertScope(scope, "SharedStorage.get");
131
+ const userId = scope === "user" && maybeKey ? userOrKey : undefined;
183
132
  const key = maybeKey ?? userOrKey;
184
- await this.awaitPendingWrites("SharedStorage.get");
185
- const dirty = this.peekDirty(bucket, key);
133
+ const dirty = this.peekDirty(scope, userId, key);
186
134
  if (dirty !== undefined) {
187
135
  return (
188
136
  dirty === null ? null : decodeStorageValue(dirty, "SharedStorage.get")
189
137
  ) as T | null;
190
138
  }
191
- return this.queueRead<T>(bucket, key);
139
+ return this.queueRead<T>(scope, userId, key);
192
140
  }
193
141
 
194
142
  set(scope: SharedScope, key: string, value: Json): void {
195
- this.enqueueWrite(
196
- scope,
197
- key,
198
- encodeStorageValue(value, "SharedStorage.set"),
199
- "SharedStorage.set",
200
- );
143
+ assertScope(scope, "SharedStorage.set");
144
+ this.write(scope, key, encodeStorageValue(value, "SharedStorage.set"));
201
145
  }
202
146
 
203
147
  remove(scope: SharedScope, key: string): void {
204
- this.enqueueWrite(scope, key, null, "SharedStorage.remove");
148
+ assertScope(scope, "SharedStorage.remove");
149
+ this.write(scope, key, null);
205
150
  }
206
151
 
207
- private async flush(): Promise<void> {
208
- await this.awaitPendingWrites("SharedStorage.flush");
209
- if (this.dirtyBuckets.size === 0) return;
210
- const tasks = Array.from(this.dirtyBuckets, ([bucketKey, dirty]) =>
211
- this.flushBucket(bucketKey, new Map(dirty)),
212
- );
213
- await Promise.all(tasks);
214
- }
215
-
216
- private async write(
217
- scope: SharedScope,
218
- key: string,
219
- encoded: EncodedValue,
220
- operation: string,
221
- ): Promise<void> {
222
- const bucket = await writeBucket(scope, operation);
223
- const bucketKey = makeBucketKey(bucket);
224
- this.buckets.set(bucketKey, bucket);
152
+ private write(scope: SharedScope, key: string, encoded: EncodedValue): void {
153
+ const bucketKey = writeBucketKey(scope);
225
154
  const dirty =
226
155
  this.dirtyBuckets.get(bucketKey) ?? new Map<string, EncodedValue>();
227
156
  dirty.set(key, encoded);
@@ -229,45 +158,24 @@ class SharedStorageImpl implements SharedStorageApi {
229
158
  this.scheduleFlush();
230
159
  }
231
160
 
232
- private enqueueWrite(
161
+ private peekDirty(
233
162
  scope: SharedScope,
163
+ userId: string | undefined,
234
164
  key: string,
235
- encoded: EncodedValue,
236
- operation: string,
237
- ): void {
238
- const pending = this.write(scope, key, encoded, operation)
239
- .catch((error: unknown) => {
240
- this.pendingWriteErrors.push(error);
241
- })
242
- .finally(() => {
243
- this.pendingWrites.delete(pending);
244
- });
245
- this.pendingWrites.add(pending);
246
- }
247
-
248
- private async awaitPendingWrites(operation: string): Promise<void> {
249
- if (this.pendingWrites.size > 0) {
250
- await Promise.all(this.pendingWrites);
251
- }
252
- const error = this.pendingWriteErrors.shift();
253
- if (error instanceof Error) throw error;
254
- if (error) {
255
- throw storageError(
256
- "CASTLE_STORAGE_WRITE_FAILED",
257
- "Queued shared storage write failed.",
258
- operation,
259
- );
260
- }
165
+ ): EncodedValue | undefined {
166
+ if (scope === "user" && userId) return undefined;
167
+ return this.dirtyBuckets.get(writeBucketKey(scope))?.get(key);
261
168
  }
262
169
 
263
- private async queueRead<T extends Json>(
264
- bucket: Bucket,
170
+ private queueRead<T extends Json>(
171
+ scope: SharedScope,
172
+ userId: string | undefined,
265
173
  key: string,
266
174
  ): Promise<T | null> {
267
- const bucketKey = makeBucketKey(bucket);
175
+ const bucketKey = readBucketKey(scope, userId);
268
176
  let batch = this.readBatches.get(bucketKey);
269
177
  if (!batch) {
270
- batch = { bucket, reads: new Map(), scheduled: false };
178
+ batch = { bucketKey, scope, userId, reads: new Map(), scheduled: false };
271
179
  this.readBatches.set(bucketKey, batch);
272
180
  }
273
181
  const reads = batch.reads.get(key) ?? [];
@@ -277,11 +185,9 @@ class SharedStorageImpl implements SharedStorageApi {
277
185
  });
278
186
  if (!batch.scheduled) {
279
187
  batch.scheduled = true;
280
- queueMicrotask(() => {
281
- void this.flushReadBatch(bucketKey);
282
- });
188
+ queueMicrotask(() => void this.flushReadBatch(bucketKey));
283
189
  }
284
- return (await promise) as T | null;
190
+ return promise as Promise<T | null>;
285
191
  }
286
192
 
287
193
  private async flushReadBatch(bucketKey: string): Promise<void> {
@@ -290,13 +196,12 @@ class SharedStorageImpl implements SharedStorageApi {
290
196
  this.readBatches.delete(bucketKey);
291
197
  const keys = Array.from(batch.reads.keys());
292
198
  try {
293
- const context = await currentContext("SharedStorage.get");
294
- const data = await storageGraphql<SharedDeckStorageData>(
295
- SHARED_DECK_STORAGE_QUERY,
296
- sharedVariables(context.deckId, batch.bucket, { keys }),
297
- "sharedDeckStorage",
298
- );
299
- this.resolveReadBatch(batch, data.sharedDeckStorage);
199
+ const { blob } = await hostRequest("sharedDeckStorage.load", {
200
+ scope: batch.scope,
201
+ userId: batch.userId ?? null,
202
+ keys,
203
+ });
204
+ this.resolveReadBatch(batch, blob);
300
205
  } catch (error) {
301
206
  rejectReadBatch(batch, error);
302
207
  }
@@ -304,7 +209,8 @@ class SharedStorageImpl implements SharedStorageApi {
304
209
 
305
210
  private resolveReadBatch(batch: ReadBatch, blob: StorageBlob): void {
306
211
  for (const [key, reads] of batch.reads) {
307
- const encoded = this.peekDirty(batch.bucket, key) ?? blob[key] ?? null;
212
+ const encoded =
213
+ this.peekDirty(batch.scope, batch.userId, key) ?? blob[key] ?? null;
308
214
  const value =
309
215
  encoded === null
310
216
  ? null
@@ -313,36 +219,34 @@ class SharedStorageImpl implements SharedStorageApi {
313
219
  }
314
220
  }
315
221
 
222
+ private async flush(): Promise<void> {
223
+ if (this.dirtyBuckets.size === 0) return;
224
+ const tasks = Array.from(this.dirtyBuckets, ([bucketKey, dirty]) =>
225
+ this.flushBucket(bucketKey, new Map(dirty)),
226
+ );
227
+ await Promise.all(tasks);
228
+ }
229
+
316
230
  private async flushBucket(
317
231
  bucketKey: string,
318
232
  snapshot: Map<string, EncodedValue>,
319
233
  ): Promise<void> {
320
234
  if (snapshot.size === 0) return;
321
- const bucket = this.buckets.get(bucketKey);
322
- if (!bucket) return;
323
- const context = await currentContext("SharedStorage.flush");
324
- await storageGraphql<UpdateSharedDeckStorageData>(
325
- UPDATE_SHARED_DECK_STORAGE_MUTATION,
326
- sharedVariables(context.deckId, bucket, {
327
- updates: updatesFromDirty(snapshot),
328
- }),
329
- "updateSharedDeckStorage",
330
- );
235
+ await hostRequest("sharedDeckStorage.update", {
236
+ scope: scopeFromWriteKey(bucketKey),
237
+ updates: updatesFromDirty(snapshot),
238
+ });
331
239
  const dirty = this.dirtyBuckets.get(bucketKey);
332
240
  if (!dirty) return;
333
241
  clearAcknowledgedDirty(dirty, snapshot);
334
242
  if (dirty.size === 0) this.dirtyBuckets.delete(bucketKey);
335
243
  }
336
244
 
337
- private peekDirty(bucket: Bucket, key: string): EncodedValue | undefined {
338
- return this.dirtyBuckets.get(makeBucketKey(bucket))?.get(key);
339
- }
340
-
341
245
  private scheduleFlush(): void {
342
246
  if (this.flushTimer) return;
343
247
  this.flushTimer = setTimeout(() => {
344
248
  this.flushTimer = null;
345
- void this.flush();
249
+ void this.flush().catch(reportSharedStorageError);
346
250
  }, FLUSH_INTERVAL_MS);
347
251
  }
348
252
  }
@@ -350,130 +254,19 @@ class SharedStorageImpl implements SharedStorageApi {
350
254
  export const Storage: StorageApi = new PrivateStorageImpl();
351
255
  export const SharedStorage: SharedStorageApi = new SharedStorageImpl();
352
256
 
353
- const DECK_STORAGE_QUERY = `
354
- query CastleDeckStorage($deckId: ID!, $sessionId: ID) {
355
- deckStorage(deckId: $deckId, sessionId: $sessionId)
356
- }
357
- `;
358
-
359
- const UPDATE_DECK_STORAGE_MUTATION = `
360
- mutation CastleUpdateDeckStorage(
361
- $deckId: ID!,
362
- $updates: [DeckStorageUpdateInput!]!,
363
- $sessionId: ID
364
- ) {
365
- updateDeckStorage(deckId: $deckId, updates: $updates, sessionId: $sessionId)
366
- }
367
- `;
368
-
369
- const SHARED_DECK_STORAGE_QUERY = `
370
- query CastleSharedDeckStorage(
371
- $deckId: ID!,
372
- $keys: [String!]!,
373
- $sessionId: ID,
374
- $userId: ID
375
- ) {
376
- sharedDeckStorage(deckId: $deckId, keys: $keys, sessionId: $sessionId, userId: $userId)
377
- }
378
- `;
379
-
380
- const UPDATE_SHARED_DECK_STORAGE_MUTATION = `
381
- mutation CastleUpdateSharedDeckStorage(
382
- $deckId: ID!,
383
- $updates: [SharedDeckStorageUpdateInput!]!,
384
- $sessionId: ID,
385
- $userId: ID
386
- ) {
387
- updateSharedDeckStorage(
388
- deckId: $deckId,
389
- updates: $updates,
390
- sessionId: $sessionId,
391
- userId: $userId
392
- )
393
- }
394
- `;
395
-
396
- async function currentContext(
397
- operation: string,
398
- ): Promise<{ deckId: string; sessionId?: string }> {
399
- const deckId = await storageDeckId(operation);
400
- const context = await getDeckContext();
401
- return { deckId, sessionId: context.sessionId ?? undefined };
402
- }
403
-
404
- async function storageDeckId(operation: string): Promise<string> {
405
- try {
406
- return await requireDeckId(operation);
407
- } catch (error) {
408
- if (error instanceof CastleError && error.code === "MISSING_DECK_ID") {
409
- throw storageError(
410
- "CASTLE_STORAGE_MISSING_DECK_ID",
411
- "Save this deck before using Castle storage.",
412
- operation,
413
- );
414
- }
415
- throw error;
416
- }
417
- }
418
-
419
- async function storageGraphql<TData>(
420
- query: string,
421
- variables: Record<string, Json | undefined>,
422
- operation: string,
423
- ): Promise<TData> {
424
- const token = await requireAuthToken(operation);
425
- return graphqlRequest<TData>(query, variables, {
426
- operation,
427
- requireAuth: true,
428
- token,
429
- });
430
- }
431
-
432
- async function readBucket(
433
- scope: SharedScope,
434
- userOrKey: string,
435
- maybeKey: string | undefined,
436
- operation: string,
437
- ): Promise<Bucket> {
438
- assertScope(scope, operation);
439
- if (scope === "deck") return withSession({ scope });
440
- if (maybeKey) return withSession({ scope, userId: userOrKey });
441
- return writeBucket(scope, operation);
442
- }
443
-
444
- async function writeBucket(
445
- scope: SharedScope,
446
- operation: string,
447
- ): Promise<Bucket> {
448
- assertScope(scope, operation);
449
- if (scope === "deck") return withSession({ scope });
450
- const auth = await getAuth();
451
- if (!auth.userId) {
452
- throw storageError(
453
- "CASTLE_STORAGE_MISSING_USER_ID",
454
- "Castle user id is required for user-scoped shared storage.",
455
- operation,
456
- );
457
- }
458
- return withSession({ scope, userId: auth.userId });
257
+ // 'user'-scope writes/self-reads share one bucket (the host resolves the
258
+ // current player); a 'user' read of someone else keys by their id.
259
+ function writeBucketKey(scope: SharedScope): string {
260
+ return scope === "deck" ? "deck" : "user:self";
459
261
  }
460
262
 
461
- async function withSession(bucket: Bucket): Promise<Bucket> {
462
- const context = await getDeckContext();
463
- return { ...bucket, sessionId: context.sessionId ?? undefined };
263
+ function readBucketKey(scope: SharedScope, userId: string | undefined): string {
264
+ if (scope === "deck") return "deck";
265
+ return userId ? `user:other:${userId}` : "user:self";
464
266
  }
465
267
 
466
- function sharedVariables(
467
- deckId: string,
468
- bucket: Bucket,
469
- extra: Record<string, Json | undefined>,
470
- ): Record<string, Json | undefined> {
471
- return {
472
- deckId,
473
- sessionId: bucket.sessionId,
474
- userId: bucket.userId,
475
- ...extra,
476
- };
268
+ function scopeFromWriteKey(bucketKey: string): SharedScope {
269
+ return bucketKey === "deck" ? "deck" : "user";
477
270
  }
478
271
 
479
272
  function updatesFromDirty(dirty: Map<string, EncodedValue>): StorageUpdate[] {
@@ -562,13 +355,7 @@ function assertJsonArray(
562
355
  seen: Set<object>,
563
356
  operation: string,
564
357
  ): void {
565
- if (seen.has(values)) {
566
- throw storageError(
567
- "CASTLE_STORAGE_SERIALIZE_FAILED",
568
- "Castle storage values cannot be cyclic.",
569
- operation,
570
- );
571
- }
358
+ assertNotCyclic(values, seen, operation);
572
359
  seen.add(values);
573
360
  for (const value of values) assertJsonValue(value, seen, operation);
574
361
  seen.delete(values);
@@ -579,14 +366,8 @@ function assertJsonObject(
579
366
  seen: Set<object>,
580
367
  operation: string,
581
368
  ): void {
582
- if (seen.has(value)) {
583
- throw storageError(
584
- "CASTLE_STORAGE_SERIALIZE_FAILED",
585
- "Castle storage values cannot be cyclic.",
586
- operation,
587
- );
588
- }
589
- const prototype = Object.getPrototypeOf(value);
369
+ assertNotCyclic(value, seen, operation);
370
+ const prototype = Object.getPrototypeOf(value) as unknown;
590
371
  if (prototype !== Object.prototype && prototype !== null) {
591
372
  throw storageError(
592
373
  "CASTLE_STORAGE_SERIALIZE_FAILED",
@@ -600,12 +381,18 @@ function assertJsonObject(
600
381
  seen.delete(value);
601
382
  }
602
383
 
603
- function makeBucketKey(bucket: Bucket): string {
604
- return `${bucket.scope}:${bucket.userId ?? ""}:${bucket.sessionId ?? ""}`;
605
- }
606
-
607
- function contextKey(deckId: string, sessionId: string | undefined): string {
608
- return `${deckId}:${sessionId ?? ""}`;
384
+ function assertNotCyclic(
385
+ value: object,
386
+ seen: Set<object>,
387
+ operation: string,
388
+ ): void {
389
+ if (seen.has(value)) {
390
+ throw storageError(
391
+ "CASTLE_STORAGE_SERIALIZE_FAILED",
392
+ "Castle storage values cannot be cyclic.",
393
+ operation,
394
+ );
395
+ }
609
396
  }
610
397
 
611
398
  function assertScope(
@@ -627,6 +414,10 @@ function rejectReadBatch(batch: ReadBatch, error: unknown): void {
627
414
  }
628
415
  }
629
416
 
417
+ function reportSharedStorageError(error: unknown): void {
418
+ console.warn("Castle shared storage write failed", error);
419
+ }
420
+
630
421
  function storageError(
631
422
  code: string,
632
423
  message: string,
package/src/time.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CastleError } from "./errors";
2
- import { graphqlRequest } from "./graphql";
2
+ import { hostRequest } from "./transport";
3
3
  import type { Json } from "./types";
4
4
 
5
5
  export type CastleClockZone = "player" | "Castle";
@@ -21,30 +21,12 @@ export interface CastleTimeApi {
21
21
  getServerDate(timezone?: CastleClockZone): Promise<CastleDateParts>;
22
22
  }
23
23
 
24
- interface ServerTimeQueryData {
25
- serverTime: {
26
- timestamp: number;
27
- timezoneOffset: number;
28
- castleEpochData: Json;
29
- };
30
- }
31
-
32
24
  interface ServerTimeSnapshot {
33
25
  offsetSeconds: number;
34
26
  castleTimezoneOffsetMinutes: number;
35
27
  daysSinceCastleEpoch: Record<CastleClockZone, number>;
36
28
  }
37
29
 
38
- const SERVER_TIME_QUERY = `
39
- query CastleServerTime {
40
- serverTime {
41
- timestamp
42
- timezoneOffset
43
- castleEpochData
44
- }
45
- }
46
- `;
47
-
48
30
  let serverTimeSnapshot: ServerTimeSnapshot | null = null;
49
31
  let serverTimePromise: Promise<ServerTimeSnapshot> | null = null;
50
32
 
@@ -84,25 +66,19 @@ async function syncServerTime(): Promise<ServerTimeSnapshot> {
84
66
 
85
67
  async function fetchServerTime(): Promise<ServerTimeSnapshot> {
86
68
  const operation = "Time.getServerTime";
87
- const data = await graphqlRequest<ServerTimeQueryData>(
88
- SERVER_TIME_QUERY,
89
- undefined,
90
- {
91
- operation,
92
- },
93
- );
69
+ const data = await hostRequest("time.getServerTime", {});
94
70
  const timestamp = numberField(
95
- data.serverTime.timestamp,
71
+ data.timestamp,
96
72
  "serverTime.timestamp",
97
73
  operation,
98
74
  );
99
75
  const timezoneOffset = numberField(
100
- data.serverTime.timezoneOffset,
76
+ data.timezoneOffset,
101
77
  "serverTime.timezoneOffset",
102
78
  operation,
103
79
  );
104
80
  const epochData = jsonObject(
105
- data.serverTime.castleEpochData,
81
+ data.castleEpochData,
106
82
  "serverTime.castleEpochData",
107
83
  operation,
108
84
  );