@pulse-js/react 0.1.3 → 0.1.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.
package/README.md CHANGED
@@ -57,13 +57,23 @@ function ProtectedRoute() {
57
57
  }
58
58
 
59
59
  if (status === "fail") {
60
- return <AccessDenied message={reason} />;
60
+ return <AccessDenied message={formatReason(reason)} />;
61
61
  }
62
62
 
63
63
  return <AdminDashboard />;
64
64
  }
65
65
  ```
66
66
 
67
+ ### Rendering Failure Reasons
68
+
69
+ The `reason` property in a `GuardState` can be a string or a `GuardReason` object. To render it safely in JSX, use the `formatReason` helper:
70
+
71
+ ```tsx
72
+ import { formatReason } from "@pulse-js/react";
73
+
74
+ <p className="error">{formatReason(reason)}</p>;
75
+ ```
76
+
67
77
  ## Developer Tools
68
78
 
69
79
  Pulse provides a dedicated inspector for debugging your reactive graph. In React applications, you can enable it with zero configuration.
package/dist/index.cjs CHANGED
@@ -20,27 +20,66 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ formatReason: () => formatReason,
24
+ useGuard: () => useGuard,
23
25
  usePulse: () => usePulse
24
26
  });
25
27
  module.exports = __toCommonJS(index_exports);
26
28
  var import_react = require("react");
29
+ var isServer = typeof window === "undefined";
30
+ var promiseCache = /* @__PURE__ */ new WeakMap();
31
+ function getGuardPromise(g) {
32
+ if (promiseCache.has(g)) return promiseCache.get(g);
33
+ const promise = new Promise((resolve) => {
34
+ const check = () => {
35
+ const state = g.state();
36
+ if (state.status !== "pending") {
37
+ unsub();
38
+ resolve(state);
39
+ }
40
+ };
41
+ const unsub = g.subscribe(check);
42
+ check();
43
+ });
44
+ promiseCache.set(g, promise);
45
+ return promise;
46
+ }
27
47
  function usePulse(unit) {
28
- const isGuard = "state" in unit;
48
+ const isGuard = unit !== null && typeof unit === "function" && "state" in unit;
49
+ if (isServer) {
50
+ return isGuard ? unit.state() : unit();
51
+ }
29
52
  if (isGuard) {
30
53
  const g = unit;
31
54
  return (0, import_react.useSyncExternalStore)(
32
55
  g.subscribe,
33
- g.state
56
+ () => g.state(),
57
+ () => g.state()
34
58
  );
35
59
  } else {
36
60
  const s = unit;
37
61
  return (0, import_react.useSyncExternalStore)(
38
62
  s.subscribe,
39
- s
63
+ () => s(),
64
+ () => s()
40
65
  );
41
66
  }
42
67
  }
68
+ function useGuard(guard, options) {
69
+ const state = usePulse(guard);
70
+ if (options?.suspend && state.status === "pending") {
71
+ throw getGuardPromise(guard);
72
+ }
73
+ return state;
74
+ }
75
+ function formatReason(reason) {
76
+ if (!reason) return "";
77
+ if (typeof reason === "string") return reason;
78
+ return reason.toString();
79
+ }
43
80
  // Annotate the CommonJS export names for ESM import in node:
44
81
  0 && (module.exports = {
82
+ formatReason,
83
+ useGuard,
45
84
  usePulse
46
85
  });
package/dist/index.d.cts CHANGED
@@ -26,5 +26,39 @@ import { Source, Guard, GuardState } from '@pulse-js/core';
26
26
  */
27
27
  declare function usePulse<T>(unit: Source<T>): T;
28
28
  declare function usePulse<T>(unit: Guard<T>): GuardState<T>;
29
+ /**
30
+ * Options for useGuard hook.
31
+ */
32
+ interface UseGuardOptions {
33
+ /**
34
+ * If true, the hook will throw a promise when the guard is in 'pending' state,
35
+ * triggering the nearest React Suspense boundary.
36
+ */
37
+ suspend?: boolean;
38
+ }
39
+ /**
40
+ * Explicit hook for Pulse Guards.
41
+ * Returns the current GuardState and updates on changes.
42
+ *
43
+ * @example
44
+ * const { ok, value, reason } = useGuard(authGuard, { suspend: true });
45
+ */
46
+ declare function useGuard<T>(guard: Guard<T>, options?: UseGuardOptions): GuardState<T>;
47
+ /**
48
+ * Formats a guard reason for display in React components.
49
+ * Handles both string reasons and GuardReason objects.
50
+ *
51
+ * @param reason - The reason from a guard state
52
+ * @returns A string suitable for rendering in React
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * const { status, reason } = usePulse(myGuard);
57
+ * return <p>Error: {formatReason(reason)}</p>;
58
+ * ```
59
+ */
60
+ declare function formatReason(reason: string | {
61
+ toString(): string;
62
+ } | undefined): string;
29
63
 
30
- export { usePulse };
64
+ export { type UseGuardOptions, formatReason, useGuard, usePulse };
package/dist/index.d.ts CHANGED
@@ -26,5 +26,39 @@ import { Source, Guard, GuardState } from '@pulse-js/core';
26
26
  */
27
27
  declare function usePulse<T>(unit: Source<T>): T;
28
28
  declare function usePulse<T>(unit: Guard<T>): GuardState<T>;
29
+ /**
30
+ * Options for useGuard hook.
31
+ */
32
+ interface UseGuardOptions {
33
+ /**
34
+ * If true, the hook will throw a promise when the guard is in 'pending' state,
35
+ * triggering the nearest React Suspense boundary.
36
+ */
37
+ suspend?: boolean;
38
+ }
39
+ /**
40
+ * Explicit hook for Pulse Guards.
41
+ * Returns the current GuardState and updates on changes.
42
+ *
43
+ * @example
44
+ * const { ok, value, reason } = useGuard(authGuard, { suspend: true });
45
+ */
46
+ declare function useGuard<T>(guard: Guard<T>, options?: UseGuardOptions): GuardState<T>;
47
+ /**
48
+ * Formats a guard reason for display in React components.
49
+ * Handles both string reasons and GuardReason objects.
50
+ *
51
+ * @param reason - The reason from a guard state
52
+ * @returns A string suitable for rendering in React
53
+ *
54
+ * @example
55
+ * ```tsx
56
+ * const { status, reason } = usePulse(myGuard);
57
+ * return <p>Error: {formatReason(reason)}</p>;
58
+ * ```
59
+ */
60
+ declare function formatReason(reason: string | {
61
+ toString(): string;
62
+ } | undefined): string;
29
63
 
30
- export { usePulse };
64
+ export { type UseGuardOptions, formatReason, useGuard, usePulse };
package/dist/index.js CHANGED
@@ -1,21 +1,58 @@
1
1
  // src/index.ts
2
2
  import { useSyncExternalStore } from "react";
3
+ var isServer = typeof window === "undefined";
4
+ var promiseCache = /* @__PURE__ */ new WeakMap();
5
+ function getGuardPromise(g) {
6
+ if (promiseCache.has(g)) return promiseCache.get(g);
7
+ const promise = new Promise((resolve) => {
8
+ const check = () => {
9
+ const state = g.state();
10
+ if (state.status !== "pending") {
11
+ unsub();
12
+ resolve(state);
13
+ }
14
+ };
15
+ const unsub = g.subscribe(check);
16
+ check();
17
+ });
18
+ promiseCache.set(g, promise);
19
+ return promise;
20
+ }
3
21
  function usePulse(unit) {
4
- const isGuard = "state" in unit;
22
+ const isGuard = unit !== null && typeof unit === "function" && "state" in unit;
23
+ if (isServer) {
24
+ return isGuard ? unit.state() : unit();
25
+ }
5
26
  if (isGuard) {
6
27
  const g = unit;
7
28
  return useSyncExternalStore(
8
29
  g.subscribe,
9
- g.state
30
+ () => g.state(),
31
+ () => g.state()
10
32
  );
11
33
  } else {
12
34
  const s = unit;
13
35
  return useSyncExternalStore(
14
36
  s.subscribe,
15
- s
37
+ () => s(),
38
+ () => s()
16
39
  );
17
40
  }
18
41
  }
42
+ function useGuard(guard, options) {
43
+ const state = usePulse(guard);
44
+ if (options?.suspend && state.status === "pending") {
45
+ throw getGuardPromise(guard);
46
+ }
47
+ return state;
48
+ }
49
+ function formatReason(reason) {
50
+ if (!reason) return "";
51
+ if (typeof reason === "string") return reason;
52
+ return reason.toString();
53
+ }
19
54
  export {
55
+ formatReason,
56
+ useGuard,
20
57
  usePulse
21
58
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-js/react",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",
@@ -28,8 +28,8 @@
28
28
  "react": "^19.2.3"
29
29
  },
30
30
  "dependencies": {
31
- "@pulse-js/core": "^0.1.6",
32
- "@pulse-js/tools": "^0.1.3"
31
+ "@pulse-js/core": "^0.1.8",
32
+ "@pulse-js/tools": "^0.1.5"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/bun": "latest",