castle-web-sdk 0.4.3 → 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/dist/runtime.js CHANGED
@@ -1,9 +1,17 @@
1
+ import { CASTLE_SDK_PROTOCOL, } from "./commands";
1
2
  import { getCastleEmbed, isEdit } from "./context";
2
3
  export const CARD_RATIO = 5 / 7;
3
4
  let ws = null;
4
5
  let logBuffer = [];
5
6
  let nextRequestId = 1;
6
7
  const pendingRequests = new Map();
8
+ // Local-dev command channel: the `castle-web serve` dev server is the host, so
9
+ // SDK commands ride the same websocket runtime.ts already uses for
10
+ // logs/screenshots/restart. Correlated by requestId, separate from the
11
+ // screenshot/write_file request map above.
12
+ const COMMAND_TIMEOUT_MS = 15000;
13
+ const SOCKET_WAIT_TIMEOUT_MS = 10000;
14
+ const pendingCommands = new Map();
7
15
  const origLog = console.log;
8
16
  const origWarn = console.warn;
9
17
  const origError = console.error;
@@ -126,6 +134,61 @@ function sendLocalRequest(msg) {
126
134
  ws.send(JSON.stringify(request));
127
135
  });
128
136
  }
137
+ // Send an SDK command to the dev server and resolve with the raw response
138
+ // envelope (ok/data/error). transport.ts interprets it — error reconstruction
139
+ // stays uniform across all three channels there. Waits for the socket to open
140
+ // so a command issued during startup isn't dropped.
141
+ export function sendLocalCommand(command, params) {
142
+ const requestId = `cmd_${nextRequestId++}`;
143
+ return new Promise((resolve, reject) => {
144
+ const timeout = setTimeout(() => {
145
+ pendingCommands.delete(requestId);
146
+ reject(new Error(`Timed out waiting for command ${command}.`));
147
+ }, COMMAND_TIMEOUT_MS);
148
+ pendingCommands.set(requestId, (env) => {
149
+ clearTimeout(timeout);
150
+ pendingCommands.delete(requestId);
151
+ resolve(env);
152
+ });
153
+ waitForSocket()
154
+ .then((socket) => {
155
+ socket.send(JSON.stringify({
156
+ type: "castle_command",
157
+ castleSdk: CASTLE_SDK_PROTOCOL,
158
+ requestId,
159
+ command,
160
+ params,
161
+ }));
162
+ })
163
+ .catch((error) => {
164
+ clearTimeout(timeout);
165
+ pendingCommands.delete(requestId);
166
+ reject(error instanceof Error ? error : new Error(String(error)));
167
+ });
168
+ });
169
+ }
170
+ function waitForSocket() {
171
+ if (ws && ws.readyState === WebSocket.OPEN)
172
+ return Promise.resolve(ws);
173
+ return new Promise((resolve, reject) => {
174
+ const start = Date.now();
175
+ const poll = setInterval(() => {
176
+ if (ws && ws.readyState === WebSocket.OPEN) {
177
+ clearInterval(poll);
178
+ resolve(ws);
179
+ }
180
+ else if (Date.now() - start > SOCKET_WAIT_TIMEOUT_MS) {
181
+ clearInterval(poll);
182
+ reject(new Error("Castle dev server is not connected."));
183
+ }
184
+ }, 100);
185
+ });
186
+ }
187
+ function resolveLocalCommand(msg) {
188
+ const pending = pendingCommands.get(msg.requestId);
189
+ if (pending)
190
+ pending(msg);
191
+ }
129
192
  function resolveLocalRequest(msg) {
130
193
  if (!msg.requestId)
131
194
  return false;
@@ -275,11 +338,40 @@ function handleLocalMessage(msg) {
275
338
  });
276
339
  }
277
340
  else if (msg.type === "restart") {
278
- location.reload();
341
+ scheduleRestart();
279
342
  }
280
343
  else if (msg.type === "write_file_response") {
281
344
  resolveLocalRequest(msg);
282
345
  }
346
+ else if (msg.type === "castle_command_response") {
347
+ resolveLocalCommand(msg);
348
+ }
349
+ }
350
+ // Restart (from `castle-web restart` / task agents) is debounced so a burst
351
+ // of reload requests -- several tasks finishing close together -- produces
352
+ // one reload. Before reloading, registered hooks run (the kit editor flushes
353
+ // its debounced unsaved edits there) so in-flight work isn't lost.
354
+ const RESTART_DEBOUNCE_MS = 1500;
355
+ let restartTimer = null;
356
+ const beforeRestartHooks = new Set();
357
+ export function onBeforeRestart(hook) {
358
+ beforeRestartHooks.add(hook);
359
+ return () => beforeRestartHooks.delete(hook);
360
+ }
361
+ function scheduleRestart() {
362
+ if (restartTimer !== null)
363
+ clearTimeout(restartTimer);
364
+ restartTimer = setTimeout(() => {
365
+ void (async () => {
366
+ try {
367
+ await Promise.all([...beforeRestartHooks].map(async (hook) => hook()));
368
+ }
369
+ catch {
370
+ // a failed flush shouldn't block the reload
371
+ }
372
+ location.reload();
373
+ })();
374
+ }, RESTART_DEBOUNCE_MS);
283
375
  }
284
376
  function localWsUrl(path) {
285
377
  const url = new URL(path, location.href);
package/dist/storage.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { type SharedScope } from "./commands";
1
2
  import type { Json } from "./types";
2
- type SharedScope = "deck" | "user";
3
3
  export interface StorageApi {
4
4
  get<T extends Json = Json>(key: string): Promise<T | null>;
5
5
  set(key: string, value: Json): void;
@@ -14,4 +14,3 @@ export interface SharedStorageApi {
14
14
  }
15
15
  export declare const Storage: StorageApi;
16
16
  export declare const SharedStorage: SharedStorageApi;
17
- export {};
package/dist/storage.js CHANGED
@@ -1,22 +1,22 @@
1
- import { getAuth, requireAuthToken } from "./auth";
2
- import { getDeckContext, requireDeckId } from "./context";
3
1
  import { CastleError } from "./errors";
4
- import { graphqlRequest } from "./graphql";
2
+ import { hostRequest } from "./transport";
5
3
  const FLUSH_INTERVAL_MS = 2000;
4
+ // Per-player private storage. The host stamps deckId/sessionId, so one deck
5
+ // session = one stable blob: load once, then serve reads from cache and batch
6
+ // writes on a 2s flush. Dirty writes overlay the server blob so an in-flight
7
+ // write isn't clobbered by the flush response.
6
8
  class PrivateStorageImpl {
7
9
  cache = new Map();
8
10
  dirty = new Map();
9
11
  loadPromise = null;
10
- loadedContextKey = null;
11
12
  flushTimer = null;
12
13
  async get(key) {
13
- await this.ensureLoaded("Storage.get");
14
+ await this.ensureLoaded();
14
15
  return (this.cache.get(key) ?? null);
15
16
  }
16
17
  set(key, value) {
17
- const encoded = encodeStorageValue(value, "Storage.set");
18
18
  this.cache.set(key, value);
19
- this.dirty.set(key, encoded);
19
+ this.dirty.set(key, encodeStorageValue(value, "Storage.set"));
20
20
  this.scheduleFlush();
21
21
  }
22
22
  remove(key) {
@@ -24,51 +24,37 @@ class PrivateStorageImpl {
24
24
  this.dirty.set(key, null);
25
25
  this.scheduleFlush();
26
26
  }
27
- async flush() {
28
- if (this.dirty.size === 0)
29
- return;
30
- const context = await currentContext("Storage.flush");
31
- const snapshot = new Map(this.dirty);
32
- const data = await storageGraphql(UPDATE_DECK_STORAGE_MUTATION, {
33
- deckId: context.deckId,
34
- sessionId: context.sessionId,
35
- updates: updatesFromDirty(snapshot),
36
- }, "updateDeckStorage");
37
- this.applyServerBlob(data.updateDeckStorage, contextKey(context.deckId, context.sessionId));
38
- clearAcknowledgedDirty(this.dirty, snapshot);
39
- this.overlayDirty();
40
- }
41
- ensureLoaded(operation) {
27
+ ensureLoaded() {
42
28
  if (!this.loadPromise) {
43
- this.loadPromise = this.load(operation).catch((error) => {
29
+ this.loadPromise = this.load().catch((error) => {
44
30
  this.loadPromise = null;
45
31
  throw error;
46
32
  });
47
33
  }
48
34
  return this.loadPromise;
49
35
  }
50
- async load(operation) {
51
- const context = await currentContext(operation);
52
- const key = contextKey(context.deckId, context.sessionId);
53
- if (this.loadedContextKey === key)
54
- return;
55
- const data = await storageGraphql(DECK_STORAGE_QUERY, { deckId: context.deckId, sessionId: context.sessionId }, "deckStorage");
56
- this.cache = decodeStorageBlob(data.deckStorage, operation);
57
- this.loadedContextKey = key;
36
+ async load() {
37
+ const { blob } = await hostRequest("deckStorage.load", {});
38
+ this.cache = decodeStorageBlob(blob, "Storage.get");
58
39
  this.overlayDirty();
59
40
  }
60
- applyServerBlob(blob, loadedContextKey) {
41
+ async flush() {
42
+ if (this.dirty.size === 0)
43
+ return;
44
+ const snapshot = new Map(this.dirty);
45
+ const { blob } = await hostRequest("deckStorage.update", {
46
+ updates: updatesFromDirty(snapshot),
47
+ });
61
48
  this.cache = decodeStorageBlob(blob, "Storage.flush");
62
- this.loadedContextKey = loadedContextKey;
49
+ clearAcknowledgedDirty(this.dirty, snapshot);
50
+ this.overlayDirty();
63
51
  }
64
52
  overlayDirty() {
65
53
  for (const [key, encoded] of this.dirty) {
66
- if (encoded === null) {
54
+ if (encoded === null)
67
55
  this.cache.delete(key);
68
- }
69
- else {
56
+ else
70
57
  this.cache.set(key, decodeStorageValue(encoded, "Storage.dirty"));
71
- }
72
58
  }
73
59
  }
74
60
  scheduleFlush() {
@@ -80,71 +66,50 @@ class PrivateStorageImpl {
80
66
  }, FLUSH_INTERVAL_MS);
81
67
  }
82
68
  }
69
+ // Cross-player storage. Writes always target the current player (the host
70
+ // forces 'user'-scope writes to whoever is signed in), so dirty writes bucket
71
+ // under a fixed key; reads of another player's 'user' bucket take an explicit
72
+ // userId and never have pending writes. Reads coalesce per microtask into one
73
+ // command with a keys[] array.
83
74
  class SharedStorageImpl {
84
75
  dirtyBuckets = new Map();
85
- buckets = new Map();
86
76
  readBatches = new Map();
87
- pendingWrites = new Set();
88
- pendingWriteErrors = [];
89
77
  flushTimer = null;
90
78
  async get(scope, userOrKey, maybeKey) {
91
- const bucket = await readBucket(scope, userOrKey, maybeKey, "SharedStorage.get");
79
+ assertScope(scope, "SharedStorage.get");
80
+ const userId = scope === "user" && maybeKey ? userOrKey : undefined;
92
81
  const key = maybeKey ?? userOrKey;
93
- await this.awaitPendingWrites("SharedStorage.get");
94
- const dirty = this.peekDirty(bucket, key);
82
+ const dirty = this.peekDirty(scope, userId, key);
95
83
  if (dirty !== undefined) {
96
84
  return (dirty === null ? null : decodeStorageValue(dirty, "SharedStorage.get"));
97
85
  }
98
- return this.queueRead(bucket, key);
86
+ return this.queueRead(scope, userId, key);
99
87
  }
100
88
  set(scope, key, value) {
101
- this.enqueueWrite(scope, key, encodeStorageValue(value, "SharedStorage.set"), "SharedStorage.set");
89
+ assertScope(scope, "SharedStorage.set");
90
+ this.write(scope, key, encodeStorageValue(value, "SharedStorage.set"));
102
91
  }
103
92
  remove(scope, key) {
104
- this.enqueueWrite(scope, key, null, "SharedStorage.remove");
105
- }
106
- async flush() {
107
- await this.awaitPendingWrites("SharedStorage.flush");
108
- if (this.dirtyBuckets.size === 0)
109
- return;
110
- const tasks = Array.from(this.dirtyBuckets, ([bucketKey, dirty]) => this.flushBucket(bucketKey, new Map(dirty)));
111
- await Promise.all(tasks);
93
+ assertScope(scope, "SharedStorage.remove");
94
+ this.write(scope, key, null);
112
95
  }
113
- async write(scope, key, encoded, operation) {
114
- const bucket = await writeBucket(scope, operation);
115
- const bucketKey = makeBucketKey(bucket);
116
- this.buckets.set(bucketKey, bucket);
96
+ write(scope, key, encoded) {
97
+ const bucketKey = writeBucketKey(scope);
117
98
  const dirty = this.dirtyBuckets.get(bucketKey) ?? new Map();
118
99
  dirty.set(key, encoded);
119
100
  this.dirtyBuckets.set(bucketKey, dirty);
120
101
  this.scheduleFlush();
121
102
  }
122
- enqueueWrite(scope, key, encoded, operation) {
123
- const pending = this.write(scope, key, encoded, operation)
124
- .catch((error) => {
125
- this.pendingWriteErrors.push(error);
126
- })
127
- .finally(() => {
128
- this.pendingWrites.delete(pending);
129
- });
130
- this.pendingWrites.add(pending);
103
+ peekDirty(scope, userId, key) {
104
+ if (scope === "user" && userId)
105
+ return undefined;
106
+ return this.dirtyBuckets.get(writeBucketKey(scope))?.get(key);
131
107
  }
132
- async awaitPendingWrites(operation) {
133
- if (this.pendingWrites.size > 0) {
134
- await Promise.all(this.pendingWrites);
135
- }
136
- const error = this.pendingWriteErrors.shift();
137
- if (error instanceof Error)
138
- throw error;
139
- if (error) {
140
- throw storageError("CASTLE_STORAGE_WRITE_FAILED", "Queued shared storage write failed.", operation);
141
- }
142
- }
143
- async queueRead(bucket, key) {
144
- const bucketKey = makeBucketKey(bucket);
108
+ queueRead(scope, userId, key) {
109
+ const bucketKey = readBucketKey(scope, userId);
145
110
  let batch = this.readBatches.get(bucketKey);
146
111
  if (!batch) {
147
- batch = { bucket, reads: new Map(), scheduled: false };
112
+ batch = { bucketKey, scope, userId, reads: new Map(), scheduled: false };
148
113
  this.readBatches.set(bucketKey, batch);
149
114
  }
150
115
  const reads = batch.reads.get(key) ?? [];
@@ -154,11 +119,9 @@ class SharedStorageImpl {
154
119
  });
155
120
  if (!batch.scheduled) {
156
121
  batch.scheduled = true;
157
- queueMicrotask(() => {
158
- void this.flushReadBatch(bucketKey);
159
- });
122
+ queueMicrotask(() => void this.flushReadBatch(bucketKey));
160
123
  }
161
- return (await promise);
124
+ return promise;
162
125
  }
163
126
  async flushReadBatch(bucketKey) {
164
127
  const batch = this.readBatches.get(bucketKey);
@@ -167,9 +130,12 @@ class SharedStorageImpl {
167
130
  this.readBatches.delete(bucketKey);
168
131
  const keys = Array.from(batch.reads.keys());
169
132
  try {
170
- const context = await currentContext("SharedStorage.get");
171
- const data = await storageGraphql(SHARED_DECK_STORAGE_QUERY, sharedVariables(context.deckId, batch.bucket, { keys }), "sharedDeckStorage");
172
- this.resolveReadBatch(batch, data.sharedDeckStorage);
133
+ const { blob } = await hostRequest("sharedDeckStorage.load", {
134
+ scope: batch.scope,
135
+ userId: batch.userId ?? null,
136
+ keys,
137
+ });
138
+ this.resolveReadBatch(batch, blob);
173
139
  }
174
140
  catch (error) {
175
141
  rejectReadBatch(batch, error);
@@ -177,7 +143,7 @@ class SharedStorageImpl {
177
143
  }
178
144
  resolveReadBatch(batch, blob) {
179
145
  for (const [key, reads] of batch.reads) {
180
- const encoded = this.peekDirty(batch.bucket, key) ?? blob[key] ?? null;
146
+ const encoded = this.peekDirty(batch.scope, batch.userId, key) ?? blob[key] ?? null;
181
147
  const value = encoded === null
182
148
  ? null
183
149
  : decodeStorageValue(encoded, "SharedStorage.get");
@@ -185,16 +151,19 @@ class SharedStorageImpl {
185
151
  read.resolve(value);
186
152
  }
187
153
  }
154
+ async flush() {
155
+ if (this.dirtyBuckets.size === 0)
156
+ return;
157
+ const tasks = Array.from(this.dirtyBuckets, ([bucketKey, dirty]) => this.flushBucket(bucketKey, new Map(dirty)));
158
+ await Promise.all(tasks);
159
+ }
188
160
  async flushBucket(bucketKey, snapshot) {
189
161
  if (snapshot.size === 0)
190
162
  return;
191
- const bucket = this.buckets.get(bucketKey);
192
- if (!bucket)
193
- return;
194
- const context = await currentContext("SharedStorage.flush");
195
- await storageGraphql(UPDATE_SHARED_DECK_STORAGE_MUTATION, sharedVariables(context.deckId, bucket, {
163
+ await hostRequest("sharedDeckStorage.update", {
164
+ scope: scopeFromWriteKey(bucketKey),
196
165
  updates: updatesFromDirty(snapshot),
197
- }), "updateSharedDeckStorage");
166
+ });
198
167
  const dirty = this.dirtyBuckets.get(bucketKey);
199
168
  if (!dirty)
200
169
  return;
@@ -202,112 +171,29 @@ class SharedStorageImpl {
202
171
  if (dirty.size === 0)
203
172
  this.dirtyBuckets.delete(bucketKey);
204
173
  }
205
- peekDirty(bucket, key) {
206
- return this.dirtyBuckets.get(makeBucketKey(bucket))?.get(key);
207
- }
208
174
  scheduleFlush() {
209
175
  if (this.flushTimer)
210
176
  return;
211
177
  this.flushTimer = setTimeout(() => {
212
178
  this.flushTimer = null;
213
- void this.flush();
179
+ void this.flush().catch(reportSharedStorageError);
214
180
  }, FLUSH_INTERVAL_MS);
215
181
  }
216
182
  }
217
183
  export const Storage = new PrivateStorageImpl();
218
184
  export const SharedStorage = new SharedStorageImpl();
219
- const DECK_STORAGE_QUERY = `
220
- query CastleDeckStorage($deckId: ID!, $sessionId: ID) {
221
- deckStorage(deckId: $deckId, sessionId: $sessionId)
222
- }
223
- `;
224
- const UPDATE_DECK_STORAGE_MUTATION = `
225
- mutation CastleUpdateDeckStorage(
226
- $deckId: ID!,
227
- $updates: [DeckStorageUpdateInput!]!,
228
- $sessionId: ID
229
- ) {
230
- updateDeckStorage(deckId: $deckId, updates: $updates, sessionId: $sessionId)
231
- }
232
- `;
233
- const SHARED_DECK_STORAGE_QUERY = `
234
- query CastleSharedDeckStorage(
235
- $deckId: ID!,
236
- $keys: [String!]!,
237
- $sessionId: ID,
238
- $userId: ID
239
- ) {
240
- sharedDeckStorage(deckId: $deckId, keys: $keys, sessionId: $sessionId, userId: $userId)
241
- }
242
- `;
243
- const UPDATE_SHARED_DECK_STORAGE_MUTATION = `
244
- mutation CastleUpdateSharedDeckStorage(
245
- $deckId: ID!,
246
- $updates: [SharedDeckStorageUpdateInput!]!,
247
- $sessionId: ID,
248
- $userId: ID
249
- ) {
250
- updateSharedDeckStorage(
251
- deckId: $deckId,
252
- updates: $updates,
253
- sessionId: $sessionId,
254
- userId: $userId
255
- )
256
- }
257
- `;
258
- async function currentContext(operation) {
259
- const deckId = await storageDeckId(operation);
260
- const context = await getDeckContext();
261
- return { deckId, sessionId: context.sessionId ?? undefined };
262
- }
263
- async function storageDeckId(operation) {
264
- try {
265
- return await requireDeckId(operation);
266
- }
267
- catch (error) {
268
- if (error instanceof CastleError && error.code === "MISSING_DECK_ID") {
269
- throw storageError("CASTLE_STORAGE_MISSING_DECK_ID", "Save this deck before using Castle storage.", operation);
270
- }
271
- throw error;
272
- }
185
+ // 'user'-scope writes/self-reads share one bucket (the host resolves the
186
+ // current player); a 'user' read of someone else keys by their id.
187
+ function writeBucketKey(scope) {
188
+ return scope === "deck" ? "deck" : "user:self";
273
189
  }
274
- async function storageGraphql(query, variables, operation) {
275
- const token = await requireAuthToken(operation);
276
- return graphqlRequest(query, variables, {
277
- operation,
278
- requireAuth: true,
279
- token,
280
- });
281
- }
282
- async function readBucket(scope, userOrKey, maybeKey, operation) {
283
- assertScope(scope, operation);
284
- if (scope === "deck")
285
- return withSession({ scope });
286
- if (maybeKey)
287
- return withSession({ scope, userId: userOrKey });
288
- return writeBucket(scope, operation);
289
- }
290
- async function writeBucket(scope, operation) {
291
- assertScope(scope, operation);
190
+ function readBucketKey(scope, userId) {
292
191
  if (scope === "deck")
293
- return withSession({ scope });
294
- const auth = await getAuth();
295
- if (!auth.userId) {
296
- throw storageError("CASTLE_STORAGE_MISSING_USER_ID", "Castle user id is required for user-scoped shared storage.", operation);
297
- }
298
- return withSession({ scope, userId: auth.userId });
192
+ return "deck";
193
+ return userId ? `user:other:${userId}` : "user:self";
299
194
  }
300
- async function withSession(bucket) {
301
- const context = await getDeckContext();
302
- return { ...bucket, sessionId: context.sessionId ?? undefined };
303
- }
304
- function sharedVariables(deckId, bucket, extra) {
305
- return {
306
- deckId,
307
- sessionId: bucket.sessionId,
308
- userId: bucket.userId,
309
- ...extra,
310
- };
195
+ function scopeFromWriteKey(bucketKey) {
196
+ return bucketKey === "deck" ? "deck" : "user";
311
197
  }
312
198
  function updatesFromDirty(dirty) {
313
199
  return Array.from(dirty, ([key, value]) => ({ key, value }));
@@ -362,18 +248,14 @@ function assertJsonValue(value, seen, operation) {
362
248
  throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values must be JSON.", operation);
363
249
  }
364
250
  function assertJsonArray(values, seen, operation) {
365
- if (seen.has(values)) {
366
- throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values cannot be cyclic.", operation);
367
- }
251
+ assertNotCyclic(values, seen, operation);
368
252
  seen.add(values);
369
253
  for (const value of values)
370
254
  assertJsonValue(value, seen, operation);
371
255
  seen.delete(values);
372
256
  }
373
257
  function assertJsonObject(value, seen, operation) {
374
- if (seen.has(value)) {
375
- throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values cannot be cyclic.", operation);
376
- }
258
+ assertNotCyclic(value, seen, operation);
377
259
  const prototype = Object.getPrototypeOf(value);
378
260
  if (prototype !== Object.prototype && prototype !== null) {
379
261
  throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage objects must be plain JSON.", operation);
@@ -383,11 +265,10 @@ function assertJsonObject(value, seen, operation) {
383
265
  assertJsonValue(child, seen, operation);
384
266
  seen.delete(value);
385
267
  }
386
- function makeBucketKey(bucket) {
387
- return `${bucket.scope}:${bucket.userId ?? ""}:${bucket.sessionId ?? ""}`;
388
- }
389
- function contextKey(deckId, sessionId) {
390
- return `${deckId}:${sessionId ?? ""}`;
268
+ function assertNotCyclic(value, seen, operation) {
269
+ if (seen.has(value)) {
270
+ throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values cannot be cyclic.", operation);
271
+ }
391
272
  }
392
273
  function assertScope(scope, operation) {
393
274
  if (scope !== "deck" && scope !== "user") {
@@ -400,6 +281,9 @@ function rejectReadBatch(batch, error) {
400
281
  read.reject(error);
401
282
  }
402
283
  }
284
+ function reportSharedStorageError(error) {
285
+ console.warn("Castle shared storage write failed", error);
286
+ }
403
287
  function storageError(code, message, operation) {
404
288
  return new CastleError({ code, message, operation });
405
289
  }
package/dist/time.js CHANGED
@@ -1,14 +1,5 @@
1
1
  import { CastleError } from "./errors";
2
- import { graphqlRequest } from "./graphql";
3
- const SERVER_TIME_QUERY = `
4
- query CastleServerTime {
5
- serverTime {
6
- timestamp
7
- timezoneOffset
8
- castleEpochData
9
- }
10
- }
11
- `;
2
+ import { hostRequest } from "./transport";
12
3
  let serverTimeSnapshot = null;
13
4
  let serverTimePromise = null;
14
5
  export const Time = {
@@ -38,12 +29,10 @@ async function syncServerTime() {
38
29
  }
39
30
  async function fetchServerTime() {
40
31
  const operation = "Time.getServerTime";
41
- const data = await graphqlRequest(SERVER_TIME_QUERY, undefined, {
42
- operation,
43
- });
44
- const timestamp = numberField(data.serverTime.timestamp, "serverTime.timestamp", operation);
45
- const timezoneOffset = numberField(data.serverTime.timezoneOffset, "serverTime.timezoneOffset", operation);
46
- const epochData = jsonObject(data.serverTime.castleEpochData, "serverTime.castleEpochData", operation);
32
+ const data = await hostRequest("time.getServerTime", {});
33
+ const timestamp = numberField(data.timestamp, "serverTime.timestamp", operation);
34
+ const timezoneOffset = numberField(data.timezoneOffset, "serverTime.timezoneOffset", operation);
35
+ const epochData = jsonObject(data.castleEpochData, "serverTime.castleEpochData", operation);
47
36
  return {
48
37
  offsetSeconds: snappedOffset(timestamp - clientUnixSeconds()),
49
38
  castleTimezoneOffsetMinutes: timezoneOffset,
@@ -0,0 +1,16 @@
1
+ import { type CommandName, type CommandParams, type CommandResult } from "./commands";
2
+ type PostChannel = "mobile" | "web";
3
+ interface ReactNativeWebViewBridge {
4
+ postMessage: (message: string) => void;
5
+ }
6
+ declare global {
7
+ interface Window {
8
+ ReactNativeWebView?: ReactNativeWebViewBridge;
9
+ __castleSdkHost?: {
10
+ receive: (message: unknown) => void;
11
+ };
12
+ }
13
+ }
14
+ export declare function hostRequest<C extends CommandName>(command: C, params: CommandParams[C]): Promise<CommandResult[C]>;
15
+ export declare function getCommandChannel(): PostChannel | "local";
16
+ export {};