castle-web-sdk 0.4.1 → 0.4.3

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.
@@ -0,0 +1,405 @@
1
+ import { getAuth, requireAuthToken } from "./auth";
2
+ import { getDeckContext, requireDeckId } from "./context";
3
+ import { CastleError } from "./errors";
4
+ import { graphqlRequest } from "./graphql";
5
+ const FLUSH_INTERVAL_MS = 2000;
6
+ class PrivateStorageImpl {
7
+ cache = new Map();
8
+ dirty = new Map();
9
+ loadPromise = null;
10
+ loadedContextKey = null;
11
+ flushTimer = null;
12
+ async get(key) {
13
+ await this.ensureLoaded("Storage.get");
14
+ return (this.cache.get(key) ?? null);
15
+ }
16
+ set(key, value) {
17
+ const encoded = encodeStorageValue(value, "Storage.set");
18
+ this.cache.set(key, value);
19
+ this.dirty.set(key, encoded);
20
+ this.scheduleFlush();
21
+ }
22
+ remove(key) {
23
+ this.cache.delete(key);
24
+ this.dirty.set(key, null);
25
+ this.scheduleFlush();
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) {
42
+ if (!this.loadPromise) {
43
+ this.loadPromise = this.load(operation).catch((error) => {
44
+ this.loadPromise = null;
45
+ throw error;
46
+ });
47
+ }
48
+ return this.loadPromise;
49
+ }
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;
58
+ this.overlayDirty();
59
+ }
60
+ applyServerBlob(blob, loadedContextKey) {
61
+ this.cache = decodeStorageBlob(blob, "Storage.flush");
62
+ this.loadedContextKey = loadedContextKey;
63
+ }
64
+ overlayDirty() {
65
+ for (const [key, encoded] of this.dirty) {
66
+ if (encoded === null) {
67
+ this.cache.delete(key);
68
+ }
69
+ else {
70
+ this.cache.set(key, decodeStorageValue(encoded, "Storage.dirty"));
71
+ }
72
+ }
73
+ }
74
+ scheduleFlush() {
75
+ if (this.flushTimer)
76
+ return;
77
+ this.flushTimer = setTimeout(() => {
78
+ this.flushTimer = null;
79
+ void this.flush();
80
+ }, FLUSH_INTERVAL_MS);
81
+ }
82
+ }
83
+ class SharedStorageImpl {
84
+ dirtyBuckets = new Map();
85
+ buckets = new Map();
86
+ readBatches = new Map();
87
+ pendingWrites = new Set();
88
+ pendingWriteErrors = [];
89
+ flushTimer = null;
90
+ async get(scope, userOrKey, maybeKey) {
91
+ const bucket = await readBucket(scope, userOrKey, maybeKey, "SharedStorage.get");
92
+ const key = maybeKey ?? userOrKey;
93
+ await this.awaitPendingWrites("SharedStorage.get");
94
+ const dirty = this.peekDirty(bucket, key);
95
+ if (dirty !== undefined) {
96
+ return (dirty === null ? null : decodeStorageValue(dirty, "SharedStorage.get"));
97
+ }
98
+ return this.queueRead(bucket, key);
99
+ }
100
+ set(scope, key, value) {
101
+ this.enqueueWrite(scope, key, encodeStorageValue(value, "SharedStorage.set"), "SharedStorage.set");
102
+ }
103
+ 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);
112
+ }
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);
117
+ const dirty = this.dirtyBuckets.get(bucketKey) ?? new Map();
118
+ dirty.set(key, encoded);
119
+ this.dirtyBuckets.set(bucketKey, dirty);
120
+ this.scheduleFlush();
121
+ }
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);
131
+ }
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);
145
+ let batch = this.readBatches.get(bucketKey);
146
+ if (!batch) {
147
+ batch = { bucket, reads: new Map(), scheduled: false };
148
+ this.readBatches.set(bucketKey, batch);
149
+ }
150
+ const reads = batch.reads.get(key) ?? [];
151
+ batch.reads.set(key, reads);
152
+ const promise = new Promise((resolve, reject) => {
153
+ reads.push({ resolve, reject });
154
+ });
155
+ if (!batch.scheduled) {
156
+ batch.scheduled = true;
157
+ queueMicrotask(() => {
158
+ void this.flushReadBatch(bucketKey);
159
+ });
160
+ }
161
+ return (await promise);
162
+ }
163
+ async flushReadBatch(bucketKey) {
164
+ const batch = this.readBatches.get(bucketKey);
165
+ if (!batch)
166
+ return;
167
+ this.readBatches.delete(bucketKey);
168
+ const keys = Array.from(batch.reads.keys());
169
+ 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);
173
+ }
174
+ catch (error) {
175
+ rejectReadBatch(batch, error);
176
+ }
177
+ }
178
+ resolveReadBatch(batch, blob) {
179
+ for (const [key, reads] of batch.reads) {
180
+ const encoded = this.peekDirty(batch.bucket, key) ?? blob[key] ?? null;
181
+ const value = encoded === null
182
+ ? null
183
+ : decodeStorageValue(encoded, "SharedStorage.get");
184
+ for (const read of reads)
185
+ read.resolve(value);
186
+ }
187
+ }
188
+ async flushBucket(bucketKey, snapshot) {
189
+ if (snapshot.size === 0)
190
+ 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, {
196
+ updates: updatesFromDirty(snapshot),
197
+ }), "updateSharedDeckStorage");
198
+ const dirty = this.dirtyBuckets.get(bucketKey);
199
+ if (!dirty)
200
+ return;
201
+ clearAcknowledgedDirty(dirty, snapshot);
202
+ if (dirty.size === 0)
203
+ this.dirtyBuckets.delete(bucketKey);
204
+ }
205
+ peekDirty(bucket, key) {
206
+ return this.dirtyBuckets.get(makeBucketKey(bucket))?.get(key);
207
+ }
208
+ scheduleFlush() {
209
+ if (this.flushTimer)
210
+ return;
211
+ this.flushTimer = setTimeout(() => {
212
+ this.flushTimer = null;
213
+ void this.flush();
214
+ }, FLUSH_INTERVAL_MS);
215
+ }
216
+ }
217
+ export const Storage = new PrivateStorageImpl();
218
+ 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
+ }
273
+ }
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);
292
+ 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 });
299
+ }
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
+ };
311
+ }
312
+ function updatesFromDirty(dirty) {
313
+ return Array.from(dirty, ([key, value]) => ({ key, value }));
314
+ }
315
+ function clearAcknowledgedDirty(dirty, acknowledged) {
316
+ for (const [key, value] of acknowledged) {
317
+ if (dirty.get(key) === value)
318
+ dirty.delete(key);
319
+ }
320
+ }
321
+ function decodeStorageBlob(blob, operation) {
322
+ return new Map(Object.entries(blob).map(([key, value]) => [
323
+ key,
324
+ decodeStorageValue(value, operation),
325
+ ]));
326
+ }
327
+ function decodeStorageValue(encoded, operation) {
328
+ try {
329
+ return JSON.parse(encoded);
330
+ }
331
+ catch {
332
+ throw storageError("CASTLE_STORAGE_PARSE_FAILED", "Stored Castle value is not valid JSON.", operation);
333
+ }
334
+ }
335
+ function encodeStorageValue(value, operation) {
336
+ try {
337
+ assertJsonValue(value, new Set(), operation);
338
+ return JSON.stringify(value);
339
+ }
340
+ catch (error) {
341
+ if (error instanceof CastleError)
342
+ throw error;
343
+ throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values must be JSON.", operation);
344
+ }
345
+ }
346
+ function assertJsonValue(value, seen, operation) {
347
+ if (value === null || typeof value === "boolean" || typeof value === "string")
348
+ return;
349
+ if (typeof value === "number") {
350
+ if (Number.isFinite(value))
351
+ return;
352
+ throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage numbers must be finite.", operation);
353
+ }
354
+ if (Array.isArray(value)) {
355
+ assertJsonArray(value, seen, operation);
356
+ return;
357
+ }
358
+ if (typeof value === "object") {
359
+ assertJsonObject(value, seen, operation);
360
+ return;
361
+ }
362
+ throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage values must be JSON.", operation);
363
+ }
364
+ 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
+ }
368
+ seen.add(values);
369
+ for (const value of values)
370
+ assertJsonValue(value, seen, operation);
371
+ seen.delete(values);
372
+ }
373
+ 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
+ }
377
+ const prototype = Object.getPrototypeOf(value);
378
+ if (prototype !== Object.prototype && prototype !== null) {
379
+ throw storageError("CASTLE_STORAGE_SERIALIZE_FAILED", "Castle storage objects must be plain JSON.", operation);
380
+ }
381
+ seen.add(value);
382
+ for (const child of Object.values(value))
383
+ assertJsonValue(child, seen, operation);
384
+ seen.delete(value);
385
+ }
386
+ function makeBucketKey(bucket) {
387
+ return `${bucket.scope}:${bucket.userId ?? ""}:${bucket.sessionId ?? ""}`;
388
+ }
389
+ function contextKey(deckId, sessionId) {
390
+ return `${deckId}:${sessionId ?? ""}`;
391
+ }
392
+ function assertScope(scope, operation) {
393
+ if (scope !== "deck" && scope !== "user") {
394
+ throw storageError("CASTLE_STORAGE_INVALID_SCOPE", 'SharedStorage scope must be "deck" or "user".', operation);
395
+ }
396
+ }
397
+ function rejectReadBatch(batch, error) {
398
+ for (const reads of batch.reads.values()) {
399
+ for (const read of reads)
400
+ read.reject(error);
401
+ }
402
+ }
403
+ function storageError(code, message, operation) {
404
+ return new CastleError({ code, message, operation });
405
+ }
package/dist/time.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ export type CastleClockZone = "player" | "Castle";
2
+ export interface CastleDateParts {
3
+ sec: number;
4
+ min: number;
5
+ hour: number;
6
+ day: number;
7
+ month: number;
8
+ year: number;
9
+ wday: number;
10
+ yday: number;
11
+ daysSinceCastleEpoch: number;
12
+ }
13
+ export interface CastleTimeApi {
14
+ getServerTime(): Promise<number>;
15
+ getServerDate(timezone?: CastleClockZone): Promise<CastleDateParts>;
16
+ }
17
+ export declare const Time: CastleTimeApi;
package/dist/time.js ADDED
@@ -0,0 +1,131 @@
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
+ `;
12
+ let serverTimeSnapshot = null;
13
+ let serverTimePromise = null;
14
+ export const Time = {
15
+ getServerTime,
16
+ getServerDate,
17
+ };
18
+ async function getServerTime() {
19
+ const snapshot = await syncServerTime();
20
+ return clientUnixSeconds() + snapshot.offsetSeconds;
21
+ }
22
+ async function getServerDate(timezone = "Castle") {
23
+ const operation = "Time.getServerDate";
24
+ const zone = clockZone(timezone, operation);
25
+ const snapshot = await syncServerTime();
26
+ return dateParts(clientUnixSeconds() + snapshot.offsetSeconds, zone, snapshot);
27
+ }
28
+ async function syncServerTime() {
29
+ if (serverTimeSnapshot)
30
+ return serverTimeSnapshot;
31
+ if (!serverTimePromise) {
32
+ serverTimePromise = fetchServerTime().then((snapshot) => {
33
+ serverTimeSnapshot = snapshot;
34
+ return snapshot;
35
+ });
36
+ }
37
+ return serverTimePromise;
38
+ }
39
+ async function fetchServerTime() {
40
+ 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);
47
+ return {
48
+ offsetSeconds: snappedOffset(timestamp - clientUnixSeconds()),
49
+ castleTimezoneOffsetMinutes: timezoneOffset,
50
+ daysSinceCastleEpoch: {
51
+ Castle: numberField(epochData.daysSinceServerTz, "castleEpochData.daysSinceServerTz", operation),
52
+ player: numberField(epochData.daysSinceUserTz, "castleEpochData.daysSinceUserTz", operation),
53
+ },
54
+ };
55
+ }
56
+ function dateParts(unixSeconds, timezone, snapshot) {
57
+ const shiftedSeconds = timezone === "Castle"
58
+ ? unixSeconds + snapshot.castleTimezoneOffsetMinutes * 60
59
+ : unixSeconds;
60
+ const date = new Date(shiftedSeconds * 1000);
61
+ return timezone === "Castle"
62
+ ? utcDateParts(date, snapshot.daysSinceCastleEpoch.Castle)
63
+ : localDateParts(date, snapshot.daysSinceCastleEpoch.player);
64
+ }
65
+ function utcDateParts(date, daysSinceCastleEpoch) {
66
+ return {
67
+ sec: date.getUTCSeconds(),
68
+ min: date.getUTCMinutes(),
69
+ hour: date.getUTCHours(),
70
+ day: date.getUTCDate(),
71
+ month: date.getUTCMonth() + 1,
72
+ year: date.getUTCFullYear(),
73
+ wday: date.getUTCDay() + 1,
74
+ yday: dayOfYear(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()),
75
+ daysSinceCastleEpoch,
76
+ };
77
+ }
78
+ function localDateParts(date, daysSinceCastleEpoch) {
79
+ return {
80
+ sec: date.getSeconds(),
81
+ min: date.getMinutes(),
82
+ hour: date.getHours(),
83
+ day: date.getDate(),
84
+ month: date.getMonth() + 1,
85
+ year: date.getFullYear(),
86
+ wday: date.getDay() + 1,
87
+ yday: dayOfYear(date.getFullYear(), date.getMonth(), date.getDate()),
88
+ daysSinceCastleEpoch,
89
+ };
90
+ }
91
+ function dayOfYear(year, monthIndex, day) {
92
+ const start = Date.UTC(year, 0, 1);
93
+ const current = Date.UTC(year, monthIndex, day);
94
+ return Math.floor((current - start) / 86400000) + 1;
95
+ }
96
+ function clockZone(timezone, operation) {
97
+ const normalized = timezone.trim().toLowerCase();
98
+ if (normalized === "castle")
99
+ return "Castle";
100
+ if (normalized === "player")
101
+ return "player";
102
+ throw new CastleError({
103
+ code: "UNSUPPORTED_TIMEZONE",
104
+ message: 'Time.getServerDate timezone must be "Castle" or "player".',
105
+ operation,
106
+ });
107
+ }
108
+ function clientUnixSeconds() {
109
+ return Math.floor(Date.now() / 1000);
110
+ }
111
+ function snappedOffset(offsetSeconds) {
112
+ return Math.abs(offsetSeconds) < 10 ? 0 : offsetSeconds;
113
+ }
114
+ function jsonObject(value, field, operation) {
115
+ if (value !== null && typeof value === "object" && !Array.isArray(value))
116
+ return value;
117
+ throw new CastleError({
118
+ code: "GRAPHQL_BAD_DATA",
119
+ message: `Castle GraphQL field ${field} was not an object.`,
120
+ operation,
121
+ });
122
+ }
123
+ function numberField(value, field, operation) {
124
+ if (typeof value === "number" && Number.isFinite(value))
125
+ return value;
126
+ throw new CastleError({
127
+ code: "GRAPHQL_BAD_DATA",
128
+ message: `Castle GraphQL field ${field} was not a number.`,
129
+ operation,
130
+ });
131
+ }
@@ -0,0 +1,3 @@
1
+ export type Json = null | boolean | number | string | Json[] | {
2
+ [key: string]: Json;
3
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/user.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export interface CastleUser {
2
+ userId: string;
3
+ username: string;
4
+ isActive: boolean;
5
+ }
6
+ export interface CastleUserApi {
7
+ getCurrent(): Promise<CastleUser>;
8
+ }
9
+ export declare const User: CastleUserApi;
package/dist/user.js ADDED
@@ -0,0 +1,58 @@
1
+ import { requireAuthToken } from "./auth";
2
+ import { CastleError } from "./errors";
3
+ import { graphqlRequest } from "./graphql";
4
+ const CURRENT_USER_QUERY = `
5
+ query CastleCurrentUser {
6
+ me {
7
+ userId
8
+ username
9
+ }
10
+ }
11
+ `;
12
+ let currentUser = null;
13
+ let currentUserPromise = null;
14
+ export const User = {
15
+ getCurrent,
16
+ };
17
+ async function getCurrent() {
18
+ if (currentUser)
19
+ return currentUser;
20
+ currentUserPromise ??= fetchCurrentUser().then((user) => {
21
+ currentUser = user;
22
+ return user;
23
+ });
24
+ return currentUserPromise;
25
+ }
26
+ async function fetchCurrentUser() {
27
+ const operation = "User.getCurrent";
28
+ const token = await requireAuthToken(operation);
29
+ const data = await graphqlRequest(CURRENT_USER_QUERY, undefined, {
30
+ operation,
31
+ requireAuth: true,
32
+ token,
33
+ });
34
+ if (!data.me) {
35
+ throw new CastleError({
36
+ code: "LOGIN_REQUIRED",
37
+ message: "Log in to Castle before using User.getCurrent().",
38
+ operation,
39
+ });
40
+ }
41
+ return normalizeCurrentUser(data.me, operation);
42
+ }
43
+ function normalizeCurrentUser(user, operation) {
44
+ return {
45
+ userId: requiredString(user.userId, "me.userId", operation),
46
+ username: requiredString(user.username, "me.username", operation),
47
+ isActive: true,
48
+ };
49
+ }
50
+ function requiredString(value, field, operation) {
51
+ if (typeof value === "string" && value.length > 0)
52
+ return value;
53
+ throw new CastleError({
54
+ code: "GRAPHQL_BAD_DATA",
55
+ message: `Castle GraphQL response did not include ${field}.`,
56
+ operation,
57
+ });
58
+ }
package/package.json CHANGED
@@ -1,9 +1,36 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
- "main": "castle.js",
5
+ "main": "dist/castle.js",
6
+ "types": "dist/castle.d.ts",
6
7
  "exports": {
7
- ".": "./castle.js"
8
+ ".": {
9
+ "types": "./dist/castle.d.ts",
10
+ "default": "./dist/castle.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "check": "eslint . && jscpd && tsc --noEmit && tsc"
20
+ },
21
+ "jscpd": {
22
+ "path": [
23
+ "src"
24
+ ],
25
+ "threshold": 0,
26
+ "reporters": [
27
+ "consoleFull"
28
+ ]
29
+ },
30
+ "devDependencies": {
31
+ "eslint": "^9.0.0",
32
+ "jscpd": "^4.0.5",
33
+ "typescript": "^5.0.0",
34
+ "typescript-eslint": "^8.0.0"
8
35
  }
9
36
  }