rask-ui 0.20.5 → 0.21.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.
Files changed (103) hide show
  1. package/dist/createComputed.d.ts +4 -0
  2. package/dist/createComputed.d.ts.map +1 -0
  3. package/dist/createComputed.js +69 -0
  4. package/dist/createEffect.d.ts +2 -0
  5. package/dist/createEffect.d.ts.map +1 -0
  6. package/dist/createEffect.js +29 -0
  7. package/dist/createRouter.d.ts +8 -0
  8. package/dist/createRouter.d.ts.map +1 -0
  9. package/dist/createRouter.js +27 -0
  10. package/dist/createState.d.ts +2 -0
  11. package/dist/createState.d.ts.map +1 -1
  12. package/dist/createState.js +40 -5
  13. package/dist/createTask.d.ts +31 -0
  14. package/dist/createTask.d.ts.map +1 -0
  15. package/dist/createTask.js +79 -0
  16. package/dist/createView.d.ts +18 -44
  17. package/dist/createView.d.ts.map +1 -1
  18. package/dist/createView.js +57 -48
  19. package/dist/error.d.ts +3 -14
  20. package/dist/error.d.ts.map +1 -1
  21. package/dist/error.js +14 -15
  22. package/dist/jsx.d.ts +10 -256
  23. package/dist/patchInferno.d.ts +6 -0
  24. package/dist/patchInferno.d.ts.map +1 -0
  25. package/dist/patchInferno.js +53 -0
  26. package/dist/scheduler.d.ts +4 -0
  27. package/dist/scheduler.d.ts.map +1 -0
  28. package/dist/scheduler.js +107 -0
  29. package/dist/tests/batch.test.d.ts +2 -0
  30. package/dist/tests/batch.test.d.ts.map +1 -0
  31. package/dist/tests/batch.test.js +244 -0
  32. package/dist/tests/createComputed.test.d.ts +2 -0
  33. package/dist/tests/createComputed.test.d.ts.map +1 -0
  34. package/dist/tests/createComputed.test.js +257 -0
  35. package/dist/tests/createContext.test.d.ts +2 -0
  36. package/dist/tests/createContext.test.d.ts.map +1 -0
  37. package/dist/tests/createContext.test.js +136 -0
  38. package/dist/tests/createEffect.test.d.ts +2 -0
  39. package/dist/tests/createEffect.test.d.ts.map +1 -0
  40. package/dist/tests/createEffect.test.js +467 -0
  41. package/dist/tests/createState.test.d.ts.map +1 -0
  42. package/dist/tests/createState.test.js +144 -0
  43. package/dist/tests/createTask.test.d.ts +2 -0
  44. package/dist/tests/createTask.test.d.ts.map +1 -0
  45. package/dist/tests/createTask.test.js +322 -0
  46. package/dist/tests/createView.test.d.ts.map +1 -0
  47. package/dist/{createView.test.js → tests/createView.test.js} +40 -40
  48. package/dist/tests/error.test.d.ts +2 -0
  49. package/dist/tests/error.test.d.ts.map +1 -0
  50. package/dist/tests/error.test.js +168 -0
  51. package/dist/tests/observation.test.d.ts.map +1 -0
  52. package/dist/tests/observation.test.js +341 -0
  53. package/dist/types.d.ts +2 -1
  54. package/dist/types.d.ts.map +1 -1
  55. package/dist/useComputed.d.ts +5 -0
  56. package/dist/useComputed.d.ts.map +1 -0
  57. package/dist/useComputed.js +69 -0
  58. package/dist/useQuery.d.ts +25 -0
  59. package/dist/useQuery.d.ts.map +1 -0
  60. package/dist/useQuery.js +25 -0
  61. package/dist/useSuspendAsync.d.ts +18 -0
  62. package/dist/useSuspendAsync.d.ts.map +1 -0
  63. package/dist/useSuspendAsync.js +37 -0
  64. package/dist/useTask.d.ts +25 -0
  65. package/dist/useTask.d.ts.map +1 -0
  66. package/dist/useTask.js +70 -0
  67. package/package.json +1 -1
  68. package/swc-plugin/target/wasm32-wasip1/release/swc_plugin_rask_component.wasm +0 -0
  69. package/dist/asyncState.d.ts +0 -16
  70. package/dist/asyncState.d.ts.map +0 -1
  71. package/dist/asyncState.js +0 -24
  72. package/dist/context.d.ts +0 -5
  73. package/dist/context.d.ts.map +0 -1
  74. package/dist/context.js +0 -29
  75. package/dist/createAsync.test.d.ts +0 -2
  76. package/dist/createAsync.test.d.ts.map +0 -1
  77. package/dist/createAsync.test.js +0 -110
  78. package/dist/createMutation.test.d.ts +0 -2
  79. package/dist/createMutation.test.d.ts.map +0 -1
  80. package/dist/createMutation.test.js +0 -168
  81. package/dist/createQuery.test.d.ts +0 -2
  82. package/dist/createQuery.test.d.ts.map +0 -1
  83. package/dist/createQuery.test.js +0 -156
  84. package/dist/createRef.d.ts +0 -6
  85. package/dist/createRef.d.ts.map +0 -1
  86. package/dist/createRef.js +0 -8
  87. package/dist/createState.test.d.ts.map +0 -1
  88. package/dist/createState.test.js +0 -111
  89. package/dist/createView.test.d.ts.map +0 -1
  90. package/dist/observation.test.d.ts.map +0 -1
  91. package/dist/observation.test.js +0 -150
  92. package/dist/suspense.d.ts +0 -25
  93. package/dist/suspense.d.ts.map +0 -1
  94. package/dist/suspense.js +0 -97
  95. package/dist/test-setup.d.ts +0 -16
  96. package/dist/test-setup.d.ts.map +0 -1
  97. package/dist/test-setup.js +0 -40
  98. package/dist/test.d.ts +0 -2
  99. package/dist/test.d.ts.map +0 -1
  100. package/dist/test.js +0 -24
  101. /package/dist/{createState.test.d.ts → tests/createState.test.d.ts} +0 -0
  102. /package/dist/{createView.test.d.ts → tests/createView.test.d.ts} +0 -0
  103. /package/dist/{observation.test.d.ts → tests/observation.test.d.ts} +0 -0
package/dist/jsx.d.ts CHANGED
@@ -1,257 +1,11 @@
1
1
  // JSX type definitions
2
- // Note: This is JSXInternal, which gets renamed to JSX on export from jsx-runtime
3
- import type { Ref } from "./state";
4
-
5
- export namespace JSXInternal {
6
- export type Element = any;
7
-
8
- export interface ElementAttributesProperty {
9
- props: {};
10
- }
11
-
12
- export interface ElementChildrenAttribute {
13
- children: {};
14
- }
15
-
16
- export interface IntrinsicAttributes {
17
- key?: any;
18
- }
19
-
20
- // CSS Properties
21
- export type CSSProperties = {
22
- [key: string]: string | number | undefined;
23
- };
24
-
25
- // Common HTML Attributes
26
- export interface HTMLAttributes<T = HTMLElement> {
27
- ref?: Ref<T> | ((element: T | null) => void);
28
- id?: string;
29
- class?: string | Record<string, boolean>;
30
- style?: string | CSSProperties;
31
- title?: string;
32
- role?: string;
33
- tabIndex?: number;
34
-
35
- onClick?: (event: MouseEvent) => void;
36
- onDblClick?: (event: MouseEvent) => void;
37
- onChange?: (event: Event) => void;
38
- onInput?: (event: Event) => void;
39
- onSubmit?: (event: Event) => void;
40
- onFocus?: (event: FocusEvent) => void;
41
- onBlur?: (event: FocusEvent) => void;
42
- onKeyDown?: (event: KeyboardEvent) => void;
43
- onKeyUp?: (event: KeyboardEvent) => void;
44
- onKeyPress?: (event: KeyboardEvent) => void;
45
- onMouseDown?: (event: MouseEvent) => void;
46
- onMouseUp?: (event: MouseEvent) => void;
47
- onMouseEnter?: (event: MouseEvent) => void;
48
- onMouseLeave?: (event: MouseEvent) => void;
49
- onMouseMove?: (event: MouseEvent) => void;
50
- onMouseOver?: (event: MouseEvent) => void;
51
- onMouseOut?: (event: MouseEvent) => void;
52
- onWheel?: (event: WheelEvent) => void;
53
- onScroll?: (event: Event) => void;
54
- onTouchStart?: (event: TouchEvent) => void;
55
- onTouchEnd?: (event: TouchEvent) => void;
56
- onTouchMove?: (event: TouchEvent) => void;
57
- onTouchCancel?: (event: TouchEvent) => void;
58
-
59
- [key: `aria-${string}`]: string | boolean | number | undefined;
60
- [key: `data-${string}`]: string | boolean | number | undefined;
61
-
62
- children?: any;
63
- }
64
-
65
- export interface AnchorHTMLAttributes<T = HTMLAnchorElement>
66
- extends HTMLAttributes<T> {
67
- href?: string;
68
- target?: "_blank" | "_self" | "_parent" | "_top";
69
- rel?: string;
70
- download?: string;
71
- }
72
-
73
- export interface ButtonHTMLAttributes<T = HTMLButtonElement>
74
- extends HTMLAttributes<T> {
75
- type?: "button" | "submit" | "reset";
76
- disabled?: boolean;
77
- name?: string;
78
- value?: string;
79
- }
80
-
81
- export interface FormHTMLAttributes<T = HTMLFormElement>
82
- extends HTMLAttributes<T> {
83
- action?: string;
84
- method?: "get" | "post";
85
- enctype?: string;
86
- target?: string;
87
- noValidate?: boolean;
88
- }
89
-
90
- export interface InputHTMLAttributes<T = HTMLInputElement>
91
- extends HTMLAttributes<T> {
92
- type?:
93
- | "button"
94
- | "checkbox"
95
- | "color"
96
- | "date"
97
- | "datetime-local"
98
- | "email"
99
- | "file"
100
- | "hidden"
101
- | "image"
102
- | "month"
103
- | "number"
104
- | "password"
105
- | "radio"
106
- | "range"
107
- | "reset"
108
- | "search"
109
- | "submit"
110
- | "tel"
111
- | "text"
112
- | "time"
113
- | "url"
114
- | "week";
115
- value?: string | number;
116
- defaultValue?: string | number;
117
- placeholder?: string;
118
- disabled?: boolean;
119
- required?: boolean;
120
- readOnly?: boolean;
121
- name?: string;
122
- checked?: boolean;
123
- defaultChecked?: boolean;
124
- min?: string | number;
125
- max?: string | number;
126
- step?: string | number;
127
- pattern?: string;
128
- accept?: string;
129
- multiple?: boolean;
130
- autoComplete?: string;
131
- autoFocus?: boolean;
132
- }
133
-
134
- export interface LabelHTMLAttributes<T = HTMLLabelElement>
135
- extends HTMLAttributes<T> {
136
- htmlFor?: string;
137
- for?: string;
138
- }
139
-
140
- export interface SelectHTMLAttributes<T = HTMLSelectElement>
141
- extends HTMLAttributes<T> {
142
- value?: string | string[];
143
- defaultValue?: string | string[];
144
- disabled?: boolean;
145
- required?: boolean;
146
- name?: string;
147
- multiple?: boolean;
148
- size?: number;
149
- }
150
-
151
- export interface OptionHTMLAttributes<T = HTMLOptionElement>
152
- extends HTMLAttributes<T> {
153
- value?: string | number;
154
- selected?: boolean;
155
- disabled?: boolean;
156
- label?: string;
157
- }
158
-
159
- export interface TextareaHTMLAttributes<T = HTMLTextAreaElement>
160
- extends HTMLAttributes<T> {
161
- value?: string;
162
- defaultValue?: string;
163
- placeholder?: string;
164
- disabled?: boolean;
165
- required?: boolean;
166
- readOnly?: boolean;
167
- name?: string;
168
- rows?: number;
169
- cols?: number;
170
- maxLength?: number;
171
- wrap?: "soft" | "hard";
172
- }
173
-
174
- export interface FieldsetHTMLAttributes<T = HTMLFieldSetElement>
175
- extends HTMLAttributes<T> {
176
- disabled?: boolean;
177
- name?: string;
178
- }
179
-
180
- export interface ImgHTMLAttributes<T = HTMLImageElement>
181
- extends HTMLAttributes<T> {
182
- src?: string;
183
- alt?: string;
184
- width?: number | string;
185
- height?: number | string;
186
- loading?: "eager" | "lazy";
187
- crossOrigin?: "anonymous" | "use-credentials";
188
- }
189
-
190
- export interface SVGAttributes<T = SVGElement> extends HTMLAttributes<T> {
191
- xmlns?: string;
192
- viewBox?: string;
193
- width?: number | string;
194
- height?: number | string;
195
- fill?: string;
196
- stroke?: string;
197
- strokeWidth?: number | string;
198
- }
199
-
200
- // Intrinsic Elements
201
- export interface IntrinsicElements {
202
- a: AnchorHTMLAttributes<HTMLAnchorElement>;
203
- abbr: HTMLAttributes<HTMLElement>;
204
- address: HTMLAttributes<HTMLElement>;
205
- article: HTMLAttributes<HTMLElement>;
206
- aside: HTMLAttributes<HTMLElement>;
207
- b: HTMLAttributes<HTMLElement>;
208
- blockquote: HTMLAttributes<HTMLQuoteElement>;
209
- body: HTMLAttributes<HTMLBodyElement>;
210
- br: HTMLAttributes<HTMLBRElement>;
211
- button: ButtonHTMLAttributes<HTMLButtonElement>;
212
- canvas: HTMLAttributes<HTMLCanvasElement>;
213
- code: HTMLAttributes<HTMLElement>;
214
- div: HTMLAttributes<HTMLDivElement>;
215
- em: HTMLAttributes<HTMLElement>;
216
- fieldset: FieldsetHTMLAttributes<HTMLFieldSetElement>;
217
- footer: HTMLAttributes<HTMLElement>;
218
- form: FormHTMLAttributes<HTMLFormElement>;
219
- h1: HTMLAttributes<HTMLHeadingElement>;
220
- h2: HTMLAttributes<HTMLHeadingElement>;
221
- h3: HTMLAttributes<HTMLHeadingElement>;
222
- h4: HTMLAttributes<HTMLHeadingElement>;
223
- h5: HTMLAttributes<HTMLHeadingElement>;
224
- h6: HTMLAttributes<HTMLHeadingElement>;
225
- head: HTMLAttributes<HTMLHeadElement>;
226
- header: HTMLAttributes<HTMLElement>;
227
- hr: HTMLAttributes<HTMLHRElement>;
228
- html: HTMLAttributes<HTMLHtmlElement>;
229
- i: HTMLAttributes<HTMLElement>;
230
- img: ImgHTMLAttributes<HTMLImageElement>;
231
- input: InputHTMLAttributes<HTMLInputElement>;
232
- label: LabelHTMLAttributes<HTMLLabelElement>;
233
- legend: HTMLAttributes<HTMLLegendElement>;
234
- li: HTMLAttributes<HTMLLIElement>;
235
- main: HTMLAttributes<HTMLElement>;
236
- nav: HTMLAttributes<HTMLElement>;
237
- ol: HTMLAttributes<HTMLOListElement>;
238
- option: OptionHTMLAttributes<HTMLOptionElement>;
239
- p: HTMLAttributes<HTMLParagraphElement>;
240
- pre: HTMLAttributes<HTMLPreElement>;
241
- section: HTMLAttributes<HTMLElement>;
242
- select: SelectHTMLAttributes<HTMLSelectElement>;
243
- small: HTMLAttributes<HTMLElement>;
244
- span: HTMLAttributes<HTMLSpanElement>;
245
- strong: HTMLAttributes<HTMLElement>;
246
- style: HTMLAttributes<HTMLStyleElement>;
247
- textarea: TextareaHTMLAttributes<HTMLTextAreaElement>;
248
- ul: HTMLAttributes<HTMLUListElement>;
249
-
250
- // SVG
251
- svg: SVGAttributes<SVGSVGElement>;
252
- circle: SVGAttributes<SVGCircleElement>;
253
- line: SVGAttributes<SVGLineElement>;
254
- path: SVGAttributes<SVGPathElement>;
255
- rect: SVGAttributes<SVGRectElement>;
256
- }
257
- }
2
+ // Re-export Inferno's comprehensive JSX types
3
+ import 'inferno';
4
+
5
+ // Re-export useful Inferno types for convenience
6
+ export type {
7
+ InfernoNode,
8
+ InfernoChild,
9
+ Component,
10
+ ComponentType,
11
+ } from 'inferno';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Temporarily patches document.addEventListener during render to capture
3
+ * and wrap Inferno's delegated event listeners with syncBatch
4
+ */
5
+ export declare function patchInfernoEventHandling(renderFn: () => void): void;
6
+ //# sourceMappingURL=patchInferno.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patchInferno.d.ts","sourceRoot":"","sources":["../src/patchInferno.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,IAAI,QA0D7D"}
@@ -0,0 +1,53 @@
1
+ import { syncBatch } from "./batch";
2
+ /**
3
+ * Temporarily patches document.addEventListener during render to capture
4
+ * and wrap Inferno's delegated event listeners with syncBatch
5
+ */
6
+ export function patchInfernoEventHandling(renderFn) {
7
+ const originalAddEventListener = document.addEventListener.bind(document);
8
+ const patchedEvents = new Set();
9
+ // Inferno's delegated events
10
+ const INFERNO_EVENTS = [
11
+ "click",
12
+ "dblclick",
13
+ "focusin",
14
+ "focusout",
15
+ "keydown",
16
+ "keypress",
17
+ "keyup",
18
+ "mousedown",
19
+ "mousemove",
20
+ "mouseup",
21
+ "touchend",
22
+ "touchmove",
23
+ "touchstart",
24
+ "change",
25
+ "input",
26
+ "submit",
27
+ ];
28
+ // Temporarily replace addEventListener
29
+ document.addEventListener = function (type, listener, options) {
30
+ // Only wrap Inferno's delegated event listeners
31
+ if (INFERNO_EVENTS.includes(type) &&
32
+ typeof listener === "function" &&
33
+ !patchedEvents.has(type)) {
34
+ patchedEvents.add(type);
35
+ const wrappedListener = function (event) {
36
+ syncBatch(() => {
37
+ listener.call(this, event);
38
+ });
39
+ };
40
+ return originalAddEventListener(type, wrappedListener, options);
41
+ }
42
+ // @ts-ignore
43
+ return originalAddEventListener(type, listener, options);
44
+ };
45
+ try {
46
+ // Call render - Inferno will synchronously attach its listeners
47
+ renderFn();
48
+ }
49
+ finally {
50
+ // Restore original addEventListener
51
+ document.addEventListener = originalAddEventListener;
52
+ }
53
+ }
@@ -0,0 +1,4 @@
1
+ export declare function markDirty(): void;
2
+ export declare function enqueueUpdateFromSetter(): void;
3
+ export declare function installGlobalBatching(target?: EventTarget): () => void;
4
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAIA,wBAAgB,SAAS,SAExB;AAeD,wBAAgB,uBAAuB,SAWtC;AAmED,wBAAgB,qBAAqB,CAAC,MAAM,GAAE,WAAoB,cAoBjE"}
@@ -0,0 +1,107 @@
1
+ let depth = 0; // batching scope nesting
2
+ let dirty = false; // has any state update been enqueued?
3
+ let scheduled = false; // async flush scheduled?
4
+ export function markDirty() {
5
+ dirty = true;
6
+ }
7
+ function performWork() {
8
+ // TODO: call your Inferno render/commit once.
9
+ // infernoRender(vnode, container);
10
+ }
11
+ function flushNow() {
12
+ scheduled = false;
13
+ if (!dirty)
14
+ return;
15
+ dirty = false;
16
+ performWork();
17
+ }
18
+ // Called by setters after enqueueing their state change
19
+ export function enqueueUpdateFromSetter() {
20
+ dirty = true;
21
+ if (depth > 0) {
22
+ // We're inside a batched input event; we'll flush on exit (same frame).
23
+ return;
24
+ }
25
+ if (!scheduled) {
26
+ scheduled = true;
27
+ queueMicrotask(flushNow); // one flush per task
28
+ }
29
+ }
30
+ // Batch-scope control used by the global capture listeners
31
+ function enter() {
32
+ depth++;
33
+ }
34
+ function exit() {
35
+ if (--depth === 0) {
36
+ // End of the event propagation; commit now (before next paint).
37
+ flushNow();
38
+ }
39
+ }
40
+ // eventBatching.ts
41
+ const INTERACTIVE_EVENTS = [
42
+ // Pointer + mouse
43
+ "click",
44
+ "dblclick",
45
+ "contextmenu",
46
+ "mousedown",
47
+ "mouseup",
48
+ "mousemove",
49
+ "pointerdown",
50
+ "pointerup",
51
+ "pointermove",
52
+ "touchstart",
53
+ "touchmove",
54
+ "touchend",
55
+ "touchcancel",
56
+ "dragstart",
57
+ "drag",
58
+ "dragend",
59
+ "dragenter",
60
+ "dragleave",
61
+ "dragover",
62
+ "drop",
63
+ "wheel",
64
+ // Keyboard
65
+ "keydown",
66
+ "keypress",
67
+ "keyup",
68
+ // Focus & input
69
+ "focus",
70
+ "blur",
71
+ "focusin",
72
+ "focusout",
73
+ "input",
74
+ "beforeinput",
75
+ "change",
76
+ "compositionstart",
77
+ "compositionupdate",
78
+ "compositionend",
79
+ // Forms
80
+ "submit",
81
+ "reset",
82
+ // Selection / clipboard
83
+ "select",
84
+ "selectionchange",
85
+ "copy",
86
+ "cut",
87
+ "paste",
88
+ ];
89
+ export function installGlobalBatching(target = window) {
90
+ const handlers = [];
91
+ INTERACTIVE_EVENTS.forEach((type) => {
92
+ const onCapture = () => {
93
+ enter();
94
+ // Close the scope after all handlers (capture→target→bubble) have run.
95
+ queueMicrotask(exit);
96
+ };
97
+ target.addEventListener(type, onCapture, { capture: true });
98
+ handlers.push([onCapture, { capture: true }]);
99
+ });
100
+ // Return a disposer so you can remove on unmount
101
+ return () => {
102
+ INTERACTIVE_EVENTS.forEach((type, i) => {
103
+ const [fn, opts] = handlers[i];
104
+ target.removeEventListener(type, fn, opts);
105
+ });
106
+ };
107
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=batch.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.test.d.ts","sourceRoot":"","sources":["../../src/tests/batch.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,244 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { syncBatch } from "../batch";
3
+ import { createState } from "../createState";
4
+ import { Observer } from "../observation";
5
+ describe("syncBatch", () => {
6
+ it("should batch multiple state changes into a single notification", () => {
7
+ const state = createState({ count: 0, name: "Alice" });
8
+ let notifyCount = 0;
9
+ const observer = new Observer(() => {
10
+ notifyCount++;
11
+ });
12
+ const dispose = observer.observe();
13
+ state.count; // Track count
14
+ state.name; // Track name
15
+ dispose();
16
+ // Make multiple changes in a batch
17
+ syncBatch(() => {
18
+ state.count = 1;
19
+ state.name = "Bob";
20
+ state.count = 2;
21
+ });
22
+ // Should only notify once despite multiple changes, and synchronously
23
+ expect(notifyCount).toBe(1);
24
+ expect(state.count).toBe(2);
25
+ expect(state.name).toBe("Bob");
26
+ observer.dispose();
27
+ });
28
+ it("should handle nested batches correctly", () => {
29
+ const state = createState({ count: 0 });
30
+ let notifyCount = 0;
31
+ const observer = new Observer(() => {
32
+ notifyCount++;
33
+ });
34
+ const dispose = observer.observe();
35
+ state.count; // Track
36
+ dispose();
37
+ syncBatch(() => {
38
+ state.count = 1;
39
+ syncBatch(() => {
40
+ state.count = 2;
41
+ });
42
+ state.count = 3;
43
+ });
44
+ // Should still only notify once for nested batches
45
+ expect(notifyCount).toBe(1);
46
+ expect(state.count).toBe(3);
47
+ observer.dispose();
48
+ });
49
+ it("should handle multiple observers with syncBatch", () => {
50
+ const state = createState({ count: 0 });
51
+ let notifyCount1 = 0;
52
+ let notifyCount2 = 0;
53
+ const observer1 = new Observer(() => {
54
+ notifyCount1++;
55
+ });
56
+ const observer2 = new Observer(() => {
57
+ notifyCount2++;
58
+ });
59
+ const dispose1 = observer1.observe();
60
+ state.count; // Track in observer1
61
+ dispose1();
62
+ const dispose2 = observer2.observe();
63
+ state.count; // Track in observer2
64
+ dispose2();
65
+ syncBatch(() => {
66
+ state.count = 1;
67
+ state.count = 2;
68
+ state.count = 3;
69
+ });
70
+ // Both observers should be notified exactly once
71
+ expect(notifyCount1).toBe(1);
72
+ expect(notifyCount2).toBe(1);
73
+ observer1.dispose();
74
+ observer2.dispose();
75
+ });
76
+ it("should maintain correct state values after syncBatch", () => {
77
+ const state = createState({
78
+ count: 0,
79
+ name: "Alice",
80
+ items: [1, 2, 3],
81
+ });
82
+ syncBatch(() => {
83
+ state.count = 10;
84
+ state.name = "Bob";
85
+ state.items.push(4);
86
+ state.items[0] = 100;
87
+ });
88
+ expect(state.count).toBe(10);
89
+ expect(state.name).toBe("Bob");
90
+ expect(state.items).toEqual([100, 2, 3, 4]);
91
+ });
92
+ it("should not flush if exception thrown within syncBatch", () => {
93
+ const state = createState({ count: 0 });
94
+ let notifyCount = 0;
95
+ const observer = new Observer(() => {
96
+ notifyCount++;
97
+ });
98
+ const dispose = observer.observe();
99
+ state.count; // Track
100
+ dispose();
101
+ try {
102
+ syncBatch(() => {
103
+ state.count = 1;
104
+ throw new Error("Test error");
105
+ });
106
+ }
107
+ catch (e) {
108
+ // Expected error
109
+ }
110
+ // Should NOT have flushed since the batch was interrupted
111
+ expect(notifyCount).toBe(0);
112
+ // But state change still occurred
113
+ expect(state.count).toBe(1);
114
+ observer.dispose();
115
+ });
116
+ it("should deduplicate notifications for the same observer", () => {
117
+ const state = createState({ count: 0, name: "Alice" });
118
+ let notifyCount = 0;
119
+ const observer = new Observer(() => {
120
+ notifyCount++;
121
+ });
122
+ const dispose = observer.observe();
123
+ state.count; // Track
124
+ state.name; // Track
125
+ dispose();
126
+ syncBatch(() => {
127
+ state.count = 1; // Triggers observer
128
+ state.name = "Bob"; // Triggers same observer again
129
+ state.count = 2; // Triggers observer yet again
130
+ });
131
+ // Should deduplicate and only notify once
132
+ expect(notifyCount).toBe(1);
133
+ observer.dispose();
134
+ });
135
+ });
136
+ describe("queue (async batching)", () => {
137
+ it("should queue updates and flush on microtask", async () => {
138
+ const state = createState({ count: 0 });
139
+ let notifyCount = 0;
140
+ const observer = new Observer(() => {
141
+ notifyCount++;
142
+ });
143
+ const dispose = observer.observe();
144
+ state.count; // Track
145
+ dispose();
146
+ // Make changes that will be queued
147
+ state.count = 1;
148
+ state.count = 2;
149
+ state.count = 3;
150
+ // Not yet notified (queued)
151
+ expect(notifyCount).toBe(0);
152
+ // Wait for microtask flush
153
+ await new Promise((resolve) => setTimeout(resolve, 0));
154
+ // Should have notified once after flush
155
+ expect(notifyCount).toBe(1);
156
+ expect(state.count).toBe(3);
157
+ observer.dispose();
158
+ });
159
+ it("should batch multiple async updates into one notification", async () => {
160
+ const state = createState({ count: 0, name: "Alice" });
161
+ let notifyCount = 0;
162
+ const observer = new Observer(() => {
163
+ notifyCount++;
164
+ });
165
+ const dispose = observer.observe();
166
+ state.count;
167
+ state.name;
168
+ dispose();
169
+ state.count = 1;
170
+ state.name = "Bob";
171
+ state.count = 2;
172
+ await new Promise((resolve) => setTimeout(resolve, 0));
173
+ // Should batch all updates into single notification
174
+ expect(notifyCount).toBe(1);
175
+ observer.dispose();
176
+ });
177
+ it("should handle separate async batches", async () => {
178
+ const state = createState({ count: 0 });
179
+ let notifyCount = 0;
180
+ const observer = new Observer(() => {
181
+ notifyCount++;
182
+ });
183
+ const dispose = observer.observe();
184
+ state.count;
185
+ dispose();
186
+ state.count = 1;
187
+ await new Promise((resolve) => setTimeout(resolve, 0));
188
+ const afterFirst = notifyCount;
189
+ state.count = 2;
190
+ await new Promise((resolve) => setTimeout(resolve, 0));
191
+ const afterSecond = notifyCount;
192
+ expect(afterFirst).toBe(1);
193
+ expect(afterSecond).toBe(2);
194
+ observer.dispose();
195
+ });
196
+ });
197
+ describe("syncBatch with nested async updates", () => {
198
+ it("should handle syncBatch inside async context", async () => {
199
+ const state = createState({ count: 0 });
200
+ let notifyCount = 0;
201
+ const observer = new Observer(() => {
202
+ notifyCount++;
203
+ });
204
+ const dispose = observer.observe();
205
+ state.count;
206
+ dispose();
207
+ // Async update
208
+ state.count = 1;
209
+ await new Promise((resolve) => setTimeout(resolve, 0));
210
+ expect(notifyCount).toBe(1);
211
+ // Sync batch after async
212
+ syncBatch(() => {
213
+ state.count = 2;
214
+ state.count = 3;
215
+ });
216
+ expect(notifyCount).toBe(2); // +1 from sync batch
217
+ observer.dispose();
218
+ });
219
+ it("should handle async updates inside syncBatch callback", async () => {
220
+ const state = createState({ count: 0 });
221
+ let notifyCount = 0;
222
+ const observer = new Observer(() => {
223
+ notifyCount++;
224
+ });
225
+ const dispose = observer.observe();
226
+ state.count;
227
+ dispose();
228
+ syncBatch(() => {
229
+ state.count = 1;
230
+ // Trigger an async update from within syncBatch
231
+ setTimeout(() => {
232
+ state.count = 2;
233
+ }, 0);
234
+ });
235
+ // Sync batch should flush immediately
236
+ expect(notifyCount).toBe(1);
237
+ expect(state.count).toBe(1);
238
+ // Wait for async update
239
+ await new Promise((resolve) => setTimeout(resolve, 10));
240
+ expect(notifyCount).toBe(2);
241
+ expect(state.count).toBe(2);
242
+ observer.dispose();
243
+ });
244
+ });