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