@pkhex-everywhere/react 0.1.0 → 0.2.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @pkhex-everywhere/react
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fb0df12: Binary inputs take `Uint8Array`, `ArrayBuffer`, `Blob` or `File`, and binary outputs return `Uint8Array`. `game.load(data, fileName?)` takes the name from a `File`. Generated methods and hooks document whether they need a loaded save or an open draft. A missing draft fails with the new `no-draft` error code instead of `not-found`.
8
+ - 938f28c: `createEngine()` starts downloading the runtime as soon as it's created in a browser (`{ lazy: true }` opts out) and reports `engine.status` with file download progress through `engine.onStatusChange`. `wasmHost()` returns one shared runtime per page, so several engines on a page all get changes and events.
9
+ - eab17a2: `react` adds `useEngineStatus()` and `<RequireGame fallback>`, and `useQuery` rewrites `no-save` and `no-draft` messages to name the call and the fix. `engine` no longer exports `createClient`, `affects`, `queryTopics`, `Invoke`, `AssemblyExports` or `EngineExports`, and `engine.call` is gone.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [fb0df12]
14
+ - Updated dependencies [938f28c]
15
+ - Updated dependencies [38e32c9]
16
+ - Updated dependencies [eab17a2]
17
+ - @pkhex-everywhere/engine@0.2.0
18
+
3
19
  ## 0.1.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -10,38 +10,82 @@ npm install @pkhex-everywhere/engine @pkhex-everywhere/react
10
10
 
11
11
  ## Example
12
12
 
13
+ This app opens a save from a file input, lists the party and downloads the edited save:
14
+
13
15
  ```tsx
14
- import { Suspense } from 'react'
16
+ import { Suspense, type ChangeEvent } from 'react'
15
17
  import { createEngine } from '@pkhex-everywhere/engine'
16
- import { EngineProvider, useParty } from '@pkhex-everywhere/react'
18
+ import { EngineProvider, RequireGame, useEngineStatus, useLoadedGame, useParty } from '@pkhex-everywhere/react'
17
19
 
18
20
  const engine = createEngine()
19
21
 
22
+ function Booting() {
23
+ const { loaded, total } = useEngineStatus()
24
+ return <progress value={loaded} max={total || 1} />
25
+ }
26
+
27
+ function OpenSave() {
28
+ const { load } = useLoadedGame()
29
+ const open = (event: ChangeEvent<HTMLInputElement>) => {
30
+ const file = event.target.files?.[0]
31
+ if (file) void load(file)
32
+ }
33
+ return <input type="file" onChange={open} />
34
+ }
35
+
20
36
  function Party() {
21
37
  const { party } = useParty()
22
38
  return (
23
- <ul>
39
+ <ul id="party">
24
40
  {party.map((pokemon) => (
25
- <li key={pokemon.id}>
26
- {pokemon.species} Lv. {pokemon.level}
27
- </li>
41
+ <li key={pokemon.id}>{pokemon.species}</li>
28
42
  ))}
29
43
  </ul>
30
44
  )
31
45
  }
32
46
 
47
+ function Download() {
48
+ const { export: exportSave } = useLoadedGame()
49
+ const download = async () => {
50
+ const { bytes, fileName } = await exportSave()
51
+ const url = URL.createObjectURL(new Blob([bytes]))
52
+ const link = document.createElement('a')
53
+ link.href = url
54
+ link.download = fileName
55
+ link.click()
56
+ setTimeout(() => URL.revokeObjectURL(url))
57
+ }
58
+ return <button onClick={download}>Download</button>
59
+ }
60
+
33
61
  export function App() {
34
62
  return (
35
63
  <EngineProvider engine={engine}>
36
- <Suspense fallback="Loading…">
37
- <Party />
64
+ <Suspense fallback={<Booting />}>
65
+ <RequireGame fallback={<OpenSave />}>
66
+ <Party />
67
+ <Download />
68
+ </RequireGame>
38
69
  </Suspense>
39
70
  </EngineProvider>
40
71
  )
41
72
  }
42
73
  ```
43
74
 
44
- Load a save with `useLoadedGame().load(base64, fileName)` or `engine.game.load` before rendering hooks that read it.
75
+ The Suspense fallback shows the runtime download with `useEngineStatus()`. `<RequireGame>` renders its fallback until a save is loaded, then its children. If the runtime fails to load, the hooks throw the error to the nearest error boundary.
76
+
77
+ CI builds this app from the packed packages and runs it.
78
+
79
+ ## Hooks that need a save
80
+
81
+ Most hooks throw `no-save` when no save is loaded, so render them inside `<RequireGame>`. Hover a hook to see what it needs. A hook that runs without a save fails with a message naming the call and the fix, and the error's `code` stays `no-save`. The same goes for `no-draft`.
82
+
83
+ These work without a save:
84
+
85
+ - `useEngineStatus()`
86
+ - `useEngine()`
87
+ - `useLoadedGame()`, where `game` is `null` until a save is loaded
88
+ - `useQuery` with `game.get`, `game.version`, `game.blankVersions`, `catalog.names` and the `plugins.*` queries
45
89
 
46
90
  ## License
47
91
 
@@ -0,0 +1,5 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare function RequireGame({ fallback, children }: {
3
+ fallback?: ReactNode;
4
+ children?: ReactNode;
5
+ }): ReactNode;
@@ -1,43 +1,66 @@
1
- import type { Base64, ItemHandle, PokemonHandle, PokemonPatch, TrainerGender } from '@pkhex-everywhere/engine';
1
+ import type { Binary, ItemHandle, PokemonHandle, PokemonPatch, TrainerGender } from '@pkhex-everywhere/engine';
2
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
2
3
  export declare function useBox(): {
4
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
3
5
  addEncounter: (index: number) => Promise<import("@pkhex-everywhere/engine").AddedPokemon>;
4
- addFromFile: (bytes: Base64) => Promise<import("@pkhex-everywhere/engine").AddedPokemon>;
6
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
7
+ addFromFile: (bytes: Binary) => Promise<import("@pkhex-everywhere/engine").AddedPokemon>;
5
8
  box: import("@pkhex-everywhere/engine").PokemonSummary[];
6
9
  };
10
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
7
11
  export declare function useEvents(): {
12
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
8
13
  giveTickets: (includeOldSeaMap: boolean) => Promise<string[]>;
14
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
9
15
  setFlag: (index: number, value: boolean) => Promise<void>;
16
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
10
17
  setWork: (index: number, value: number) => Promise<void>;
11
18
  events: import("@pkhex-everywhere/engine").SaveEvents | null;
12
19
  };
13
20
  export declare function useLoadedGame(): {
14
21
  close: () => Promise<void>;
22
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
15
23
  export: () => Promise<import("@pkhex-everywhere/engine").ExportedSave>;
16
24
  file: () => Promise<import("@pkhex-everywhere/engine").LoadedSave | null>;
17
- load: (data: Base64, fileName: string) => Promise<void>;
25
+ load: (data: Binary, fileName?: string | null) => Promise<void>;
18
26
  loadBlank: (version: number) => Promise<void>;
19
27
  game: import("@pkhex-everywhere/engine").SaveSummary | null;
20
28
  };
29
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
21
30
  export declare function useInventory(): {
31
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
22
32
  setItem: (at: ItemHandle, count: number) => Promise<void>;
23
33
  inventory: import("@pkhex-everywhere/engine").Pouch[];
24
34
  };
35
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
25
36
  export declare function useParty(): {
26
37
  party: import("@pkhex-everywhere/engine").PokemonSummary[];
27
38
  };
39
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
28
40
  export declare function usePokemon(at: PokemonHandle): {
41
+ /** Requires a loaded save and an open draft; throws `no-save` or `no-draft` otherwise. Wrap in `<RequireGame>`. */
29
42
  addToBox: () => Promise<import("@pkhex-everywhere/engine").AddedPokemon>;
43
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
30
44
  clone: () => Promise<void>;
45
+ /** Requires a loaded save and an open draft; throws `no-save` or `no-draft` otherwise. Wrap in `<RequireGame>`. */
31
46
  commit: () => Promise<import("@pkhex-everywhere/engine").PokemonId>;
47
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
32
48
  edit: () => Promise<void>;
49
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
33
50
  setLevel: (level: number) => Promise<void>;
51
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
34
52
  update: (patch: PokemonPatch) => Promise<void>;
35
53
  pokemon: import("@pkhex-everywhere/engine").PokemonSummary;
36
54
  };
55
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
37
56
  export declare function useTrainer(): {
57
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
38
58
  setBattlePoints: (value: number) => Promise<void>;
59
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
39
60
  setGender: (gender: TrainerGender) => Promise<void>;
61
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
40
62
  setMoney: (amount: number) => Promise<void>;
63
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
41
64
  setName: (name: string) => Promise<void>;
42
65
  trainer: import("@pkhex-everywhere/engine").TrainerCard;
43
66
  };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { EngineProvider, useEngine } from './EngineProvider';
2
2
  export { useQuery, type CallArgs, type CallResult } from './useQuery';
3
+ export { useEngineStatus } from './useEngineStatus';
4
+ export { RequireGame } from './RequireGame';
3
5
  export * from './generated/hooks';
4
6
  export { usePokemonDetails } from './usePokemonDetails';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
- import { EngineError, affects, queryTopics } from "@pkhex-everywhere/engine";
2
+ import { EngineError } from "@pkhex-everywhere/engine";
3
+ import { affects, call, queryTopics } from "@pkhex-everywhere/engine/internal";
3
4
  import { jsx } from "react/jsx-runtime";
4
5
  //#region src/cache.ts
5
6
  var QueryCache = class {
@@ -91,33 +92,59 @@ function useEngine() {
91
92
  }
92
93
  //#endregion
93
94
  //#region src/useQuery.ts
94
- function useQuery(call, ...args) {
95
+ var fixes = {
96
+ "no-save": (name) => `${name} needs a loaded save. Load one with useLoadedGame().load(file) and wrap the component in <RequireGame>.`,
97
+ "no-draft": (name) => `${name} needs an open draft. Open one with usePokemon(at).edit() or .clone() first.`
98
+ };
99
+ function explain(name, error) {
100
+ if (!(error instanceof EngineError)) return error;
101
+ const fix = fixes[error.code];
102
+ return fix ? new EngineError(error.code, fix(name)) : error;
103
+ }
104
+ function useQuery(name, ...args) {
95
105
  const { engine, cache } = useEngineContext();
96
- const key = JSON.stringify([call, args]);
97
- const entry = useSyncExternalStore(cache.subscribe, () => cache.peek(key)) ?? cache.get(key, queryTopics[call], () => engine.call(call, args));
106
+ const key = JSON.stringify([name, args]);
107
+ const load = () => call(engine, name, args).catch((error) => {
108
+ throw explain(name, error);
109
+ });
110
+ const entry = useSyncExternalStore(cache.subscribe, () => cache.peek(key)) ?? cache.get(key, queryTopics[name], load);
98
111
  if (entry.status === "pending") throw entry.promise;
99
112
  if (entry.status === "error") throw entry.error;
100
113
  return entry.value;
101
114
  }
102
115
  //#endregion
116
+ //#region src/useEngineStatus.ts
117
+ function useEngineStatus() {
118
+ const engine = useEngine();
119
+ const read = () => engine.status;
120
+ return useSyncExternalStore(engine.onStatusChange, read, read);
121
+ }
122
+ //#endregion
103
123
  //#region src/generated/hooks.ts
124
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
104
125
  function useBox() {
105
126
  const engine = useEngine();
106
127
  return {
107
128
  box: useQuery("box.get"),
108
129
  ...useMemo(() => ({
130
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
109
131
  addEncounter: (index) => engine.box.addEncounter(index),
132
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
110
133
  addFromFile: (bytes) => engine.box.addFromFile(bytes)
111
134
  }), [engine])
112
135
  };
113
136
  }
137
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
114
138
  function useEvents() {
115
139
  const engine = useEngine();
116
140
  return {
117
141
  events: useQuery("events.get"),
118
142
  ...useMemo(() => ({
143
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
119
144
  giveTickets: (includeOldSeaMap) => engine.events.giveTickets(includeOldSeaMap),
145
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
120
146
  setFlag: (index, value) => engine.events.setFlag(index, value),
147
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
121
148
  setWork: (index, value) => engine.events.setWork(index, value)
122
149
  }), [engine])
123
150
  };
@@ -128,6 +155,7 @@ function useLoadedGame() {
128
155
  game: useQuery("game.get"),
129
156
  ...useMemo(() => ({
130
157
  close: () => engine.game.close(),
158
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
131
159
  export: () => engine.game.export(),
132
160
  file: () => engine.game.file(),
133
161
  load: (data, fileName) => engine.game.load(data, fileName),
@@ -135,16 +163,21 @@ function useLoadedGame() {
135
163
  }), [engine])
136
164
  };
137
165
  }
166
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
138
167
  function useInventory() {
139
168
  const engine = useEngine();
140
169
  return {
141
170
  inventory: useQuery("inventory.get"),
142
- ...useMemo(() => ({ setItem: (at, count) => engine.inventory.setItem(at, count) }), [engine])
171
+ ...useMemo(() => ({
172
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
173
+ setItem: (at, count) => engine.inventory.setItem(at, count) }), [engine])
143
174
  };
144
175
  }
176
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
145
177
  function useParty() {
146
178
  return { party: useQuery("party.get") };
147
179
  }
180
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
148
181
  function usePokemon(at) {
149
182
  const engine = useEngine();
150
183
  const pokemon = useQuery("pokemon.get", at);
@@ -152,28 +185,45 @@ function usePokemon(at) {
152
185
  return {
153
186
  pokemon,
154
187
  ...useMemo(() => ({
188
+ /** Requires a loaded save and an open draft; throws `no-save` or `no-draft` otherwise. Wrap in `<RequireGame>`. */
155
189
  addToBox: () => engine.pokemon.addToBox(),
190
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
156
191
  clone: () => engine.pokemon.clone(at),
192
+ /** Requires a loaded save and an open draft; throws `no-save` or `no-draft` otherwise. Wrap in `<RequireGame>`. */
157
193
  commit: () => engine.pokemon.commit(),
194
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
158
195
  edit: () => engine.pokemon.edit(at),
196
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
159
197
  setLevel: (level) => engine.pokemon.setLevel(at, level),
198
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
160
199
  update: (patch) => engine.pokemon.update(at, patch)
161
200
  }), [engine, key])
162
201
  };
163
202
  }
203
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
164
204
  function useTrainer() {
165
205
  const engine = useEngine();
166
206
  return {
167
207
  trainer: useQuery("trainer.get"),
168
208
  ...useMemo(() => ({
209
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
169
210
  setBattlePoints: (value) => engine.trainer.setBattlePoints(value),
211
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
170
212
  setGender: (gender) => engine.trainer.setGender(gender),
213
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
171
214
  setMoney: (amount) => engine.trainer.setMoney(amount),
215
+ /** Requires a loaded save; throws `no-save` otherwise. Wrap in `<RequireGame>`. */
172
216
  setName: (name) => engine.trainer.setName(name)
173
217
  }), [engine])
174
218
  };
175
219
  }
176
220
  //#endregion
221
+ //#region src/RequireGame.tsx
222
+ function RequireGame({ fallback = null, children }) {
223
+ const { game } = useLoadedGame();
224
+ return game ? children : fallback;
225
+ }
226
+ //#endregion
177
227
  //#region src/usePokemonDetails.ts
178
228
  function usePokemonDetails(at) {
179
229
  const engine = useEngine();
@@ -185,4 +235,4 @@ function usePokemonDetails(at) {
185
235
  };
186
236
  }
187
237
  //#endregion
188
- export { EngineProvider, useBox, useEngine, useEvents, useInventory, useLoadedGame, useParty, usePokemon, usePokemonDetails, useQuery, useTrainer };
238
+ export { EngineProvider, RequireGame, useBox, useEngine, useEngineStatus, useEvents, useInventory, useLoadedGame, useParty, usePokemon, usePokemonDetails, useQuery, useTrainer };
@@ -0,0 +1,2 @@
1
+ import type { EngineStatus } from '@pkhex-everywhere/engine';
2
+ export declare function useEngineStatus(): EngineStatus;
@@ -3,5 +3,5 @@ type Lookup<T, Path extends string> = Path extends `${infer Head}.${infer Rest}`
3
3
  type CallFunction<Name extends CallName> = Extract<Lookup<EngineClient, Name>, (...args: never[]) => Promise<unknown>>;
4
4
  export type CallArgs<Name extends CallName> = Parameters<CallFunction<Name>>;
5
5
  export type CallResult<Name extends CallName> = Awaited<ReturnType<CallFunction<Name>>>;
6
- export declare function useQuery<Name extends QueryName>(call: Name, ...args: CallArgs<Name>): CallResult<Name>;
6
+ export declare function useQuery<Name extends QueryName>(name: Name, ...args: CallArgs<Name>): CallResult<Name>;
7
7
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pkhex-everywhere/react",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "React hooks for @pkhex-everywhere/engine.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {