@rustra/react 0.5.0 → 0.7.1

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
@@ -19,5 +19,33 @@ function Item({ id }: { id: string }) {
19
19
  ```
20
20
 
21
21
  The package provides `RustraProvider`, `useRustraEngine`, `useCommand`,
22
- `useMutation`, and `useEvent`. React is a peer dependency; configure the
23
- platform-specific Rustra engine before rendering the provider.
22
+ `useMutation`, `useEvent`, and `useSuspenseCommand`. React is a peer dependency;
23
+ configure the platform-specific Rustra engine before rendering the provider.
24
+
25
+ `useSuspenseCommand(commandFn, input?, options?)` is the Suspense-compatible read:
26
+ it suspends by throwing the in-flight promise until the command resolves, then
27
+ returns the value directly (a rejection is re-thrown to the nearest error
28
+ boundary). Results live in a module-level cache keyed by command name + serialized
29
+ input for the whole session — there is no eviction — so call
30
+ `invalidateCommands(commandName?)` to drop one command's entries (or, with no
31
+ argument, the whole cache) and make the next render re-invoke.
32
+
33
+ ```tsx
34
+ import { Suspense } from 'react';
35
+ import { invalidateCommands, useSuspenseCommand } from '@rustra/react';
36
+ import { getItem, saveItem } from './generated/commands.js';
37
+
38
+ function Item({ id }: { id: string }) {
39
+ const item = useSuspenseCommand(getItem, { id }); // suspends until resolved
40
+ return <span>{item.name}</span>;
41
+ }
42
+
43
+ async function rename(id: string, name: string) {
44
+ await saveItem({ id, name });
45
+ invalidateCommands('getItem'); // next render of <Item> re-fetches
46
+ }
47
+
48
+ <Suspense fallback={<span>Loading...</span>}>
49
+ <Item id="item-1" />
50
+ </Suspense>;
51
+ ```
package/dist/index.d.ts CHANGED
@@ -26,6 +26,8 @@ export { useCommand } from './useCommand.js';
26
26
  export type { UseCommandOptions, UseCommandResult, CommandFn, VoidCommandFn, } from './useCommand.js';
27
27
  export { useMutation } from './useMutation.js';
28
28
  export type { UseMutationOptions, UseMutationResult } from './useMutation.js';
29
+ export { useSuspenseCommand, invalidateCommands } from './useSuspenseCommand.js';
30
+ export type { SuspenseEntry } from './useSuspenseCommand.js';
29
31
  export { useEvent } from './useEvent.js';
30
32
  export type { EventCallback, UnsubscribeFn, SubscribeResult, SubscribeFn } from './useEvent.js';
31
33
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/D,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE9E,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/D,YAAY,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,EACT,aAAa,GACd,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE9E,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACjF,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -23,5 +23,6 @@
23
23
  export { RustraProvider, useRustraEngine } from './context.js';
24
24
  export { useCommand } from './useCommand.js';
25
25
  export { useMutation } from './useMutation.js';
26
+ export { useSuspenseCommand, invalidateCommands } from './useSuspenseCommand.js';
26
27
  export { useEvent } from './useEvent.js';
27
28
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAQ7C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAG/D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAQ7C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAG/C,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAGjF,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,50 @@
1
+ import type { InvokeOptions } from '@rustra/types';
2
+ import type { CommandFn, VoidCommandFn } from './useCommand.js';
3
+ /**
4
+ * Cache entry state machine — `pending` until the in-flight promise settles,
5
+ * then `fulfilled` (value stored) or `rejected` (error stored).
6
+ */
7
+ export interface SuspenseEntry<O = unknown> {
8
+ /** Owning command — `invalidateCommands` matches this field, not the key prefix. */
9
+ commandName: string;
10
+ promise: Promise<O>;
11
+ status: 'pending' | 'fulfilled' | 'rejected';
12
+ value?: O;
13
+ error?: unknown;
14
+ }
15
+ /**
16
+ * Pure cache state machine: returns the existing entry for `key` when present,
17
+ * otherwise creates one by invoking `start()` and drives it to its terminal
18
+ * state. Settling updates the entry in place, so later accessors read the
19
+ * cached value/error without re-invoking. Rejections are normalized to `Error`
20
+ * (matching `useCommand`/`useMutation`); the stored object is re-thrown as-is.
21
+ *
22
+ * Exported for testing — the hook stays thin, and this is testable without
23
+ * React (a thrown promise is awkward to exercise under SSR renderToString).
24
+ */
25
+ export declare function resolveSuspenseEntry<O>(key: string, commandName: string, start: () => Promise<O>): SuspenseEntry<O>;
26
+ /**
27
+ * Invalidate cached suspense entries.
28
+ *
29
+ * - `invalidateCommands('getItem')` — drop only that command's entries,
30
+ * matched by the entry's owning command (exact, so command names containing
31
+ * `::` cannot be over-matched by a shorter prefix).
32
+ * - `invalidateCommands()` — drop every cached entry.
33
+ *
34
+ * Works outside components (module-level cache), e.g. after a mutation.
35
+ */
36
+ export declare function invalidateCommands(commandName?: string): void;
37
+ /**
38
+ * Suspense-compatible command hook. Must be called under a Suspense boundary.
39
+ *
40
+ * First call starts the invocation and throws the in-flight promise —
41
+ * thrown-thenable Suspense on React 18, `use` on React 19. Once settled,
42
+ * subsequent calls return the cached value. A rejected invocation re-throws
43
+ * the same stored error object on every access (error boundary contract) until
44
+ * `invalidateCommands` clears it.
45
+ *
46
+ * `options` are honored only on the first invocation per key; later calls
47
+ * reuse the cached promise.
48
+ */
49
+ export declare function useSuspenseCommand<I, O>(commandFn: CommandFn<I, O> | VoidCommandFn<O>, input?: I, options?: InvokeOptions): O;
50
+ //# sourceMappingURL=useSuspenseCommand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSuspenseCommand.d.ts","sourceRoot":"","sources":["../src/useSuspenseCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhE;;;GAGG;AACH,MAAM,WAAW,aAAa,CAAC,CAAC,GAAG,OAAO;IACxC,oFAAoF;IACpF,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;IAC7C,KAAK,CAAC,EAAE,CAAC,CAAC;IACV,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAeD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACtB,aAAa,CAAC,CAAC,CAAC,CAmBlB;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAQ7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,CAAC,EACrC,SAAS,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,EAC7C,KAAK,CAAC,EAAE,CAAC,EACT,OAAO,CAAC,EAAE,aAAa,GACtB,CAAC,CAYH"}
@@ -0,0 +1,85 @@
1
+ import { resolveCommandId } from '@rustra/types';
2
+ import { useRustraEngine } from './context.js';
3
+ import { inputKey } from './input-key.js';
4
+ /**
5
+ * Module-level suspense cache. Key is the resolved command name followed by
6
+ * `::` and the serialized input — an absent input collapses to the bare
7
+ * `commandName + '::'` suffix, otherwise `inputKey(input)` (bigint-safe).
8
+ *
9
+ * No eviction policy by design (YAGNI) — entries live for the session.
10
+ */
11
+ const cache = new Map();
12
+ function cacheKey(commandName, input) {
13
+ return `${commandName}::${input === undefined ? '' : inputKey(input)}`;
14
+ }
15
+ /**
16
+ * Pure cache state machine: returns the existing entry for `key` when present,
17
+ * otherwise creates one by invoking `start()` and drives it to its terminal
18
+ * state. Settling updates the entry in place, so later accessors read the
19
+ * cached value/error without re-invoking. Rejections are normalized to `Error`
20
+ * (matching `useCommand`/`useMutation`); the stored object is re-thrown as-is.
21
+ *
22
+ * Exported for testing — the hook stays thin, and this is testable without
23
+ * React (a thrown promise is awkward to exercise under SSR renderToString).
24
+ */
25
+ export function resolveSuspenseEntry(key, commandName, start) {
26
+ const existing = cache.get(key);
27
+ if (existing)
28
+ return existing;
29
+ const promise = start();
30
+ const entry = { commandName, promise, status: 'pending' };
31
+ cache.set(key, entry);
32
+ promise.then((value) => {
33
+ entry.status = 'fulfilled';
34
+ entry.value = value;
35
+ }, (err) => {
36
+ entry.status = 'rejected';
37
+ entry.error = err instanceof Error ? err : new Error(String(err));
38
+ });
39
+ return entry;
40
+ }
41
+ /**
42
+ * Invalidate cached suspense entries.
43
+ *
44
+ * - `invalidateCommands('getItem')` — drop only that command's entries,
45
+ * matched by the entry's owning command (exact, so command names containing
46
+ * `::` cannot be over-matched by a shorter prefix).
47
+ * - `invalidateCommands()` — drop every cached entry.
48
+ *
49
+ * Works outside components (module-level cache), e.g. after a mutation.
50
+ */
51
+ export function invalidateCommands(commandName) {
52
+ if (commandName === undefined) {
53
+ cache.clear();
54
+ return;
55
+ }
56
+ for (const [key, entry] of cache) {
57
+ if (entry.commandName === commandName)
58
+ cache.delete(key);
59
+ }
60
+ }
61
+ /**
62
+ * Suspense-compatible command hook. Must be called under a Suspense boundary.
63
+ *
64
+ * First call starts the invocation and throws the in-flight promise —
65
+ * thrown-thenable Suspense on React 18, `use` on React 19. Once settled,
66
+ * subsequent calls return the cached value. A rejected invocation re-throws
67
+ * the same stored error object on every access (error boundary contract) until
68
+ * `invalidateCommands` clears it.
69
+ *
70
+ * `options` are honored only on the first invocation per key; later calls
71
+ * reuse the cached promise.
72
+ */
73
+ export function useSuspenseCommand(commandFn, input, options) {
74
+ const engine = useRustraEngine();
75
+ // minify-안전 식별: 코드젠이 심은 commandId 를 우선한다 (Function.name 은
76
+ // 프로덕션 번들러 mangling 으로 바뀔 수 있다).
77
+ const commandName = resolveCommandId(commandFn);
78
+ const entry = resolveSuspenseEntry(cacheKey(commandName, input), commandName, () => engine.invoke(commandName, input, options));
79
+ if (entry.status === 'pending')
80
+ throw entry.promise;
81
+ if (entry.status === 'rejected')
82
+ throw entry.error;
83
+ return entry.value;
84
+ }
85
+ //# sourceMappingURL=useSuspenseCommand.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSuspenseCommand.js","sourceRoot":"","sources":["../src/useSuspenseCommand.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAE/C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAe1C;;;;;;GAMG;AACH,MAAM,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;AAE/C,SAAS,QAAQ,CAAC,WAAmB,EAAE,KAAc;IACnD,OAAO,GAAG,WAAW,KAAK,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACzE,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAClC,GAAW,EACX,WAAmB,EACnB,KAAuB;IAEvB,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,QAAQ;QAAE,OAAO,QAA4B,CAAC;IAElD,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IACxB,MAAM,KAAK,GAAqB,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC5E,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAEtB,OAAO,CAAC,IAAI,CACV,CAAC,KAAK,EAAE,EAAE;QACR,KAAK,CAAC,MAAM,GAAG,WAAW,CAAC;QAC3B,KAAK,CAAC,KAAK,GAAG,KAAK,CAAC;IACtB,CAAC,EACD,CAAC,GAAY,EAAE,EAAE;QACf,KAAK,CAAC,MAAM,GAAG,UAAU,CAAC;QAC1B,KAAK,CAAC,KAAK,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACpE,CAAC,CACF,CAAC;IACF,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,WAAoB;IACrD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,OAAO;IACT,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC;QACjC,IAAI,KAAK,CAAC,WAAW,KAAK,WAAW;YAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,SAA6C,EAC7C,KAAS,EACT,OAAuB;IAEvB,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;IACjC,0DAA0D;IAC1D,iCAAiC;IACjC,MAAM,WAAW,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,oBAAoB,CAAI,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,CACpF,MAAM,CAAC,MAAM,CAAI,WAAW,EAAE,KAAK,EAAE,OAAO,CAAC,CAC9C,CAAC;IAEF,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;QAAE,MAAM,KAAK,CAAC,OAAO,CAAC;IACpD,IAAI,KAAK,CAAC,MAAM,KAAK,UAAU;QAAE,MAAM,KAAK,CAAC,KAAK,CAAC;IACnD,OAAO,KAAK,CAAC,KAAU,CAAC;AAC1B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rustra/react",
3
- "version": "0.5.0",
3
+ "version": "0.7.1",
4
4
  "description": "React and React Native bindings and hooks for rustra-bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "react": ">=18.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "@rustra/types": "^0.6.0"
33
+ "@rustra/types": "^0.9.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/react": "^19.0.0",