@ai-matrx/kit 0.5.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 +132 -0
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/autosave.cjs +92 -0
- package/dist/autosave.cjs.map +1 -0
- package/dist/autosave.d.cts +17 -0
- package/dist/autosave.d.ts +17 -0
- package/dist/autosave.js +72 -0
- package/dist/autosave.js.map +1 -0
- package/dist/clipboard.cjs +152 -0
- package/dist/clipboard.cjs.map +1 -0
- package/dist/clipboard.d.cts +17 -0
- package/dist/clipboard.d.ts +17 -0
- package/dist/clipboard.js +132 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/color-util.cjs +790 -0
- package/dist/color-util.cjs.map +1 -0
- package/dist/color-util.d.cts +235 -0
- package/dist/color-util.d.ts +235 -0
- package/dist/color-util.js +767 -0
- package/dist/color-util.js.map +1 -0
- package/dist/concurrency.cjs +59 -0
- package/dist/concurrency.cjs.map +1 -0
- package/dist/concurrency.d.cts +19 -0
- package/dist/concurrency.d.ts +19 -0
- package/dist/concurrency.js +38 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/confirm.cjs +344 -0
- package/dist/confirm.cjs.map +1 -0
- package/dist/confirm.d.cts +132 -0
- package/dist/confirm.d.ts +132 -0
- package/dist/confirm.js +312 -0
- package/dist/confirm.js.map +1 -0
- package/dist/delimiter-guard.cjs +172 -0
- package/dist/delimiter-guard.cjs.map +1 -0
- package/dist/delimiter-guard.d.cts +119 -0
- package/dist/delimiter-guard.d.ts +119 -0
- package/dist/delimiter-guard.js +151 -0
- package/dist/delimiter-guard.js.map +1 -0
- package/dist/drafts.cjs +223 -0
- package/dist/drafts.cjs.map +1 -0
- package/dist/drafts.d.cts +62 -0
- package/dist/drafts.d.ts +62 -0
- package/dist/drafts.js +201 -0
- package/dist/drafts.js.map +1 -0
- package/dist/idb-store.cjs +178 -0
- package/dist/idb-store.cjs.map +1 -0
- package/dist/idb-store.d.cts +111 -0
- package/dist/idb-store.d.ts +111 -0
- package/dist/idb-store.js +155 -0
- package/dist/idb-store.js.map +1 -0
- package/dist/idle-scheduler.cjs +268 -0
- package/dist/idle-scheduler.cjs.map +1 -0
- package/dist/idle-scheduler.d.cts +185 -0
- package/dist/idle-scheduler.d.ts +185 -0
- package/dist/idle-scheduler.js +246 -0
- package/dist/idle-scheduler.js.map +1 -0
- package/dist/index.cjs +3113 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +897 -0
- package/dist/index.d.ts +897 -0
- package/dist/index.js +3088 -0
- package/dist/index.js.map +1 -0
- package/dist/invalidation.cjs +64 -0
- package/dist/invalidation.cjs.map +1 -0
- package/dist/invalidation.d.cts +52 -0
- package/dist/invalidation.d.ts +52 -0
- package/dist/invalidation.js +43 -0
- package/dist/invalidation.js.map +1 -0
- package/dist/json-format.cjs +319 -0
- package/dist/json-format.cjs.map +1 -0
- package/dist/json-format.d.cts +148 -0
- package/dist/json-format.d.ts +148 -0
- package/dist/json-format.js +286 -0
- package/dist/json-format.js.map +1 -0
- package/dist/latest-request.cjs +36 -0
- package/dist/latest-request.cjs.map +1 -0
- package/dist/latest-request.d.cts +29 -0
- package/dist/latest-request.d.ts +29 -0
- package/dist/latest-request.js +16 -0
- package/dist/latest-request.js.map +1 -0
- package/dist/qr.cjs +109 -0
- package/dist/qr.cjs.map +1 -0
- package/dist/qr.d.cts +47 -0
- package/dist/qr.d.ts +47 -0
- package/dist/qr.js +78 -0
- package/dist/qr.js.map +1 -0
- package/dist/react-tree.cjs +69 -0
- package/dist/react-tree.cjs.map +1 -0
- package/dist/react-tree.d.cts +38 -0
- package/dist/react-tree.d.ts +38 -0
- package/dist/react-tree.js +38 -0
- package/dist/react-tree.js.map +1 -0
- package/dist/search-scoring.cjs +116 -0
- package/dist/search-scoring.cjs.map +1 -0
- package/dist/search-scoring.d.cts +77 -0
- package/dist/search-scoring.d.ts +77 -0
- package/dist/search-scoring.js +95 -0
- package/dist/search-scoring.js.map +1 -0
- package/dist/text-case.cjs +313 -0
- package/dist/text-case.cjs.map +1 -0
- package/dist/text-case.d.cts +53 -0
- package/dist/text-case.d.ts +53 -0
- package/dist/text-case.js +292 -0
- package/dist/text-case.js.map +1 -0
- package/dist/toast.cjs +74 -0
- package/dist/toast.cjs.map +1 -0
- package/dist/toast.d.cts +92 -0
- package/dist/toast.d.ts +92 -0
- package/dist/toast.js +54 -0
- package/dist/toast.js.map +1 -0
- package/dist/url-state.cjs +212 -0
- package/dist/url-state.cjs.map +1 -0
- package/dist/url-state.d.cts +90 -0
- package/dist/url-state.d.ts +90 -0
- package/dist/url-state.js +199 -0
- package/dist/url-state.js.map +1 -0
- package/package.json +286 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IdleScheduler — a priority-aware deferred execution system for browser apps.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* - Process-wide singleton (NOT React context) — registrations cause zero re-renders
|
|
6
|
+
* - Components register lightweight callbacks with priority 1-5
|
|
7
|
+
* - The scheduler waits for the browser to be truly idle after full page render
|
|
8
|
+
* - Then flushes all registered callbacks in priority order
|
|
9
|
+
*
|
|
10
|
+
* Priority levels:
|
|
11
|
+
* 1 = Highest (first of the "last things") — e.g., analytics init, critical measurements
|
|
12
|
+
* 2 = High — e.g., prefetching next-page data, service worker registration
|
|
13
|
+
* 3 = Normal — e.g., lazy-loading non-critical UI, initializing 3rd party widgets
|
|
14
|
+
* 4 = Low — e.g., telemetry, background sync setup
|
|
15
|
+
* 5 = Lowest (absolute last) — e.g., prewarming caches, speculative prefetch
|
|
16
|
+
*
|
|
17
|
+
* Cross-browser idle detection chain:
|
|
18
|
+
* document.readyState === 'complete'
|
|
19
|
+
* → requestAnimationFrame (past next paint)
|
|
20
|
+
* → scheduler.postTask({ priority: 'background' }) [Chrome/Edge/Firefox 142+]
|
|
21
|
+
* → requestIdleCallback [Chrome/Firefox, NOT Safari]
|
|
22
|
+
* → MessageChannel postMessage [Universal — React's own trick]
|
|
23
|
+
*
|
|
24
|
+
* Ported from matrx-frontend `utils/idle-scheduler/idle-scheduler.ts`, with
|
|
25
|
+
* TWO structural inversions, behavior otherwise verbatim:
|
|
26
|
+
* - Scheduler state (queue, flush state, cleanups, listeners) lives on
|
|
27
|
+
* `globalThis` under `Symbol.for("ai-matrx.kit.idle-scheduler-state")`
|
|
28
|
+
* instead of module-level variables. With the package built
|
|
29
|
+
* `splitting: false` in dual ESM/CJS format this module is duplicated into
|
|
30
|
+
* the root and `./idle-scheduler` bundles, and CJS/ESM each instantiate
|
|
31
|
+
* their own module graph — module-level state would silently split the
|
|
32
|
+
* flush pipeline from some registrants (the confirm-opener hazard). Never
|
|
33
|
+
* "clean this up" into module locals.
|
|
34
|
+
* - The `window.__idleSched()` diagnostics probe installs on first API use
|
|
35
|
+
* instead of at module evaluation, keeping every entry point import-time
|
|
36
|
+
* inert (the package standard). The probe itself is unchanged.
|
|
37
|
+
*/
|
|
38
|
+
type IdlePriority = 1 | 2 | 3 | 4 | 5;
|
|
39
|
+
interface IdleRegistration {
|
|
40
|
+
/** Unique key for deduplication and cancellation */
|
|
41
|
+
key: string;
|
|
42
|
+
/** 1 = highest (first to run), 5 = lowest (last to run) */
|
|
43
|
+
priority: IdlePriority;
|
|
44
|
+
/** The deferred work */
|
|
45
|
+
callback: () => void | Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
type UnregisterFn = () => void;
|
|
48
|
+
type FlushState = "idle" | "waiting" | "flushing" | "done";
|
|
49
|
+
/**
|
|
50
|
+
* Register a callback to run after the page is fully idle.
|
|
51
|
+
*
|
|
52
|
+
* - If the scheduler hasn't flushed yet: queues the callback.
|
|
53
|
+
* - If the scheduler already flushed: runs the callback immediately
|
|
54
|
+
* (through the same idle detection chain, so it still won't block).
|
|
55
|
+
*
|
|
56
|
+
* Returns an unregister function for cleanup.
|
|
57
|
+
*/
|
|
58
|
+
declare function registerIdleTask(key: string, priority: IdlePriority, callback: () => void | Promise<void>): UnregisterFn;
|
|
59
|
+
/**
|
|
60
|
+
* Subscribe to the flush-complete event.
|
|
61
|
+
* Useful for components that just need a "ready" signal without registering work.
|
|
62
|
+
*/
|
|
63
|
+
declare function onFlushComplete(listener: () => void): UnregisterFn;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve after the initial page-load idle flush, or earlier when aborted.
|
|
66
|
+
* Non-React startup services use this instead of recreating the scheduler's
|
|
67
|
+
* load → paint → browser-idle detection chain.
|
|
68
|
+
*
|
|
69
|
+
* @returns false when the caller aborted before idle; true otherwise.
|
|
70
|
+
*/
|
|
71
|
+
declare function whenPageIdle(signal?: AbortSignal): Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* Get current state — useful for debugging or conditional logic.
|
|
74
|
+
*/
|
|
75
|
+
declare function getSchedulerState(): {
|
|
76
|
+
flushState: FlushState;
|
|
77
|
+
pendingCount: number;
|
|
78
|
+
pendingKeys: string[];
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Reset the scheduler — primarily for testing or hot-reload scenarios.
|
|
82
|
+
*/
|
|
83
|
+
declare function resetScheduler(): void;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* React hooks for the IdleScheduler.
|
|
87
|
+
*
|
|
88
|
+
* These are intentionally thin — they just wire up registration/cleanup
|
|
89
|
+
* to React's lifecycle. No state, no context, no re-renders on registration.
|
|
90
|
+
*
|
|
91
|
+
* Three hooks for three use cases:
|
|
92
|
+
*
|
|
93
|
+
* 1. useIdleTask(key, priority, callback)
|
|
94
|
+
* → "Run this callback when idle. I don't need to know when."
|
|
95
|
+
* → Fire-and-forget. Zero re-renders.
|
|
96
|
+
*
|
|
97
|
+
* 2. useIdleReady()
|
|
98
|
+
* → "Just tell me when idle flush is done so I can wake up."
|
|
99
|
+
* → Returns a boolean. One re-render: false → true.
|
|
100
|
+
*
|
|
101
|
+
* 3. useIdleGate(key, priority, callback)
|
|
102
|
+
* → "Run this callback when idle AND tell me when it's done."
|
|
103
|
+
* → Returns { ready: boolean }. Combines both patterns.
|
|
104
|
+
*
|
|
105
|
+
* Plus useIdleRegister() for imperative/conditional registration.
|
|
106
|
+
*
|
|
107
|
+
* Ported verbatim from matrx-frontend `utils/idle-scheduler/hooks.ts`.
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Register a callback to execute after page idle. Zero re-renders.
|
|
112
|
+
*
|
|
113
|
+
* @param key Unique identifier (for deduplication/cancellation)
|
|
114
|
+
* @param priority 1 (first of last) through 5 (absolute last)
|
|
115
|
+
* @param callback The deferred work
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```tsx
|
|
119
|
+
* useIdleTask('analytics-init', 3, () => {
|
|
120
|
+
* initializeAnalytics();
|
|
121
|
+
* });
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
declare function useIdleTask(key: string, priority: IdlePriority, callback: () => void | Promise<void>): void;
|
|
125
|
+
/**
|
|
126
|
+
* Returns `true` once the idle flush has completed.
|
|
127
|
+
* Causes exactly one re-render (false → true). No work is registered.
|
|
128
|
+
*
|
|
129
|
+
* Use this when a component wants to stay dormant (show nothing, or a skeleton)
|
|
130
|
+
* until the page is fully settled, then "turn on."
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```tsx
|
|
134
|
+
* function HeavyWidget() {
|
|
135
|
+
* const ready = useIdleReady();
|
|
136
|
+
* if (!ready) return null; // or a skeleton
|
|
137
|
+
* return <ExpensiveComponent />;
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
declare function useIdleReady(): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Register deferred work and get a `ready` signal when it completes.
|
|
144
|
+
*
|
|
145
|
+
* This is the "full package" — your component stays dormant, the scheduler
|
|
146
|
+
* runs your callback at the right time, and then you get notified to
|
|
147
|
+
* update your UI.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```tsx
|
|
151
|
+
* function PrefetchedSection() {
|
|
152
|
+
* const { ready } = useIdleGate('prefetch-recommendations', 2, async () => {
|
|
153
|
+
* await prefetchRecommendations();
|
|
154
|
+
* });
|
|
155
|
+
*
|
|
156
|
+
* if (!ready) return <Skeleton />;
|
|
157
|
+
* return <Recommendations />;
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
declare function useIdleGate(key: string, priority: IdlePriority, callback: () => void | Promise<void>): {
|
|
162
|
+
ready: boolean;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Returns a `register` function you can call imperatively.
|
|
166
|
+
* Useful when the deferred work depends on runtime conditions.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```tsx
|
|
170
|
+
* function SearchResults({ query }) {
|
|
171
|
+
* const scheduleIdle = useIdleRegister();
|
|
172
|
+
*
|
|
173
|
+
* useEffect(() => {
|
|
174
|
+
* if (query) {
|
|
175
|
+
* scheduleIdle(`prefetch-${query}`, 4, () => {
|
|
176
|
+
* prefetchRelatedResults(query);
|
|
177
|
+
* });
|
|
178
|
+
* }
|
|
179
|
+
* }, [query, scheduleIdle]);
|
|
180
|
+
* }
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
declare function useIdleRegister(): (key: string, priority: IdlePriority, callback: () => void | Promise<void>) => void;
|
|
184
|
+
|
|
185
|
+
export { type FlushState, type IdlePriority, type IdleRegistration, type UnregisterFn, getSchedulerState, onFlushComplete, registerIdleTask, resetScheduler, useIdleGate, useIdleReady, useIdleRegister, useIdleTask, whenPageIdle };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IdleScheduler — a priority-aware deferred execution system for browser apps.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* - Process-wide singleton (NOT React context) — registrations cause zero re-renders
|
|
6
|
+
* - Components register lightweight callbacks with priority 1-5
|
|
7
|
+
* - The scheduler waits for the browser to be truly idle after full page render
|
|
8
|
+
* - Then flushes all registered callbacks in priority order
|
|
9
|
+
*
|
|
10
|
+
* Priority levels:
|
|
11
|
+
* 1 = Highest (first of the "last things") — e.g., analytics init, critical measurements
|
|
12
|
+
* 2 = High — e.g., prefetching next-page data, service worker registration
|
|
13
|
+
* 3 = Normal — e.g., lazy-loading non-critical UI, initializing 3rd party widgets
|
|
14
|
+
* 4 = Low — e.g., telemetry, background sync setup
|
|
15
|
+
* 5 = Lowest (absolute last) — e.g., prewarming caches, speculative prefetch
|
|
16
|
+
*
|
|
17
|
+
* Cross-browser idle detection chain:
|
|
18
|
+
* document.readyState === 'complete'
|
|
19
|
+
* → requestAnimationFrame (past next paint)
|
|
20
|
+
* → scheduler.postTask({ priority: 'background' }) [Chrome/Edge/Firefox 142+]
|
|
21
|
+
* → requestIdleCallback [Chrome/Firefox, NOT Safari]
|
|
22
|
+
* → MessageChannel postMessage [Universal — React's own trick]
|
|
23
|
+
*
|
|
24
|
+
* Ported from matrx-frontend `utils/idle-scheduler/idle-scheduler.ts`, with
|
|
25
|
+
* TWO structural inversions, behavior otherwise verbatim:
|
|
26
|
+
* - Scheduler state (queue, flush state, cleanups, listeners) lives on
|
|
27
|
+
* `globalThis` under `Symbol.for("ai-matrx.kit.idle-scheduler-state")`
|
|
28
|
+
* instead of module-level variables. With the package built
|
|
29
|
+
* `splitting: false` in dual ESM/CJS format this module is duplicated into
|
|
30
|
+
* the root and `./idle-scheduler` bundles, and CJS/ESM each instantiate
|
|
31
|
+
* their own module graph — module-level state would silently split the
|
|
32
|
+
* flush pipeline from some registrants (the confirm-opener hazard). Never
|
|
33
|
+
* "clean this up" into module locals.
|
|
34
|
+
* - The `window.__idleSched()` diagnostics probe installs on first API use
|
|
35
|
+
* instead of at module evaluation, keeping every entry point import-time
|
|
36
|
+
* inert (the package standard). The probe itself is unchanged.
|
|
37
|
+
*/
|
|
38
|
+
type IdlePriority = 1 | 2 | 3 | 4 | 5;
|
|
39
|
+
interface IdleRegistration {
|
|
40
|
+
/** Unique key for deduplication and cancellation */
|
|
41
|
+
key: string;
|
|
42
|
+
/** 1 = highest (first to run), 5 = lowest (last to run) */
|
|
43
|
+
priority: IdlePriority;
|
|
44
|
+
/** The deferred work */
|
|
45
|
+
callback: () => void | Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
type UnregisterFn = () => void;
|
|
48
|
+
type FlushState = "idle" | "waiting" | "flushing" | "done";
|
|
49
|
+
/**
|
|
50
|
+
* Register a callback to run after the page is fully idle.
|
|
51
|
+
*
|
|
52
|
+
* - If the scheduler hasn't flushed yet: queues the callback.
|
|
53
|
+
* - If the scheduler already flushed: runs the callback immediately
|
|
54
|
+
* (through the same idle detection chain, so it still won't block).
|
|
55
|
+
*
|
|
56
|
+
* Returns an unregister function for cleanup.
|
|
57
|
+
*/
|
|
58
|
+
declare function registerIdleTask(key: string, priority: IdlePriority, callback: () => void | Promise<void>): UnregisterFn;
|
|
59
|
+
/**
|
|
60
|
+
* Subscribe to the flush-complete event.
|
|
61
|
+
* Useful for components that just need a "ready" signal without registering work.
|
|
62
|
+
*/
|
|
63
|
+
declare function onFlushComplete(listener: () => void): UnregisterFn;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve after the initial page-load idle flush, or earlier when aborted.
|
|
66
|
+
* Non-React startup services use this instead of recreating the scheduler's
|
|
67
|
+
* load → paint → browser-idle detection chain.
|
|
68
|
+
*
|
|
69
|
+
* @returns false when the caller aborted before idle; true otherwise.
|
|
70
|
+
*/
|
|
71
|
+
declare function whenPageIdle(signal?: AbortSignal): Promise<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* Get current state — useful for debugging or conditional logic.
|
|
74
|
+
*/
|
|
75
|
+
declare function getSchedulerState(): {
|
|
76
|
+
flushState: FlushState;
|
|
77
|
+
pendingCount: number;
|
|
78
|
+
pendingKeys: string[];
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Reset the scheduler — primarily for testing or hot-reload scenarios.
|
|
82
|
+
*/
|
|
83
|
+
declare function resetScheduler(): void;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* React hooks for the IdleScheduler.
|
|
87
|
+
*
|
|
88
|
+
* These are intentionally thin — they just wire up registration/cleanup
|
|
89
|
+
* to React's lifecycle. No state, no context, no re-renders on registration.
|
|
90
|
+
*
|
|
91
|
+
* Three hooks for three use cases:
|
|
92
|
+
*
|
|
93
|
+
* 1. useIdleTask(key, priority, callback)
|
|
94
|
+
* → "Run this callback when idle. I don't need to know when."
|
|
95
|
+
* → Fire-and-forget. Zero re-renders.
|
|
96
|
+
*
|
|
97
|
+
* 2. useIdleReady()
|
|
98
|
+
* → "Just tell me when idle flush is done so I can wake up."
|
|
99
|
+
* → Returns a boolean. One re-render: false → true.
|
|
100
|
+
*
|
|
101
|
+
* 3. useIdleGate(key, priority, callback)
|
|
102
|
+
* → "Run this callback when idle AND tell me when it's done."
|
|
103
|
+
* → Returns { ready: boolean }. Combines both patterns.
|
|
104
|
+
*
|
|
105
|
+
* Plus useIdleRegister() for imperative/conditional registration.
|
|
106
|
+
*
|
|
107
|
+
* Ported verbatim from matrx-frontend `utils/idle-scheduler/hooks.ts`.
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Register a callback to execute after page idle. Zero re-renders.
|
|
112
|
+
*
|
|
113
|
+
* @param key Unique identifier (for deduplication/cancellation)
|
|
114
|
+
* @param priority 1 (first of last) through 5 (absolute last)
|
|
115
|
+
* @param callback The deferred work
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```tsx
|
|
119
|
+
* useIdleTask('analytics-init', 3, () => {
|
|
120
|
+
* initializeAnalytics();
|
|
121
|
+
* });
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
declare function useIdleTask(key: string, priority: IdlePriority, callback: () => void | Promise<void>): void;
|
|
125
|
+
/**
|
|
126
|
+
* Returns `true` once the idle flush has completed.
|
|
127
|
+
* Causes exactly one re-render (false → true). No work is registered.
|
|
128
|
+
*
|
|
129
|
+
* Use this when a component wants to stay dormant (show nothing, or a skeleton)
|
|
130
|
+
* until the page is fully settled, then "turn on."
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```tsx
|
|
134
|
+
* function HeavyWidget() {
|
|
135
|
+
* const ready = useIdleReady();
|
|
136
|
+
* if (!ready) return null; // or a skeleton
|
|
137
|
+
* return <ExpensiveComponent />;
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
declare function useIdleReady(): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Register deferred work and get a `ready` signal when it completes.
|
|
144
|
+
*
|
|
145
|
+
* This is the "full package" — your component stays dormant, the scheduler
|
|
146
|
+
* runs your callback at the right time, and then you get notified to
|
|
147
|
+
* update your UI.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```tsx
|
|
151
|
+
* function PrefetchedSection() {
|
|
152
|
+
* const { ready } = useIdleGate('prefetch-recommendations', 2, async () => {
|
|
153
|
+
* await prefetchRecommendations();
|
|
154
|
+
* });
|
|
155
|
+
*
|
|
156
|
+
* if (!ready) return <Skeleton />;
|
|
157
|
+
* return <Recommendations />;
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
declare function useIdleGate(key: string, priority: IdlePriority, callback: () => void | Promise<void>): {
|
|
162
|
+
ready: boolean;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Returns a `register` function you can call imperatively.
|
|
166
|
+
* Useful when the deferred work depends on runtime conditions.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* ```tsx
|
|
170
|
+
* function SearchResults({ query }) {
|
|
171
|
+
* const scheduleIdle = useIdleRegister();
|
|
172
|
+
*
|
|
173
|
+
* useEffect(() => {
|
|
174
|
+
* if (query) {
|
|
175
|
+
* scheduleIdle(`prefetch-${query}`, 4, () => {
|
|
176
|
+
* prefetchRelatedResults(query);
|
|
177
|
+
* });
|
|
178
|
+
* }
|
|
179
|
+
* }, [query, scheduleIdle]);
|
|
180
|
+
* }
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
declare function useIdleRegister(): (key: string, priority: IdlePriority, callback: () => void | Promise<void>) => void;
|
|
184
|
+
|
|
185
|
+
export { type FlushState, type IdlePriority, type IdleRegistration, type UnregisterFn, getSchedulerState, onFlushComplete, registerIdleTask, resetScheduler, useIdleGate, useIdleReady, useIdleRegister, useIdleTask, whenPageIdle };
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/idle-scheduler/scheduler.ts
|
|
4
|
+
var STATE_SLOT = /* @__PURE__ */ Symbol.for("ai-matrx.kit.idle-scheduler-state");
|
|
5
|
+
function getState() {
|
|
6
|
+
const holder = globalThis;
|
|
7
|
+
let state = holder[STATE_SLOT];
|
|
8
|
+
if (!state) {
|
|
9
|
+
state = {
|
|
10
|
+
queue: /* @__PURE__ */ new Map(),
|
|
11
|
+
flushState: "idle",
|
|
12
|
+
cleanupFns: [],
|
|
13
|
+
flushListeners: /* @__PURE__ */ new Set()
|
|
14
|
+
};
|
|
15
|
+
holder[STATE_SLOT] = state;
|
|
16
|
+
if (typeof window !== "undefined") {
|
|
17
|
+
window.__idleSched = getSchedulerState;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return state;
|
|
21
|
+
}
|
|
22
|
+
function registerIdleTask(key, priority, callback) {
|
|
23
|
+
const state = getState();
|
|
24
|
+
if (state.flushState === "done") {
|
|
25
|
+
scheduleImmediate(callback);
|
|
26
|
+
return () => {
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
state.queue.set(key, { key, priority, callback });
|
|
30
|
+
if (state.flushState === "idle") {
|
|
31
|
+
startFlushPipeline(state);
|
|
32
|
+
}
|
|
33
|
+
return () => {
|
|
34
|
+
state.queue.delete(key);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function onFlushComplete(listener) {
|
|
38
|
+
const state = getState();
|
|
39
|
+
if (state.flushState === "done") {
|
|
40
|
+
queueMicrotask(listener);
|
|
41
|
+
return () => {
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
state.flushListeners.add(listener);
|
|
45
|
+
if (state.flushState === "idle") {
|
|
46
|
+
startFlushPipeline(state);
|
|
47
|
+
}
|
|
48
|
+
return () => {
|
|
49
|
+
state.flushListeners.delete(listener);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function whenPageIdle(signal) {
|
|
53
|
+
if (signal?.aborted) return Promise.resolve(false);
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let settled = false;
|
|
56
|
+
let unsubscribe = () => {
|
|
57
|
+
};
|
|
58
|
+
const finish = (ready) => {
|
|
59
|
+
if (settled) return;
|
|
60
|
+
settled = true;
|
|
61
|
+
unsubscribe();
|
|
62
|
+
signal?.removeEventListener("abort", onAbort);
|
|
63
|
+
resolve(ready);
|
|
64
|
+
};
|
|
65
|
+
const onAbort = () => finish(false);
|
|
66
|
+
unsubscribe = onFlushComplete(() => finish(true));
|
|
67
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function getSchedulerState() {
|
|
71
|
+
const state = getState();
|
|
72
|
+
return {
|
|
73
|
+
flushState: state.flushState,
|
|
74
|
+
pendingCount: state.queue.size,
|
|
75
|
+
pendingKeys: Array.from(state.queue.keys())
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function resetScheduler() {
|
|
79
|
+
const state = getState();
|
|
80
|
+
state.cleanupFns.forEach((fn) => fn());
|
|
81
|
+
state.cleanupFns = [];
|
|
82
|
+
state.queue.clear();
|
|
83
|
+
state.flushListeners.clear();
|
|
84
|
+
state.flushState = "idle";
|
|
85
|
+
}
|
|
86
|
+
function startFlushPipeline(state) {
|
|
87
|
+
if (typeof window === "undefined") return;
|
|
88
|
+
state.flushState = "waiting";
|
|
89
|
+
const waitForLoad = () => {
|
|
90
|
+
if (document.readyState === "complete") {
|
|
91
|
+
waitForPaint();
|
|
92
|
+
} else {
|
|
93
|
+
const onLoad = () => waitForPaint();
|
|
94
|
+
window.addEventListener("load", onLoad, { once: true });
|
|
95
|
+
state.cleanupFns.push(() => window.removeEventListener("load", onLoad));
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const waitForPaint = () => {
|
|
99
|
+
let advanced = false;
|
|
100
|
+
const advance = () => {
|
|
101
|
+
if (advanced) return;
|
|
102
|
+
advanced = true;
|
|
103
|
+
waitForIdle();
|
|
104
|
+
};
|
|
105
|
+
const rafId = requestAnimationFrame(advance);
|
|
106
|
+
const timeoutId = setTimeout(
|
|
107
|
+
advance,
|
|
108
|
+
document.visibilityState === "hidden" ? 250 : 1500
|
|
109
|
+
);
|
|
110
|
+
state.cleanupFns.push(() => {
|
|
111
|
+
cancelAnimationFrame(rafId);
|
|
112
|
+
clearTimeout(timeoutId);
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
const waitForIdle = () => {
|
|
116
|
+
const schedulerGlobal = globalThis.scheduler;
|
|
117
|
+
if (schedulerGlobal && "postTask" in schedulerGlobal) {
|
|
118
|
+
schedulerGlobal.postTask(() => void flush(state), { priority: "background" }).catch(() => {
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if ("requestIdleCallback" in window) {
|
|
123
|
+
const idleId = requestIdleCallback(() => void flush(state));
|
|
124
|
+
state.cleanupFns.push(() => cancelIdleCallback(idleId));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const channel = new MessageChannel();
|
|
128
|
+
channel.port1.onmessage = () => void flush(state);
|
|
129
|
+
channel.port2.postMessage(void 0);
|
|
130
|
+
};
|
|
131
|
+
waitForLoad();
|
|
132
|
+
}
|
|
133
|
+
async function flush(state) {
|
|
134
|
+
if (state.flushState === "done" || state.flushState === "flushing") return;
|
|
135
|
+
state.flushState = "flushing";
|
|
136
|
+
const sorted = Array.from(state.queue.values()).sort(
|
|
137
|
+
(a, b) => a.priority - b.priority
|
|
138
|
+
);
|
|
139
|
+
state.queue.clear();
|
|
140
|
+
for (const task of sorted) {
|
|
141
|
+
try {
|
|
142
|
+
await task.callback();
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error(`[IdleScheduler] Task "${task.key}" failed:`, err);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
state.flushState = "done";
|
|
148
|
+
if (state.queue.size > 0) {
|
|
149
|
+
const late = Array.from(state.queue.values()).sort(
|
|
150
|
+
(a, b) => a.priority - b.priority
|
|
151
|
+
);
|
|
152
|
+
state.queue.clear();
|
|
153
|
+
for (const task of late) scheduleImmediate(task.callback);
|
|
154
|
+
}
|
|
155
|
+
state.flushListeners.forEach((listener) => {
|
|
156
|
+
try {
|
|
157
|
+
listener();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error("[IdleScheduler] Flush listener failed:", err);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
state.flushListeners.clear();
|
|
163
|
+
}
|
|
164
|
+
function scheduleImmediate(callback) {
|
|
165
|
+
requestAnimationFrame(() => {
|
|
166
|
+
const schedulerGlobal = globalThis.scheduler;
|
|
167
|
+
if (schedulerGlobal && "postTask" in schedulerGlobal) {
|
|
168
|
+
schedulerGlobal.postTask(() => void callback(), { priority: "background" }).catch(() => {
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if ("requestIdleCallback" in window) {
|
|
173
|
+
requestIdleCallback(() => void callback());
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const channel = new MessageChannel();
|
|
177
|
+
channel.port1.onmessage = () => void callback();
|
|
178
|
+
channel.port2.postMessage(void 0);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/idle-scheduler/hooks.ts
|
|
183
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
184
|
+
function useIdleTask(key, priority, callback) {
|
|
185
|
+
const callbackRef = useRef(callback);
|
|
186
|
+
callbackRef.current = callback;
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
const unregister = registerIdleTask(key, priority, () => {
|
|
189
|
+
return callbackRef.current();
|
|
190
|
+
});
|
|
191
|
+
return unregister;
|
|
192
|
+
}, [key, priority]);
|
|
193
|
+
}
|
|
194
|
+
function useIdleReady() {
|
|
195
|
+
const [ready, setReady] = useState(false);
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
const unsubscribe = onFlushComplete(() => {
|
|
198
|
+
setReady(true);
|
|
199
|
+
});
|
|
200
|
+
return unsubscribe;
|
|
201
|
+
}, []);
|
|
202
|
+
return ready;
|
|
203
|
+
}
|
|
204
|
+
function useIdleGate(key, priority, callback) {
|
|
205
|
+
const [ready, setReady] = useState(false);
|
|
206
|
+
const callbackRef = useRef(callback);
|
|
207
|
+
callbackRef.current = callback;
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const unregister = registerIdleTask(key, priority, async () => {
|
|
210
|
+
await callbackRef.current();
|
|
211
|
+
setReady(true);
|
|
212
|
+
});
|
|
213
|
+
return unregister;
|
|
214
|
+
}, [key, priority]);
|
|
215
|
+
return { ready };
|
|
216
|
+
}
|
|
217
|
+
function useIdleRegister() {
|
|
218
|
+
const unregisterRefs = useRef(/* @__PURE__ */ new Map());
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
const refs = unregisterRefs.current;
|
|
221
|
+
return () => {
|
|
222
|
+
refs.forEach((unregister) => unregister());
|
|
223
|
+
refs.clear();
|
|
224
|
+
};
|
|
225
|
+
}, []);
|
|
226
|
+
return useCallback(
|
|
227
|
+
(key, priority, callback) => {
|
|
228
|
+
unregisterRefs.current.get(key)?.();
|
|
229
|
+
const unregister = registerIdleTask(key, priority, callback);
|
|
230
|
+
unregisterRefs.current.set(key, unregister);
|
|
231
|
+
},
|
|
232
|
+
[]
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
export {
|
|
236
|
+
getSchedulerState,
|
|
237
|
+
onFlushComplete,
|
|
238
|
+
registerIdleTask,
|
|
239
|
+
resetScheduler,
|
|
240
|
+
useIdleGate,
|
|
241
|
+
useIdleReady,
|
|
242
|
+
useIdleRegister,
|
|
243
|
+
useIdleTask,
|
|
244
|
+
whenPageIdle
|
|
245
|
+
};
|
|
246
|
+
//# sourceMappingURL=idle-scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/idle-scheduler/scheduler.ts","../src/idle-scheduler/hooks.ts"],"sourcesContent":["/**\n * IdleScheduler — a priority-aware deferred execution system for browser apps.\n *\n * Architecture:\n * - Process-wide singleton (NOT React context) — registrations cause zero re-renders\n * - Components register lightweight callbacks with priority 1-5\n * - The scheduler waits for the browser to be truly idle after full page render\n * - Then flushes all registered callbacks in priority order\n *\n * Priority levels:\n * 1 = Highest (first of the \"last things\") — e.g., analytics init, critical measurements\n * 2 = High — e.g., prefetching next-page data, service worker registration\n * 3 = Normal — e.g., lazy-loading non-critical UI, initializing 3rd party widgets\n * 4 = Low — e.g., telemetry, background sync setup\n * 5 = Lowest (absolute last) — e.g., prewarming caches, speculative prefetch\n *\n * Cross-browser idle detection chain:\n * document.readyState === 'complete'\n * → requestAnimationFrame (past next paint)\n * → scheduler.postTask({ priority: 'background' }) [Chrome/Edge/Firefox 142+]\n * → requestIdleCallback [Chrome/Firefox, NOT Safari]\n * → MessageChannel postMessage [Universal — React's own trick]\n *\n * Ported from matrx-frontend `utils/idle-scheduler/idle-scheduler.ts`, with\n * TWO structural inversions, behavior otherwise verbatim:\n * - Scheduler state (queue, flush state, cleanups, listeners) lives on\n * `globalThis` under `Symbol.for(\"ai-matrx.kit.idle-scheduler-state\")`\n * instead of module-level variables. With the package built\n * `splitting: false` in dual ESM/CJS format this module is duplicated into\n * the root and `./idle-scheduler` bundles, and CJS/ESM each instantiate\n * their own module graph — module-level state would silently split the\n * flush pipeline from some registrants (the confirm-opener hazard). Never\n * \"clean this up\" into module locals.\n * - The `window.__idleSched()` diagnostics probe installs on first API use\n * instead of at module evaluation, keeping every entry point import-time\n * inert (the package standard). The probe itself is unchanged.\n */\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type IdlePriority = 1 | 2 | 3 | 4 | 5;\n\nexport interface IdleRegistration {\n /** Unique key for deduplication and cancellation */\n key: string;\n /** 1 = highest (first to run), 5 = lowest (last to run) */\n priority: IdlePriority;\n /** The deferred work */\n callback: () => void | Promise<void>;\n}\n\nexport type UnregisterFn = () => void;\n\nexport type FlushState = \"idle\" | \"waiting\" | \"flushing\" | \"done\";\n\n/** Experimental Scheduler API (Chrome/Edge/Firefox 142+) — not yet in standard lib types. */\ninterface GlobalThisWithScheduler {\n scheduler?: {\n postTask: (callback: () => void, options?: { priority: string }) => Promise<void>;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Singleton state — on globalThis so every bundle graph shares ONE pipeline\n// ---------------------------------------------------------------------------\n\ninterface SchedulerState {\n queue: Map<string, IdleRegistration>;\n flushState: FlushState;\n cleanupFns: Array<() => void>;\n /** Listeners that want to know when flush completes (for useIdleReady) */\n flushListeners: Set<() => void>;\n}\n\nconst STATE_SLOT = Symbol.for(\"ai-matrx.kit.idle-scheduler-state\");\n\nfunction getState(): SchedulerState {\n const holder = globalThis as Record<symbol, SchedulerState | undefined>;\n let state = holder[STATE_SLOT];\n if (!state) {\n state = {\n queue: new Map(),\n flushState: \"idle\",\n cleanupFns: [],\n flushListeners: new Set(),\n };\n holder[STATE_SLOT] = state;\n\n // Diagnostics probe: lets a console / agent read the live scheduler state\n // (`window.__idleSched()`) — the wrapper-never-mounts class of bug is\n // invisible without it.\n if (typeof window !== \"undefined\") {\n (window as unknown as { __idleSched?: () => unknown }).__idleSched =\n getSchedulerState;\n }\n }\n return state;\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Register a callback to run after the page is fully idle.\n *\n * - If the scheduler hasn't flushed yet: queues the callback.\n * - If the scheduler already flushed: runs the callback immediately\n * (through the same idle detection chain, so it still won't block).\n *\n * Returns an unregister function for cleanup.\n */\nexport function registerIdleTask(\n key: string,\n priority: IdlePriority,\n callback: () => void | Promise<void>,\n): UnregisterFn {\n const state = getState();\n\n // If we already flushed, schedule this one immediately (still deferred)\n if (state.flushState === \"done\") {\n scheduleImmediate(callback);\n return () => {};\n }\n\n state.queue.set(key, { key, priority, callback });\n\n // Ensure the flush pipeline is started\n if (state.flushState === \"idle\") {\n startFlushPipeline(state);\n }\n\n return () => {\n state.queue.delete(key);\n };\n}\n\n/**\n * Subscribe to the flush-complete event.\n * Useful for components that just need a \"ready\" signal without registering work.\n */\nexport function onFlushComplete(listener: () => void): UnregisterFn {\n const state = getState();\n\n if (state.flushState === \"done\") {\n // Already flushed — notify immediately (but async to avoid sync side effects)\n queueMicrotask(listener);\n return () => {};\n }\n\n state.flushListeners.add(listener);\n\n // A \"tell me when idle\" subscriber needs the pipeline RUNNING. If only\n // registerIdleTask started it, then on pages where nothing registered an\n // idle task the flush would never run and every ready-gated mount would\n // stay dormant forever. Subscribing must start the pipeline too.\n if (state.flushState === \"idle\") {\n startFlushPipeline(state);\n }\n\n return () => {\n state.flushListeners.delete(listener);\n };\n}\n\n/**\n * Resolve after the initial page-load idle flush, or earlier when aborted.\n * Non-React startup services use this instead of recreating the scheduler's\n * load → paint → browser-idle detection chain.\n *\n * @returns false when the caller aborted before idle; true otherwise.\n */\nexport function whenPageIdle(signal?: AbortSignal): Promise<boolean> {\n if (signal?.aborted) return Promise.resolve(false);\n\n return new Promise((resolve) => {\n let settled = false;\n let unsubscribe: UnregisterFn = () => {};\n\n const finish = (ready: boolean) => {\n if (settled) return;\n settled = true;\n unsubscribe();\n signal?.removeEventListener(\"abort\", onAbort);\n resolve(ready);\n };\n const onAbort = () => finish(false);\n\n unsubscribe = onFlushComplete(() => finish(true));\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/**\n * Get current state — useful for debugging or conditional logic.\n */\nexport function getSchedulerState(): {\n flushState: FlushState;\n pendingCount: number;\n pendingKeys: string[];\n} {\n const state = getState();\n return {\n flushState: state.flushState,\n pendingCount: state.queue.size,\n pendingKeys: Array.from(state.queue.keys()),\n };\n}\n\n/**\n * Reset the scheduler — primarily for testing or hot-reload scenarios.\n */\nexport function resetScheduler(): void {\n const state = getState();\n state.cleanupFns.forEach((fn) => fn());\n state.cleanupFns = [];\n state.queue.clear();\n state.flushListeners.clear();\n state.flushState = \"idle\";\n}\n\n// ---------------------------------------------------------------------------\n// Flush pipeline\n// ---------------------------------------------------------------------------\n\nfunction startFlushPipeline(state: SchedulerState): void {\n if (typeof window === \"undefined\") return; // SSR guard\n\n state.flushState = \"waiting\";\n\n const waitForLoad = () => {\n if (document.readyState === \"complete\") {\n waitForPaint();\n } else {\n const onLoad = () => waitForPaint();\n window.addEventListener(\"load\", onLoad, { once: true });\n state.cleanupFns.push(() => window.removeEventListener(\"load\", onLoad));\n }\n };\n\n const waitForPaint = () => {\n // requestAnimationFrame NEVER fires while the tab is hidden (background\n // tab, restored session, headless browser) — waiting on it alone hangs the\n // whole pipeline forever in that state, so every ready-gated mount stays\n // dormant. Paint alignment is an optimization, not a correctness\n // requirement: race the rAF against a timeout so a hidden tab still\n // flushes.\n let advanced = false;\n const advance = () => {\n if (advanced) return;\n advanced = true;\n waitForIdle();\n };\n const rafId = requestAnimationFrame(advance);\n const timeoutId = setTimeout(\n advance,\n document.visibilityState === \"hidden\" ? 250 : 1_500,\n );\n state.cleanupFns.push(() => {\n cancelAnimationFrame(rafId);\n clearTimeout(timeoutId);\n });\n };\n\n const waitForIdle = () => {\n // Tier 1: scheduler.postTask with background priority (Chrome/Edge/Firefox 142+)\n const schedulerGlobal = (globalThis as GlobalThisWithScheduler).scheduler;\n if (schedulerGlobal && \"postTask\" in schedulerGlobal) {\n schedulerGlobal\n .postTask(() => void flush(state), { priority: \"background\" })\n .catch(() => {});\n return;\n }\n\n // Tier 2: requestIdleCallback (Chrome, Firefox — NOT Safari stable)\n if (\"requestIdleCallback\" in window) {\n const idleId = requestIdleCallback(() => void flush(state));\n state.cleanupFns.push(() => cancelIdleCallback(idleId));\n return;\n }\n\n // Tier 3: MessageChannel — universal, including Safari + iOS Safari\n // This is what React's scheduler uses internally.\n const channel = new MessageChannel();\n channel.port1.onmessage = () => void flush(state);\n channel.port2.postMessage(undefined);\n };\n\n waitForLoad();\n}\n\nasync function flush(state: SchedulerState): Promise<void> {\n if (state.flushState === \"done\" || state.flushState === \"flushing\") return;\n state.flushState = \"flushing\";\n\n // Sort by priority (1 first, 5 last), stable sort preserving insertion order within priority\n const sorted = Array.from(state.queue.values()).sort(\n (a, b) => a.priority - b.priority,\n );\n\n // Clear the queue before executing (so late registrations during flush\n // are treated as \"post-flush\" and get scheduled immediately)\n state.queue.clear();\n\n for (const task of sorted) {\n try {\n await task.callback();\n } catch (err) {\n console.error(`[IdleScheduler] Task \"${task.key}\" failed:`, err);\n }\n }\n\n state.flushState = \"done\";\n\n // DEFECT FIX vs the original (see CHANGELOG 0.4.0): a task registered WHILE\n // the flush loop was running (flushState === \"flushing\") landed back in the\n // queue — which nothing ever drained again, so the task silently never ran.\n // The comment above promises such registrations are \"treated as post-flush\n // and get scheduled immediately\"; make that true.\n if (state.queue.size > 0) {\n const late = Array.from(state.queue.values()).sort(\n (a, b) => a.priority - b.priority,\n );\n state.queue.clear();\n for (const task of late) scheduleImmediate(task.callback);\n }\n\n // Notify all listeners\n state.flushListeners.forEach((listener) => {\n try {\n listener();\n } catch (err) {\n console.error(\"[IdleScheduler] Flush listener failed:\", err);\n }\n });\n state.flushListeners.clear();\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Schedule a single callback through the idle chain (for post-flush registrations) */\nfunction scheduleImmediate(callback: () => void | Promise<void>): void {\n requestAnimationFrame(() => {\n const schedulerGlobal = (globalThis as GlobalThisWithScheduler).scheduler;\n if (schedulerGlobal && \"postTask\" in schedulerGlobal) {\n schedulerGlobal\n .postTask(() => void callback(), { priority: \"background\" })\n .catch(() => {});\n return;\n }\n if (\"requestIdleCallback\" in window) {\n requestIdleCallback(() => void callback());\n return;\n }\n const channel = new MessageChannel();\n channel.port1.onmessage = () => void callback();\n channel.port2.postMessage(undefined);\n });\n}\n","/**\n * React hooks for the IdleScheduler.\n *\n * These are intentionally thin — they just wire up registration/cleanup\n * to React's lifecycle. No state, no context, no re-renders on registration.\n *\n * Three hooks for three use cases:\n *\n * 1. useIdleTask(key, priority, callback)\n * → \"Run this callback when idle. I don't need to know when.\"\n * → Fire-and-forget. Zero re-renders.\n *\n * 2. useIdleReady()\n * → \"Just tell me when idle flush is done so I can wake up.\"\n * → Returns a boolean. One re-render: false → true.\n *\n * 3. useIdleGate(key, priority, callback)\n * → \"Run this callback when idle AND tell me when it's done.\"\n * → Returns { ready: boolean }. Combines both patterns.\n *\n * Plus useIdleRegister() for imperative/conditional registration.\n *\n * Ported verbatim from matrx-frontend `utils/idle-scheduler/hooks.ts`.\n */\n\n\"use client\";\n\nimport { useEffect, useRef, useState, useCallback } from \"react\";\nimport {\n registerIdleTask,\n onFlushComplete,\n type IdlePriority,\n} from \"./scheduler\";\n\n// ---------------------------------------------------------------------------\n// useIdleTask — fire-and-forget deferred work\n// ---------------------------------------------------------------------------\n\n/**\n * Register a callback to execute after page idle. Zero re-renders.\n *\n * @param key Unique identifier (for deduplication/cancellation)\n * @param priority 1 (first of last) through 5 (absolute last)\n * @param callback The deferred work\n *\n * @example\n * ```tsx\n * useIdleTask('analytics-init', 3, () => {\n * initializeAnalytics();\n * });\n * ```\n */\nexport function useIdleTask(\n key: string,\n priority: IdlePriority,\n callback: () => void | Promise<void>,\n): void {\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n const unregister = registerIdleTask(key, priority, () => {\n return callbackRef.current();\n });\n return unregister;\n }, [key, priority]);\n}\n\n// ---------------------------------------------------------------------------\n// useIdleReady — \"am I allowed to wake up yet?\"\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` once the idle flush has completed.\n * Causes exactly one re-render (false → true). No work is registered.\n *\n * Use this when a component wants to stay dormant (show nothing, or a skeleton)\n * until the page is fully settled, then \"turn on.\"\n *\n * @example\n * ```tsx\n * function HeavyWidget() {\n * const ready = useIdleReady();\n * if (!ready) return null; // or a skeleton\n * return <ExpensiveComponent />;\n * }\n * ```\n */\nexport function useIdleReady(): boolean {\n const [ready, setReady] = useState(false);\n\n useEffect(() => {\n const unsubscribe = onFlushComplete(() => {\n setReady(true);\n });\n return unsubscribe;\n }, []);\n\n return ready;\n}\n\n// ---------------------------------------------------------------------------\n// useIdleGate — register work AND get a ready signal\n// ---------------------------------------------------------------------------\n\n/**\n * Register deferred work and get a `ready` signal when it completes.\n *\n * This is the \"full package\" — your component stays dormant, the scheduler\n * runs your callback at the right time, and then you get notified to\n * update your UI.\n *\n * @example\n * ```tsx\n * function PrefetchedSection() {\n * const { ready } = useIdleGate('prefetch-recommendations', 2, async () => {\n * await prefetchRecommendations();\n * });\n *\n * if (!ready) return <Skeleton />;\n * return <Recommendations />;\n * }\n * ```\n */\nexport function useIdleGate(\n key: string,\n priority: IdlePriority,\n callback: () => void | Promise<void>,\n): { ready: boolean } {\n const [ready, setReady] = useState(false);\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n useEffect(() => {\n const unregister = registerIdleTask(key, priority, async () => {\n await callbackRef.current();\n setReady(true);\n });\n return unregister;\n }, [key, priority]);\n\n return { ready };\n}\n\n// ---------------------------------------------------------------------------\n// useIdleRegister — imperative registration (for dynamic/conditional work)\n// ---------------------------------------------------------------------------\n\n/**\n * Returns a `register` function you can call imperatively.\n * Useful when the deferred work depends on runtime conditions.\n *\n * @example\n * ```tsx\n * function SearchResults({ query }) {\n * const scheduleIdle = useIdleRegister();\n *\n * useEffect(() => {\n * if (query) {\n * scheduleIdle(`prefetch-${query}`, 4, () => {\n * prefetchRelatedResults(query);\n * });\n * }\n * }, [query, scheduleIdle]);\n * }\n * ```\n */\nexport function useIdleRegister(): (\n key: string,\n priority: IdlePriority,\n callback: () => void | Promise<void>,\n) => void {\n const unregisterRefs = useRef<Map<string, () => void>>(new Map());\n\n // Cleanup all registrations on unmount\n useEffect(() => {\n const refs = unregisterRefs.current;\n return () => {\n refs.forEach((unregister) => unregister());\n refs.clear();\n };\n }, []);\n\n return useCallback(\n (\n key: string,\n priority: IdlePriority,\n callback: () => void | Promise<void>,\n ) => {\n // Cancel previous registration with same key\n unregisterRefs.current.get(key)?.();\n\n const unregister = registerIdleTask(key, priority, callback);\n unregisterRefs.current.set(key, unregister);\n },\n [],\n );\n}\n"],"mappings":"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","names":[]}
|