@indietabletop/appkit 7.0.0-rc.3 → 7.0.0-rc.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.
@@ -58,7 +58,7 @@ export type AppConfig = {
58
58
  */
59
59
  fmt: AppkitFormatters;
60
60
 
61
- database: ModernIDB<any, any> & DatabaseAppMachineMethods;
61
+ database: ModernIDB<any, any> & DatabaseAppMachineMethods<any>;
62
62
  };
63
63
 
64
64
  const [AppConfigContext, useAppConfig] = createStrictContext<AppConfig>();
package/lib/client.ts CHANGED
@@ -14,7 +14,6 @@ import {
14
14
  partial,
15
15
  string,
16
16
  Struct,
17
- unknown,
18
17
  type Infer,
19
18
  } from "superstruct";
20
19
  import { Failure, Success } from "./async-op.ts";
@@ -42,9 +41,8 @@ type ClientEventMap = {
42
41
  };
43
42
 
44
43
  type ClientEventArgs<T extends ClientEventType> =
45
- ClientEventMap[T] extends undefined
46
- ? [type: T]
47
- : [type: T, detail: ClientEventMap[T]];
44
+ ClientEventMap[T] extends undefined ? [type: T]
45
+ : [type: T, detail: ClientEventMap[T]];
48
46
 
49
47
  export class ClientEvent<T extends keyof ClientEventMap> extends CustomEvent<
50
48
  ClientEventMap[T]
@@ -70,8 +68,8 @@ function toParams(init: Record<string, Primitives | Array<Primitives>>) {
70
68
  const params = new URLSearchParams();
71
69
 
72
70
  const entries = Object.entries(init).flatMap(([key, value]) => {
73
- return Array.isArray(value)
74
- ? value.map((v) => [key, v] as const)
71
+ return Array.isArray(value) ?
72
+ value.map((v) => [key, v] as const)
75
73
  : [[key, value] as const];
76
74
  });
77
75
 
@@ -82,13 +80,22 @@ function toParams(init: Record<string, Primitives | Array<Primitives>>) {
82
80
  return params;
83
81
  }
84
82
 
85
- export class IndieTabletopClient {
83
+ export class IndieTabletopClient<
84
+ SnapshotPayload = any,
85
+ GameDataPayload = any,
86
+ RulesetPayload = any,
87
+ > {
86
88
  origin: string;
87
89
  private refreshTokenPromise?: Promise<
88
90
  Success<{ sessionInfo: SessionInfo }> | Failure<FailurePayload>
89
91
  >;
90
92
  private maxLogLevel: number;
91
93
  private eventTarget: EventTarget;
94
+ private validation: {
95
+ snapshot: Struct<SnapshotPayload, any>;
96
+ gameData: Struct<GameDataPayload, any>;
97
+ ruleset: Struct<RulesetPayload, any>;
98
+ };
92
99
 
93
100
  constructor(props: {
94
101
  apiOrigin: string;
@@ -119,10 +126,17 @@ export class IndieTabletopClient {
119
126
  * @default 'info'
120
127
  */
121
128
  logLevel?: LogLevel;
129
+
130
+ validation: {
131
+ snapshot: Struct<SnapshotPayload, any>;
132
+ gameData: Struct<GameDataPayload, any>;
133
+ ruleset: Struct<RulesetPayload, any>;
134
+ };
122
135
  }) {
123
136
  this.eventTarget = new EventTarget();
124
137
  this.origin = props.apiOrigin;
125
138
  this.maxLogLevel = props.logLevel ? logLevelToInt[props.logLevel] : 1;
139
+ this.validation = props.validation;
126
140
 
127
141
  // If handlers were passed to the constructor, we set them up here. No need
128
142
  // to clean them up, as if the instance is destroyed, the listeners will
@@ -466,12 +480,11 @@ export class IndieTabletopClient {
466
480
  return req;
467
481
  }
468
482
 
469
- async getSnapshot<T, S>(
470
- gameCode: string,
471
- snapshotId: string,
472
- struct: Struct<T, S>,
473
- ) {
474
- return await this.fetch(`/v1/snapshots/${gameCode}/${snapshotId}`, struct);
483
+ async getSnapshot(gameCode: string, snapshotId: string) {
484
+ return await this.fetch(
485
+ `/v1/snapshots/${gameCode}/${snapshotId}`,
486
+ this.validation.snapshot,
487
+ );
475
488
  }
476
489
 
477
490
  async createSnapshot(gameCode: string, payload: object) {
@@ -503,7 +516,10 @@ export class IndieTabletopClient {
503
516
  }
504
517
 
505
518
  getRuleset(game: string, version: string) {
506
- return this.fetch(`/v1/rulesets/${game}/${version}`, unknown());
519
+ return this.fetch(
520
+ `/v1/rulesets/${game}/${version}`,
521
+ this.validation.ruleset,
522
+ );
507
523
  }
508
524
 
509
525
  /**
@@ -578,7 +594,9 @@ export class IndieTabletopClient {
578
594
 
579
595
  async pullUserData(props: {
580
596
  sinceTs: number | null;
581
- include: GameCode | GameCode[];
597
+ include:
598
+ | (string & keyof GameDataPayload)
599
+ | (string & keyof GameDataPayload)[];
582
600
  expectCurrentUserId: string;
583
601
  }) {
584
602
  const params = toParams({
@@ -590,30 +608,26 @@ export class IndieTabletopClient {
590
608
 
591
609
  return await this.fetch(
592
610
  `/v1/me/game-data?${params}`,
593
- unknown() as Struct<UserGameData, null>,
611
+ this.validation.gameData,
594
612
  );
595
613
  }
596
614
 
597
615
  async pushUserData(props: {
598
616
  currentSyncTs: number;
599
617
  pullSinceTs: number | null;
600
- data: UserGameData;
618
+ data: GameDataPayload;
601
619
  expectCurrentUserId: string | null | undefined;
602
620
  }) {
603
- return await this.fetch(
604
- `/v1/me/game-data`,
605
- unknown() as Struct<UserGameData, null>,
606
- {
607
- method: "PATCH",
608
- json: {
609
- data: props.data,
610
- syncedTs: props.currentSyncTs,
611
- pullSinceTs: props.pullSinceTs ?? 0,
612
- pullOmitDeleted: !props.pullSinceTs,
613
- expectCurrentUserId: props.expectCurrentUserId,
614
- },
621
+ return await this.fetch(`/v1/me/game-data`, this.validation.gameData, {
622
+ method: "PATCH",
623
+ json: {
624
+ data: props.data,
625
+ syncedTs: props.currentSyncTs,
626
+ pullSinceTs: props.pullSinceTs ?? 0,
627
+ pullOmitDeleted: !props.pullSinceTs,
628
+ expectCurrentUserId: props.expectCurrentUserId,
615
629
  },
616
- );
630
+ });
617
631
  }
618
632
 
619
633
  async getUnlocks<T extends FeatureUnlock["gameCode"]>(gameCode: T) {
package/lib/hrefs.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { omitUndefinedKeys } from "./omitUndefinedKeys.ts";
1
2
  import type { LinkUtmParams, createUtm } from "./utm.ts";
2
3
 
3
4
  export function withParams(path: string, params?: Record<string, string>) {
@@ -5,7 +6,12 @@ export function withParams(path: string, params?: Record<string, string>) {
5
6
  return path;
6
7
  }
7
8
 
8
- return `${path}?${new URLSearchParams(params)}`;
9
+ const cleanParams = omitUndefinedKeys(params);
10
+ if (Object.keys(cleanParams).length === 0) {
11
+ return path;
12
+ }
13
+
14
+ return `${path}?${new URLSearchParams(cleanParams)}`;
9
15
  }
10
16
 
11
17
  export type AccountParams = {
@@ -1,10 +1,9 @@
1
- import type { UserGameData } from "@indietabletop/types";
2
1
  import { useActorRef, useSelector } from "@xstate/react";
3
2
  import { useMemo, type ReactNode } from "react";
4
3
  import { Actor, fromCallback, fromPromise } from "xstate";
5
4
  import { useAppConfig } from "../AppConfig/AppConfig.tsx";
6
5
  import { Failure, Success } from "../async-op.ts";
7
- import type { GameCode, IndieTabletopClient } from "../client.ts";
6
+ import type { IndieTabletopClient } from "../client.ts";
8
7
  import { createStrictContext } from "../createStrictContext.ts";
9
8
  import type { ModernIDB } from "../ModernIDB/ModernIDB.ts";
10
9
  import type { ModernIDBIndexes, ModernIDBSchema } from "../ModernIDB/types.ts";
@@ -16,7 +15,7 @@ import {
16
15
  type PushChangesInput,
17
16
  } from "./store.ts";
18
17
  import type { MachineEvent, PullResult, PushResult } from "./types.ts";
19
- import { toSyncedItems } from "./utils.ts";
18
+ import { toSyncedItems, type UserGameDataShape } from "./utils.ts";
20
19
 
21
20
  export type AppActions = ReturnType<typeof useCreateActions>;
22
21
 
@@ -104,13 +103,13 @@ export function useAppActions() {
104
103
  return useAppMachineContext().actions;
105
104
  }
106
105
 
107
- export type DatabaseAppMachineMethods = {
108
- upsertGameData(data: UserGameData): Promise<Success<string[]>>;
106
+ export type DatabaseAppMachineMethods<GameDataPayload> = {
107
+ upsertGameData(data: GameDataPayload): Promise<Success<string[]>>;
109
108
 
110
109
  getUpdatedGameDataSince(props: {
111
110
  sinceTs: number | null;
112
111
  exclude: Set<string>;
113
- }): Promise<Success<UserGameData>>;
112
+ }): Promise<Success<GameDataPayload>>;
114
113
 
115
114
  clearAll(): Promise<Success<string> | Failure<string>>;
116
115
  };
@@ -118,9 +117,14 @@ export type DatabaseAppMachineMethods = {
118
117
  export function createMachine<
119
118
  Schema extends ModernIDBSchema,
120
119
  Indexes extends ModernIDBIndexes<Schema>,
120
+ SnapshotPayload,
121
+ GameDataPayload extends UserGameDataShape,
122
+ RulesetPayload,
121
123
  >(options: {
122
- database: ModernIDB<Schema, Indexes> & DatabaseAppMachineMethods;
123
- client: IndieTabletopClient;
124
+ database: ModernIDB<Schema, Indexes> &
125
+ DatabaseAppMachineMethods<GameDataPayload>;
126
+
127
+ client: IndieTabletopClient<SnapshotPayload, GameDataPayload, RulesetPayload>;
124
128
 
125
129
  /**
126
130
  * Which games should the machine pull game data for?
@@ -130,7 +134,10 @@ export function createMachine<
130
134
  * @remarks Honestly this is a bit hacky and we should have a better way to
131
135
  * disable sync entirely for apps that don't need it (e.g. the Creators App).
132
136
  */
133
- pullGameDataFor: GameCode | GameCode[] | null;
137
+ pullGameDataFor:
138
+ | (string & keyof GameDataPayload)
139
+ | (string & keyof GameDataPayload)[]
140
+ | null;
134
141
  }) {
135
142
  const { client, database } = options;
136
143
 
@@ -1,12 +1,22 @@
1
- import type { UserGameData } from "@indietabletop/types";
2
1
  import { Failure } from "../async-op.js";
3
2
  import type { FailurePayload } from "../types.ts";
4
3
  import type { SyncedItem } from "./types.ts";
5
4
 
5
+ export type UserGameDataShape = Record<
6
+ string,
7
+ Record<
8
+ string,
9
+ (
10
+ | { id: string; name: string; updatedTs: number }
11
+ | { id: string; name: string; updatedTs: number; deleted: boolean }
12
+ )[]
13
+ >
14
+ >;
15
+
6
16
  /**
7
17
  * Flattens the UserGameData structure to a flat SyncedItem[].
8
18
  */
9
- export function toSyncedItems(data: UserGameData) {
19
+ export function toSyncedItems(data: UserGameDataShape) {
10
20
  return Object.values(data).flatMap((groups) => {
11
21
  return Object.values(groups).flatMap((items) => {
12
22
  return items.map((item): SyncedItem => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indietabletop/appkit",
3
- "version": "7.0.0-rc.3",
3
+ "version": "7.0.0-rc.5",
4
4
  "description": "A collection of modules used in apps built by Indie Tabletop Club",
5
5
  "private": false,
6
6
  "type": "module",