castle-web-sdk 0.4.27 → 0.4.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -416,7 +416,10 @@ const dailyPuzzle = (date.daysSinceCastleEpoch % 30) + 1;
416
416
  Returns the signed-in player. Throws `CastleError`
417
417
  (`LOGIN_REQUIRED`) when nobody is signed in.
418
418
 
419
- The returned `CastleUser` has `userId`, `username`, `isAnonymous`, and `isActive`.
419
+ A `CastleUser` has `userId`, `username`, `isAnonymous`, `isActive`, `photoUrl`
420
+ (their 256px avatar), `frameUrl` (the 96px frame drawn around it) and `color`
421
+ (the hex their username is shown in); the last three are `null` when unset. The
422
+ urls are `data:` urls, so they go straight into an `<img>` or a texture.
420
423
  Anonymous accounts are real logins with generated `anonymous-user-...` names;
421
424
  check `isAnonymous` before accepting writes into anything other players see,
422
425
  such as a shared gallery.
@@ -426,6 +429,19 @@ const me = await User.getCurrent();
426
429
  greet(me.username);
427
430
  ```
428
431
 
432
+ ### `User.get(userId): Promise<CastleUser | null>`
433
+
434
+ Another player as anyone may see them, or `null` when no such user exists.
435
+ Results are cached per id for the life of the page. The ids come from the
436
+ session's player list or from `Store.user(id)` rows.
437
+
438
+ ```js
439
+ const them = await User.get(player.userId);
440
+ if (them?.photoUrl) {
441
+ avatar.src = them.photoUrl;
442
+ }
443
+ ```
444
+
429
445
  ## Pass
430
446
 
431
447
  A pass is something a creator sells to players for Castle bricks (the
@@ -120,6 +120,9 @@ export interface CommandParams {
120
120
  score?: number | null;
121
121
  };
122
122
  "user.getCurrent": Record<string, never>;
123
+ "user.get": {
124
+ userId: string;
125
+ };
123
126
  "time.getServerTime": Record<string, never>;
124
127
  "pass.has": {
125
128
  passId: string;
@@ -214,6 +217,9 @@ export interface CommandResult {
214
217
  isAnonymous?: boolean;
215
218
  } | null;
216
219
  };
220
+ "user.get": {
221
+ user: UserPayload | null;
222
+ };
217
223
  "time.getServerTime": {
218
224
  timestamp: number;
219
225
  timezoneOffset: number;
@@ -254,6 +260,14 @@ export interface CommandResult {
254
260
  };
255
261
  "lifecycle.restoreState": LifecycleRestoreStateResult;
256
262
  }
263
+ export interface UserPayload {
264
+ userId: string;
265
+ username: string;
266
+ isAnonymous: boolean;
267
+ photoUrl: string | null;
268
+ frameUrl: string | null;
269
+ color: string | null;
270
+ }
257
271
  export type CommandName = keyof CommandParams;
258
272
  export interface SerializedCommandError {
259
273
  code: string;
package/dist/user.d.ts CHANGED
@@ -3,8 +3,12 @@ export interface CastleUser {
3
3
  username: string;
4
4
  isAnonymous: boolean;
5
5
  isActive: boolean;
6
+ photoUrl: string | null;
7
+ frameUrl: string | null;
8
+ color: string | null;
6
9
  }
7
10
  export interface CastleUserApi {
8
11
  getCurrent(): Promise<CastleUser>;
12
+ get(userId: string): Promise<CastleUser | null>;
9
13
  }
10
14
  export declare const User: CastleUserApi;
package/dist/user.js CHANGED
@@ -2,8 +2,12 @@ import { CastleError } from "./errors";
2
2
  import { hostRequest } from "./transport";
3
3
  let currentUser = null;
4
4
  let currentUserPromise = null;
5
+ // Profiles by user id. A profile changes rarely, and a multiplayer deck asks for
6
+ // the same few ids every time a player joins.
7
+ const profiles = new Map();
5
8
  export const User = {
6
9
  getCurrent,
10
+ get,
7
11
  };
8
12
  async function getCurrent() {
9
13
  if (currentUser)
@@ -25,15 +29,46 @@ async function fetchCurrentUser() {
25
29
  });
26
30
  }
27
31
  const username = requiredString(user.username, "user.username", operation);
32
+ const userId = requiredString(user.userId, "user.userId", operation);
33
+ // The host answers identity from what it already knows. The profile fields
34
+ // come from the same read `get` does; a host that predates that command
35
+ // leaves them null.
36
+ const profile = await get(userId).catch(() => null);
28
37
  return {
29
- userId: requiredString(user.userId, "user.userId", operation),
38
+ userId,
30
39
  username,
31
40
  // A host that predates the flag omits it; anonymous account usernames are
32
41
  // always minted as `anonymous-user-<uuid>`, so the prefix is the fallback.
33
42
  isAnonymous: user.isAnonymous === true || username.toLowerCase().startsWith("anonymous-user-"),
34
43
  isActive: true,
44
+ photoUrl: profile?.photoUrl ?? null,
45
+ frameUrl: profile?.frameUrl ?? null,
46
+ color: profile?.color ?? null,
35
47
  };
36
48
  }
49
+ // One user by id, or null when no such user exists. Throws `CastleError` on a
50
+ // host that predates the command (`UNKNOWN_COMMAND`).
51
+ async function get(userId) {
52
+ if (typeof userId !== "string" || userId.length === 0) {
53
+ throw new CastleError({
54
+ code: "INVALID_ARGUMENT",
55
+ message: "User.get(userId) needs a user id.",
56
+ operation: "User.get",
57
+ });
58
+ }
59
+ let pending = profiles.get(userId);
60
+ if (!pending) {
61
+ pending = fetchProfile(userId);
62
+ profiles.set(userId, pending);
63
+ // A failed read is not kept, so the next call asks again.
64
+ pending.catch(() => profiles.delete(userId));
65
+ }
66
+ return pending;
67
+ }
68
+ async function fetchProfile(userId) {
69
+ const { user } = await hostRequest("user.get", { userId });
70
+ return user ? { ...user, isActive: true } : null;
71
+ }
37
72
  function requiredString(value, field, operation) {
38
73
  if (typeof value === "string" && value.length > 0)
39
74
  return value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "castle-web-sdk",
3
- "version": "0.4.27",
3
+ "version": "0.4.28",
4
4
  "type": "module",
5
5
  "main": "dist/castle.js",
6
6
  "types": "dist/castle.d.ts",