@mythicalos/react-ui 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/dist/hooks.js +41 -22
  2. package/package.json +24 -20
package/dist/hooks.js CHANGED
@@ -63,10 +63,22 @@ export function usePoll(fn, ms, opts = {}) {
63
63
  let timer;
64
64
  let stopped = false;
65
65
  let failures = 0; // consecutive failed reads — drives the ×2 backoff ladder (reset on success)
66
+ // true from the moment a tick fires its fetch until that fetch settles — the ONE in-flight
67
+ // read this call site is allowed to have. The visibility handler below must never start a
68
+ // second tick while this is true; it lets the in-flight tick's own onSettled → schedule()
69
+ // continue the single chain instead (a prior version fired an immediate tick on visibility
70
+ // return unconditionally, which could open a second concurrent chain).
71
+ let pending = false;
66
72
  const isHidden = () => typeof document !== "undefined" && document.hidden === true;
67
73
  const schedule = () => {
68
74
  if (!alive || stopped)
69
75
  return;
76
+ // Clear any already-armed timer before arming a new one — defense in depth so a stray
77
+ // double-settle can never leave an earlier timer orphaned (never cleared, still firing).
78
+ if (timer) {
79
+ clearTimeout(timer);
80
+ timer = undefined;
81
+ }
70
82
  // U7 pause: while the document is hidden no timer is armed — the
71
83
  // visibilitychange handler below refreshes immediately on return.
72
84
  if (isHidden())
@@ -76,26 +88,31 @@ export function usePoll(fn, ms, opts = {}) {
76
88
  // diff r3-F1: every response routes through runPollTick — stamped with the epoch at fire
77
89
  // time, applied only while that epoch is still live. A stale settle leaves loading alone
78
90
  // (the reset already set it for the new epoch) but still rearms via schedule()'s own guards.
79
- const tick = () => runPollTick({
80
- fetch: () => fnRef.current(),
81
- guard,
82
- isAlive: () => alive && !stopped,
83
- isHidden,
84
- onSuccess: (v) => {
85
- failures = 0;
86
- setData(v);
87
- setError(undefined);
88
- },
89
- onFailure: (message) => {
90
- failures += 1;
91
- setError(message);
92
- },
93
- onSettled: (applied) => {
94
- if (applied)
95
- setLoading(false);
96
- schedule();
97
- },
98
- });
91
+ const tick = () => {
92
+ pending = true;
93
+ return runPollTick({
94
+ fetch: () => fnRef.current(),
95
+ guard,
96
+ isAlive: () => alive && !stopped,
97
+ isHidden,
98
+ onSuccess: (v) => {
99
+ failures = 0;
100
+ setData(v);
101
+ setError(undefined);
102
+ },
103
+ onFailure: (message) => {
104
+ failures += 1;
105
+ setError(message);
106
+ },
107
+ onSettled: (applied) => {
108
+ if (applied)
109
+ setLoading(false);
110
+ schedule();
111
+ },
112
+ }).finally(() => {
113
+ pending = false;
114
+ });
115
+ };
99
116
  const onVisibility = () => {
100
117
  if (!alive || stopped)
101
118
  return;
@@ -103,8 +120,10 @@ export function usePoll(fn, ms, opts = {}) {
103
120
  clearTimeout(timer);
104
121
  timer = undefined;
105
122
  }
106
- if (!isHidden())
107
- void tick(); // immediate refresh on visibility return
123
+ // Immediate refresh on visibility return — but only when idle. A tick already in flight
124
+ // keeps its place as the single chain: its own onSettled → schedule() picks up from here.
125
+ if (!isHidden() && !pending)
126
+ void tick();
108
127
  };
109
128
  if (typeof document !== "undefined")
110
129
  document.addEventListener("visibilitychange", onVisibility);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mythicalos/react-ui",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "mythicalOS thin React bindings over @mythicalos/ui-core — Button, Input/Toggle/Checkbox, MaskedSecretInput, EmptyState, ConfirmDialog/Scrim, Toast/ToastProvider, Chip, Card, Avatar, StatusLine, SearchInput, Banner, Gauge, and the usePoll/useInterval hooks. Render + framework wiring only — every class string, poll-scheduling math, glyph map, and text composition is derived by @mythicalos/ui-core so this binding and its Preact sibling can never drift. Apache-2.0.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,7 +20,11 @@
20
20
  "default": "./dist/index.js"
21
21
  }
22
22
  },
23
- "files": ["dist", "README.md", "NOTICE"],
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "NOTICE"
27
+ ],
24
28
  "peerDependencies": {
25
29
  "react": "^18.2.0 || ^19",
26
30
  "react-dom": "^18.2.0 || ^19",
@@ -28,23 +32,23 @@
28
32
  },
29
33
  "peerDependenciesMeta": {
30
34
  "@mythicalos/tokens": {
31
- "optional": true
35
+ "optional": true
36
+ }
37
+ },
38
+ "dependencies": {
39
+ "@mythicalos/ui-core": "^0.1.1"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.build.json",
43
+ "typecheck": "tsc --noEmit",
44
+ "test": "bun test",
45
+ "prepublishOnly": "bun run build && bun test && bun run typecheck"
46
+ },
47
+ "devDependencies": {
48
+ "react": "^19.2.8",
49
+ "react-dom": "^19.2.8",
50
+ "@types/react": "^19",
51
+ "@types/react-dom": "^19",
52
+ "typescript": "^5"
32
53
  }
33
- },
34
- "dependencies": {
35
- "@mythicalos/ui-core": "^0.1.0"
36
- },
37
- "scripts": {
38
- "build": "tsc -p tsconfig.build.json",
39
- "typecheck": "tsc --noEmit",
40
- "test": "bun test",
41
- "prepublishOnly": "bun run build && bun test && bun run typecheck"
42
- },
43
- "devDependencies": {
44
- "react": "^19.2.8",
45
- "react-dom": "^19.2.8",
46
- "@types/react": "^19",
47
- "@types/react-dom": "^19",
48
- "typescript": "^5"
49
- }
50
54
  }