rask-ui 0.10.5 → 0.12.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 (52) hide show
  1. package/README.md +257 -5
  2. package/dist/batch.d.ts.map +1 -1
  3. package/dist/batch.js +36 -55
  4. package/dist/component.d.ts +2 -1
  5. package/dist/component.d.ts.map +1 -1
  6. package/dist/component.js +37 -22
  7. package/dist/createContext.d.ts +1 -0
  8. package/dist/createContext.d.ts.map +1 -1
  9. package/dist/createContext.js +11 -0
  10. package/dist/createEffect.d.ts +1 -1
  11. package/dist/createEffect.d.ts.map +1 -1
  12. package/dist/createEffect.js +15 -5
  13. package/dist/createRouter.d.ts +8 -0
  14. package/dist/createRouter.d.ts.map +1 -0
  15. package/dist/createRouter.js +24 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -0
  19. package/dist/jsx-runtime.d.ts +2 -2
  20. package/dist/jsx-runtime.js +2 -2
  21. package/dist/plugin.d.ts +9 -1
  22. package/dist/plugin.d.ts.map +1 -1
  23. package/dist/plugin.js +4 -3
  24. package/dist/tests/batch.test.d.ts +2 -0
  25. package/dist/tests/batch.test.d.ts.map +1 -0
  26. package/dist/tests/batch.test.js +244 -0
  27. package/dist/tests/createComputed.test.d.ts +2 -0
  28. package/dist/tests/createComputed.test.d.ts.map +1 -0
  29. package/dist/tests/createComputed.test.js +257 -0
  30. package/dist/tests/createContext.test.d.ts +2 -0
  31. package/dist/tests/createContext.test.d.ts.map +1 -0
  32. package/dist/tests/createContext.test.js +136 -0
  33. package/dist/tests/createEffect.test.d.ts +2 -0
  34. package/dist/tests/createEffect.test.d.ts.map +1 -0
  35. package/dist/tests/createEffect.test.js +454 -0
  36. package/dist/tests/createState.test.d.ts +2 -0
  37. package/dist/tests/createState.test.d.ts.map +1 -0
  38. package/dist/tests/createState.test.js +144 -0
  39. package/dist/tests/createTask.test.d.ts +2 -0
  40. package/dist/tests/createTask.test.d.ts.map +1 -0
  41. package/dist/tests/createTask.test.js +322 -0
  42. package/dist/tests/createView.test.d.ts +2 -0
  43. package/dist/tests/createView.test.d.ts.map +1 -0
  44. package/dist/tests/createView.test.js +203 -0
  45. package/dist/tests/error.test.d.ts +2 -0
  46. package/dist/tests/error.test.d.ts.map +1 -0
  47. package/dist/tests/error.test.js +168 -0
  48. package/dist/tests/observation.test.d.ts +2 -0
  49. package/dist/tests/observation.test.d.ts.map +1 -0
  50. package/dist/tests/observation.test.js +341 -0
  51. package/package.json +4 -3
  52. package/swc-plugin/target/wasm32-wasip1/release/swc_plugin_rask_component.wasm +0 -0
package/README.md CHANGED
@@ -484,23 +484,58 @@ function Timer() {
484
484
  }
485
485
  ```
486
486
 
487
+ **Effect with Disposal:**
488
+
489
+ The callback can optionally return a dispose function that runs before the effect executes again:
490
+
491
+ ```tsx
492
+ import { createEffect, createState } from "rask-ui";
493
+
494
+ function LiveData() {
495
+ const state = createState({ url: "/api/data", data: null });
496
+
497
+ createEffect(() => {
498
+ const eventSource = new EventSource(state.url);
499
+
500
+ eventSource.onmessage = (event) => {
501
+ state.data = JSON.parse(event.data);
502
+ };
503
+
504
+ // Dispose function runs before effect re-executes
505
+ return () => {
506
+ eventSource.close();
507
+ };
508
+ });
509
+
510
+ return () => (
511
+ <div>
512
+ <input value={state.url} onInput={(e) => state.url = e.target.value} />
513
+ <pre>{JSON.stringify(state.data, null, 2)}</pre>
514
+ </div>
515
+ );
516
+ }
517
+ ```
518
+
487
519
  **Parameters:**
488
520
 
489
- - `callback: () => void` - Function to run when dependencies change
521
+ - `callback: () => void | (() => void)` - Function to run when dependencies change. Can optionally return a dispose function that runs before the effect executes again.
490
522
 
491
523
  **Features:**
492
524
 
493
- - Runs immediately on creation
525
+ - Runs immediately and synchronously on creation during setup
494
526
  - Automatically tracks reactive dependencies accessed during execution
495
- - Re-runs on microtask when dependencies change (prevents synchronous cascades)
527
+ - Re-runs when dependencies change
496
528
  - Automatically cleaned up when component unmounts
497
- - Can be used for side effects like logging, syncing to localStorage, or updating derived state
529
+ - Optional dispose function for cleaning up resources before re-execution
530
+ - Can be used for side effects like logging, syncing to localStorage, managing subscriptions, or updating derived state
498
531
 
499
532
  **Notes:**
500
533
 
501
534
  - Only call during component setup phase (not in render function)
502
- - Effects are queued on microtask to avoid synchronous execution from prop changes
535
+ - Effect runs synchronously during setup, making it predictable and easier to reason about
503
536
  - Be careful with effects that modify state - can cause infinite loops if not careful
537
+ - Dispose functions run before the effect re-executes, not when the component unmounts
538
+ - For component unmount cleanup, use `createCleanup()` instead
504
539
 
505
540
  ---
506
541
 
@@ -953,6 +988,223 @@ Error:
953
988
 
954
989
  ---
955
990
 
991
+ ### Routing
992
+
993
+ #### `createRouter<T>(config, options?)`
994
+
995
+ Creates a reactive router for client-side navigation. Built on [typed-client-router](https://github.com/christianalfoni/typed-client-router), it integrates seamlessly with RASK's reactive system for fully type-safe routing.
996
+
997
+ ```tsx
998
+ import { createRouter } from "rask-ui";
999
+
1000
+ const routes = {
1001
+ home: "/",
1002
+ about: "/about",
1003
+ user: "/users/:id",
1004
+ post: "/posts/:id",
1005
+ } as const;
1006
+
1007
+ function App() {
1008
+ const router = createRouter(routes);
1009
+
1010
+ return () => {
1011
+ // Match current route
1012
+ if (router.route?.name === "home") {
1013
+ return <Home />;
1014
+ }
1015
+
1016
+ if (router.route?.name === "user") {
1017
+ return <User id={router.route.params.id} />;
1018
+ }
1019
+
1020
+ if (router.route?.name === "post") {
1021
+ return <Post id={router.route.params.id} />;
1022
+ }
1023
+
1024
+ return <NotFound />;
1025
+ };
1026
+ }
1027
+ ```
1028
+
1029
+ **Parameters:**
1030
+
1031
+ - `config: T` - Route configuration object mapping route names to path patterns
1032
+ - `options?: { base?: string }` - Optional base path for all routes
1033
+
1034
+ **Returns:** Router object with reactive state and navigation methods:
1035
+
1036
+ **Properties:**
1037
+ - `route?: Route` - Current active route with `name` and `params` properties (reactive)
1038
+ - `queries: Record<string, string>` - Current URL query parameters (reactive)
1039
+
1040
+ **Methods:**
1041
+ - `push(name, params?, query?)` - Navigate to a route by name
1042
+ - `replace(name, params?, query?)` - Replace current route (no history entry)
1043
+ - `setQuery(query)` - Update query parameters
1044
+ - `url(name, params?, query?)` - Generate URL for a route
1045
+
1046
+ **Route Configuration:**
1047
+
1048
+ Define routes with path patterns. Use `:param` for dynamic segments:
1049
+
1050
+ ```tsx
1051
+ const routes = {
1052
+ home: "/",
1053
+ users: "/users",
1054
+ user: "/users/:id",
1055
+ userPosts: "/users/:userId/posts/:postId",
1056
+ } as const;
1057
+ ```
1058
+
1059
+ **Navigation:**
1060
+
1061
+ Navigate programmatically using route names:
1062
+
1063
+ ```tsx
1064
+ function Navigation() {
1065
+ const router = createRouter(routes);
1066
+
1067
+ return () => (
1068
+ <nav>
1069
+ <button onClick={() => router.push("home")}>Home</button>
1070
+ <button onClick={() => router.push("user", { id: "123" })}>
1071
+ User 123
1072
+ </button>
1073
+ <button onClick={() => router.push("home", {}, { tab: "recent" })}>
1074
+ Home (Recent)
1075
+ </button>
1076
+ </nav>
1077
+ );
1078
+ }
1079
+ ```
1080
+
1081
+ **Query Parameters:**
1082
+
1083
+ Access and modify query parameters:
1084
+
1085
+ ```tsx
1086
+ function SearchPage() {
1087
+ const router = createRouter(routes);
1088
+
1089
+ return () => (
1090
+ <div>
1091
+ <p>Search: {router.queries.q || "none"}</p>
1092
+ <input
1093
+ value={router.queries.q || ""}
1094
+ onInput={(e) => router.setQuery({ q: e.target.value })}
1095
+ />
1096
+ </div>
1097
+ );
1098
+ }
1099
+ ```
1100
+
1101
+ **Reactive Routing:**
1102
+
1103
+ The router integrates with RASK's reactivity system. Accessing `router.route` or `router.queries` automatically tracks dependencies:
1104
+
1105
+ ```tsx
1106
+ function App() {
1107
+ const router = createRouter(routes);
1108
+ const state = createState({ posts: [] });
1109
+
1110
+ // Effect runs when route changes
1111
+ createEffect(() => {
1112
+ if (router.route?.name === "user") {
1113
+ // Fetch user data when route changes
1114
+ fetch(`/api/users/${router.route.params.id}`)
1115
+ .then((r) => r.json())
1116
+ .then((data) => (state.posts = data.posts));
1117
+ }
1118
+ });
1119
+
1120
+ return () => (
1121
+ <div>
1122
+ {router.route?.name === "user" && (
1123
+ <div>
1124
+ <h1>User {router.route.params.id}</h1>
1125
+ <ul>
1126
+ {state.posts.map((post) => (
1127
+ <li key={post.id}>{post.title}</li>
1128
+ ))}
1129
+ </ul>
1130
+ </div>
1131
+ )}
1132
+ </div>
1133
+ );
1134
+ }
1135
+ ```
1136
+
1137
+ **Type Safety:**
1138
+
1139
+ Routes are fully type-safe. TypeScript will infer parameter types from route patterns:
1140
+
1141
+ ```tsx
1142
+ const routes = {
1143
+ user: "/users/:id",
1144
+ post: "/posts/:postId/:commentId",
1145
+ } as const;
1146
+
1147
+ const router = createRouter(routes);
1148
+
1149
+ // ✅ Type-safe - id is required
1150
+ router.push("user", { id: "123" });
1151
+
1152
+ // ❌ Type error - missing required params
1153
+ router.push("post", { postId: "1" }); // Error: missing commentId
1154
+
1155
+ // ✅ Type-safe params access
1156
+ if (router.route?.name === "post") {
1157
+ const postId = router.route.params.postId; // string
1158
+ const commentId = router.route.params.commentId; // string
1159
+ }
1160
+ ```
1161
+
1162
+ **Context Pattern:**
1163
+
1164
+ Share router across components using context:
1165
+
1166
+ ```tsx
1167
+ import { createRouter, createContext } from "rask-ui";
1168
+
1169
+ const routes = {
1170
+ home: "/",
1171
+ about: "/about",
1172
+ } as const;
1173
+
1174
+ const RouterContext = createContext<Router<typeof routes>>();
1175
+
1176
+ function App() {
1177
+ const router = createRouter(routes);
1178
+
1179
+ RouterContext.inject(router);
1180
+
1181
+ return () => <Content />;
1182
+ }
1183
+
1184
+ function Content() {
1185
+ const router = RouterContext.get();
1186
+
1187
+ return () => (
1188
+ <nav>
1189
+ <button onClick={() => router.push("home")}>Home</button>
1190
+ <button onClick={() => router.push("about")}>About</button>
1191
+ </nav>
1192
+ );
1193
+ }
1194
+ ```
1195
+
1196
+ **Features:**
1197
+
1198
+ - **Type-safe** - Full TypeScript inference for routes and parameters
1199
+ - **Reactive** - Automatically tracks route changes
1200
+ - **Declarative** - Navigate using route names, not URLs
1201
+ - **Query parameters** - Built-in query string management
1202
+ - **No special components** - Use standard conditionals and component composition
1203
+ - **History API** - Built on the browser's History API
1204
+ - **Automatic cleanup** - Router listener cleaned up when component unmounts
1205
+
1206
+ ---
1207
+
956
1208
  ### Developer Tools
957
1209
 
958
1210
  #### `inspect(root, callback)`
@@ -1 +1 @@
1
- {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAgElE,wBAAgB,KAAK,CAAC,EAAE,EAAE,cAAc,QAevC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAgBvC"}
1
+ {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../src/batch.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG;IAAE,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AA8ClE,wBAAgB,KAAK,CAAC,EAAE,EAAE,cAAc,QAiBvC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAgBvC"}
package/dist/batch.js CHANGED
@@ -1,11 +1,7 @@
1
1
  const asyncQueue = [];
2
- const syncQueue = [];
2
+ const syncQueueStack = [];
3
3
  let inInteractive = 0;
4
4
  let asyncScheduled = false;
5
- let inSyncBatch = 0;
6
- // New: guards against re-entrant flushing
7
- let inAsyncFlush = false;
8
- let inSyncFlush = false;
9
5
  function scheduleAsyncFlush() {
10
6
  if (asyncScheduled)
11
7
  return;
@@ -13,77 +9,62 @@ function scheduleAsyncFlush() {
13
9
  queueMicrotask(flushAsyncQueue);
14
10
  }
15
11
  function flushAsyncQueue() {
16
- if (inAsyncFlush)
17
- return;
18
- inAsyncFlush = true;
19
12
  asyncScheduled = false;
20
- try {
21
- if (!asyncQueue.length)
22
- return;
23
- // Note: we intentionally DO NOT snapshot.
24
- // If callbacks queue more async work, it gets picked up
25
- // in this same loop because length grows.
26
- for (let i = 0; i < asyncQueue.length; i++) {
27
- const cb = asyncQueue[i];
28
- asyncQueue[i] = undefined;
29
- cb();
30
- cb.__queued = false;
31
- }
32
- asyncQueue.length = 0;
33
- }
34
- finally {
35
- inAsyncFlush = false;
13
+ if (!asyncQueue.length)
14
+ return;
15
+ // Note: we intentionally DO NOT snapshot.
16
+ // If callbacks queue more async work, it gets picked up
17
+ // in this same loop because length grows.
18
+ for (let i = 0; i < asyncQueue.length; i++) {
19
+ const cb = asyncQueue[i];
20
+ asyncQueue[i] = undefined;
21
+ cb();
22
+ cb.__queued = false;
36
23
  }
24
+ asyncQueue.length = 0;
37
25
  }
38
- function flushSyncQueue() {
39
- if (inSyncFlush)
26
+ function flushSyncQueue(queue) {
27
+ if (!queue.length)
40
28
  return;
41
- inSyncFlush = true;
42
- try {
43
- if (!syncQueue.length)
44
- return;
45
- // Same pattern as async: no snapshot, just iterate.
46
- // New callbacks queued via syncBatch inside this flush
47
- // will be pushed to syncQueue and picked up by this loop.
48
- for (let i = 0; i < syncQueue.length; i++) {
49
- const cb = syncQueue[i];
50
- syncQueue[i] = undefined;
51
- cb();
52
- cb.__queued = false;
53
- }
54
- syncQueue.length = 0;
55
- }
56
- finally {
57
- inSyncFlush = false;
29
+ // No snapshot, just iterate.
30
+ // New callbacks queued via nested syncBatch will create
31
+ // their own queue on the stack and flush independently.
32
+ for (let i = 0; i < queue.length; i++) {
33
+ const cb = queue[i];
34
+ queue[i] = undefined;
35
+ cb();
36
+ cb.__queued = false;
58
37
  }
38
+ queue.length = 0;
59
39
  }
60
40
  export function queue(cb) {
61
41
  // Optional: uncomment this if you want deduping:
62
42
  // if (cb.__queued) return;
63
43
  cb.__queued = true;
64
- if (inSyncBatch) {
65
- syncQueue.push(cb);
44
+ // If we're in a sync batch, push to the current sync queue
45
+ if (syncQueueStack.length) {
46
+ syncQueueStack[syncQueueStack.length - 1].push(cb);
66
47
  return;
67
48
  }
49
+ // Otherwise, push to async queue
68
50
  asyncQueue.push(cb);
69
51
  if (!inInteractive) {
70
52
  scheduleAsyncFlush();
71
53
  }
72
54
  }
73
55
  export function syncBatch(cb) {
74
- inSyncBatch++;
56
+ // Create a new queue for this sync batch
57
+ const queue = [];
58
+ syncQueueStack.push(queue);
75
59
  try {
76
60
  cb();
77
61
  }
78
62
  catch (e) {
79
- inSyncBatch--;
80
- throw e; // no flush on error
81
- }
82
- inSyncBatch--;
83
- if (!inSyncBatch) {
84
- // Only the outermost syncBatch triggers a flush.
85
- // If this happens *inside* an ongoing flushSyncQueue,
86
- // inSyncFlush will be true and flushSyncQueue will no-op.
87
- flushSyncQueue();
63
+ // Pop the queue even on error, but don't flush
64
+ syncQueueStack.pop();
65
+ throw e;
88
66
  }
67
+ // Pop the queue and flush it
68
+ syncQueueStack.pop();
69
+ flushSyncQueue(queue);
89
70
  }
@@ -17,6 +17,8 @@ export declare class RaskStatefulComponent<P extends Props<any>> extends Compone
17
17
  private reactiveProps?;
18
18
  private observer;
19
19
  private isRendering;
20
+ private willRender;
21
+ private nextProps;
20
22
  effects: Array<{
21
23
  isDirty: boolean;
22
24
  run: () => void;
@@ -32,7 +34,6 @@ export declare class RaskStatefulComponent<P extends Props<any>> extends Compone
32
34
  *
33
35
  */
34
36
  componentWillUpdate(nextProps: any): void;
35
- componentWillReceiveProps(): void;
36
37
  shouldComponentUpdate(nextProps: Props<any>): boolean;
37
38
  render(): any;
38
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EACL,SAAS,EACT,KAAK,EAEN,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAsB,QAAQ,EAAU,MAAM,eAAe,CAAC;AAGrE,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC3D,CAAC,MAAM,KAAK,CAAC,GACb,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC;AAE1B,qBAAa,sBAAuB,SAAQ,SAAS;IAC3C,QAAQ,EAAE,8BAA8B,CAAC,GAAG,CAAC,CAAC;IACtD,QAAQ,WAEL;IACH,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAUrD,MAAM;CAMP;AAID,wBAAgB,mBAAmB,+BAMlC;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,IAAI,QAM/C;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,IAAI,QAM3C;AAED,MAAM,MAAM,6BAA6B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC1D,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,qBAAa,qBAAqB,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CAAC,CAAC,CAAC;IACnE,KAAK,EAAE,6BAA6B,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,CAAC,QAAQ,CAAC,CAAc;IAC/B,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,QAAQ,CAEb;IACH,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAM;IAC3D,QAAQ,gBAAa;IACrB,eAAe;IAUf,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACjC,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACnC,OAAO,CAAC,mBAAmB;IA2D3B,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAG5B;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,GAAG;IAYlC,yBAAyB,IAAI,IAAI;IACjC,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAWrD,MAAM;CAyCP;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,SAO9D"}
1
+ {"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../src/component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAExE,OAAO,EAAsB,QAAQ,EAAU,MAAM,eAAe,CAAC;AAIrE,MAAM,MAAM,8BAA8B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC3D,CAAC,MAAM,KAAK,CAAC,GACb,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC;AAE1B,qBAAa,sBAAuB,SAAQ,SAAS;IAC3C,QAAQ,EAAE,8BAA8B,CAAC,GAAG,CAAC,CAAC;IACtD,QAAQ,WAEL;IACH,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAUrD,MAAM;CAMP;AAID,wBAAgB,mBAAmB,+BAMlC;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,IAAI,QAM/C;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,IAAI,QAM3C;AAED,MAAM,MAAM,6BAA6B,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,IAC1D,CAAC,MAAM,MAAM,KAAK,CAAC,GACnB,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,KAAK,CAAC,CAAC;AAEhC,qBAAa,qBAAqB,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,CAAC,CAAE,SAAQ,SAAS,CAAC,CAAC,CAAC;IACnE,KAAK,EAAE,6BAA6B,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,CAAC,QAAQ,CAAC,CAAc;IAC/B,OAAO,CAAC,aAAa,CAAC,CAAa;IACnC,OAAO,CAAC,QAAQ,CAMb;IAEH,OAAO,CAAC,WAAW,CAAS;IAE5B,OAAO,CAAC,UAAU,CAAQ;IAG1B,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAM;IAC3D,QAAQ,gBAAa;IACrB,eAAe;IAUf,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACjC,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAM;IACnC,OAAO,CAAC,mBAAmB;IAyD3B,iBAAiB,IAAI,IAAI;IAGzB,oBAAoB,IAAI,IAAI;IAG5B;;OAEG;IACH,mBAAmB,CAAC,SAAS,EAAE,GAAG;IAoBlC,qBAAqB,CAAC,SAAS,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,OAAO;IAWrD,MAAM;CA2CP;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,SAO9D"}
package/dist/component.js CHANGED
@@ -1,6 +1,7 @@
1
- import { createComponentVNode, Component, } from "inferno";
1
+ import { createComponentVNode, Component } from "inferno";
2
2
  import { getCurrentObserver, Observer, Signal } from "./observation";
3
3
  import { syncBatch } from "./batch";
4
+ import { PROXY_MARKER } from "./createState";
4
5
  export class RaskStatelessComponent extends Component {
5
6
  observer = new Observer(() => {
6
7
  this.forceUpdate();
@@ -44,9 +45,19 @@ export class RaskStatefulComponent extends Component {
44
45
  renderFn;
45
46
  reactiveProps;
46
47
  observer = new Observer(() => {
48
+ console.log("OBSERVER", this.willRender, this.setup.name);
49
+ if (this.willRender) {
50
+ return;
51
+ }
47
52
  this.forceUpdate();
48
53
  });
54
+ // Flag to prevent props from tracking in render scope (We use props reconciliation)
49
55
  isRendering = false;
56
+ // Flag to prevent observer notifications to cause render during reconciliation
57
+ willRender = true;
58
+ // Since reactive props updates before the reconciliation (without causing a new one), we
59
+ // need to return these from the reactive props
60
+ nextProps = this.props;
50
61
  effects = [];
51
62
  contexts = new Map();
52
63
  getChildContext() {
@@ -64,44 +75,42 @@ export class RaskStatefulComponent extends Component {
64
75
  const reactiveProps = {};
65
76
  const self = this;
66
77
  const signals = new Map();
67
- for (const prop in this.props) {
68
- const value = this.props[prop];
78
+ for (const prop in this.nextProps) {
79
+ const value = this.nextProps[prop];
69
80
  // Skip known non-reactive props
70
- if (typeof value === "function" ||
71
- prop === "children" ||
72
- prop === "key" ||
73
- prop === "ref") {
81
+ if (prop === "key" || prop === "ref") {
74
82
  reactiveProps[prop] = value;
75
83
  continue;
76
84
  }
77
85
  // Skip objects/arrays - they're already reactive if they're proxies
78
86
  // No need to wrap them in additional signals
79
- if (typeof value === "object" && value !== null) {
87
+ if (typeof value === "object" &&
88
+ value !== null &&
89
+ PROXY_MARKER in value) {
80
90
  reactiveProps[prop] = value;
81
91
  continue;
82
92
  }
83
93
  // Only create reactive getters for primitives
84
94
  Object.defineProperty(reactiveProps, prop, {
95
+ enumerable: true,
85
96
  get() {
86
- if (!self.isRendering) {
87
- const observer = getCurrentObserver();
88
- if (observer) {
89
- // Lazy create signal only when accessed in reactive context
90
- let signal = signals.get(prop);
91
- if (!signal) {
92
- signal = new Signal();
93
- signals.set(prop, signal);
94
- }
95
- observer.subscribeSignal(signal);
97
+ const observer = getCurrentObserver();
98
+ if (!self.isRendering && observer) {
99
+ // Lazy create signal only when accessed in reactive context
100
+ let signal = signals.get(prop);
101
+ if (!signal) {
102
+ signal = new Signal();
103
+ signals.set(prop, signal);
96
104
  }
105
+ observer.subscribeSignal(signal);
97
106
  }
98
107
  // @ts-ignore
99
- return self.props[prop];
108
+ return self.nextProps[prop];
100
109
  },
101
110
  set(value) {
102
111
  // Only notify if signal was created (i.e., prop was accessed reactively)
103
112
  const signal = signals.get(prop);
104
- if (signal && self.props[prop] !== value) {
113
+ if (signal) {
105
114
  signal.notify();
106
115
  }
107
116
  },
@@ -119,17 +128,21 @@ export class RaskStatefulComponent extends Component {
119
128
  *
120
129
  */
121
130
  componentWillUpdate(nextProps) {
131
+ this.willRender = true;
132
+ this.nextProps = nextProps;
133
+ console.log("Props update", this.setup.name);
122
134
  syncBatch(() => {
123
135
  for (const prop in nextProps) {
124
- if (prop === "children") {
136
+ if (prop === "children" ||
137
+ this.props[prop] === nextProps[prop]) {
125
138
  continue;
126
139
  }
127
140
  // @ts-ignore
128
141
  this.reactiveProps[prop] = nextProps[prop];
129
142
  }
130
143
  });
144
+ console.log("Props update end");
131
145
  }
132
- componentWillReceiveProps() { }
133
146
  shouldComponentUpdate(nextProps) {
134
147
  // Shallow comparison of props, excluding internal props
135
148
  for (const prop in nextProps) {
@@ -162,9 +175,11 @@ export class RaskStatefulComponent extends Component {
162
175
  const stopObserving = this.observer.observe();
163
176
  let result = null;
164
177
  try {
178
+ console.log("RENDER", this.setup.name);
165
179
  this.isRendering = true;
166
180
  result = this.renderFn();
167
181
  this.isRendering = false;
182
+ this.willRender = false;
168
183
  }
169
184
  catch (error) {
170
185
  if (typeof this.context.notifyError !== "function") {
@@ -25,5 +25,6 @@
25
25
  export declare function createContext<T>(): {
26
26
  inject(value: T): void;
27
27
  get(): T;
28
+ hasValue(): boolean;
28
29
  };
29
30
  //# sourceMappingURL=createContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createContext.d.ts","sourceRoot":"","sources":["../src/createContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,wBAAgB,aAAa,CAAC,CAAC;kBAEb,CAAC;WASR,CAAC;EA0BX"}
1
+ {"version":3,"file":"createContext.d.ts","sourceRoot":"","sources":["../src/createContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH,wBAAgB,aAAa,CAAC,CAAC;kBAEb,CAAC;WASR,CAAC;;EA2CX"}
@@ -46,6 +46,17 @@ export function createContext() {
46
46
  }
47
47
  return contextValue;
48
48
  },
49
+ hasValue() {
50
+ let currentComponent = getCurrentComponent();
51
+ if (!currentComponent) {
52
+ throw new Error("You can not get context outside component setup");
53
+ }
54
+ if (typeof currentComponent.context.getContext !== "function") {
55
+ return false;
56
+ }
57
+ const contextValue = currentComponent.context.getContext(context);
58
+ return Boolean(contextValue);
59
+ },
49
60
  };
50
61
  return context;
51
62
  }
@@ -1,2 +1,2 @@
1
- export declare function createEffect(cb: () => void): void;
1
+ export declare function createEffect(cb: () => void | (() => void)): void;
2
2
  //# sourceMappingURL=createEffect.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAGA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,QAwB1C"}
1
+ {"version":3,"file":"createEffect.d.ts","sourceRoot":"","sources":["../src/createEffect.ts"],"names":[],"mappings":"AAIA,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,QA+BzD"}
@@ -1,3 +1,4 @@
1
+ import { syncBatch } from "./batch";
1
2
  import { getCurrentComponent, createCleanup } from "./component";
2
3
  import { Observer } from "./observation";
3
4
  export function createEffect(cb) {
@@ -8,18 +9,27 @@ export function createEffect(cb) {
8
9
  catch {
9
10
  currentComponent = undefined;
10
11
  }
12
+ let disposer;
11
13
  const observer = new Observer(() => {
12
- // We trigger effects on micro task as synchronous observer notifications
13
- // (Like when components sets props) should not synchronously trigger effects
14
- queueMicrotask(runEffect);
14
+ console.log("FIRED");
15
+ syncBatch(runEffect);
15
16
  });
16
17
  const runEffect = () => {
18
+ try {
19
+ disposer?.();
20
+ }
21
+ catch (error) {
22
+ console.error("Error in effect dispose function:", error);
23
+ }
17
24
  const stopObserving = observer.observe();
18
- cb();
25
+ disposer = cb();
19
26
  stopObserving();
20
27
  };
21
28
  if (currentComponent) {
22
- createCleanup(() => observer.dispose());
29
+ createCleanup(() => {
30
+ observer.dispose();
31
+ disposer?.();
32
+ });
23
33
  }
24
34
  runEffect();
25
35
  }
@@ -0,0 +1,8 @@
1
+ import { RoutesConfig, TRouter, TRoutes } from "typed-client-router";
2
+ export type Router<T extends RoutesConfig> = Omit<TRouter<T>, "current" | "listen" | "pathname"> & {
3
+ route?: TRoutes<T>;
4
+ };
5
+ export declare function createRouter<const T extends RoutesConfig>(config: T, options?: {
6
+ base?: string;
7
+ }): Router<T>;
8
+ //# sourceMappingURL=createRouter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createRouter.d.ts","sourceRoot":"","sources":["../src/createRouter.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,YAAY,EACZ,OAAO,EACP,OAAO,EACR,MAAM,qBAAqB,CAAC;AAI7B,MAAM,MAAM,MAAM,CAAC,CAAC,SAAS,YAAY,IAAI,IAAI,CAC/C,OAAO,CAAC,CAAC,CAAC,EACV,SAAS,GAAG,QAAQ,GAAG,UAAU,CAClC,GAAG;IACF,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAgB,YAAY,CAAC,KAAK,CAAC,CAAC,SAAS,YAAY,EACvD,MAAM,EAAE,CAAC,EACT,OAAO,CAAC,EAAE;IACR,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GACA,MAAM,CAAC,CAAC,CAAC,CAwBX"}