fusee-framework 1.6.0 → 1.8.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 (129) hide show
  1. package/README.md +145 -123
  2. package/app/benchmark/benchmark.js +633 -0
  3. package/app/benchmark/index.html +102 -0
  4. package/app/benchmark/styles.css +249 -0
  5. package/app/components/Counter.js +17 -7
  6. package/app/components/CounterSection.js +3 -4
  7. package/app/main.js +50 -62
  8. package/app/main.ts +78 -63
  9. package/app/pages/404.js +16 -0
  10. package/app/pages/[...all].js +35 -0
  11. package/app/pages/[...all].ts +35 -0
  12. package/app/pages/_layout.js +53 -0
  13. package/app/pages/_layout.ts +26 -0
  14. package/app/pages/about.js +23 -0
  15. package/app/pages/about.ts +29 -0
  16. package/app/pages/async-work.js +65 -0
  17. package/app/pages/composables.js +158 -0
  18. package/app/pages/composables.ts +158 -0
  19. package/app/pages/dynamic.js +40 -0
  20. package/app/pages/dynamic.ts +40 -0
  21. package/app/pages/index.js +137 -0
  22. package/app/pages/index.ts +87 -0
  23. package/app/pages/login.ts +21 -0
  24. package/app/pages/objects/[id].js +56 -0
  25. package/app/pages/objects/[id].ts +56 -0
  26. package/app/pages/posts/[category]/[slug].js +56 -0
  27. package/app/pages/posts/[category]/[slug].ts +56 -0
  28. package/app/pages/router-example.js +79 -0
  29. package/app/pages/router-example.ts +56 -0
  30. package/app/pages/scroll-demo.ts +102 -0
  31. package/app/pages/test-for.js +65 -0
  32. package/app/pages/test-for.ts +80 -0
  33. package/app/pages/users/[id].js +24 -0
  34. package/app/pages/users/[id].ts +46 -0
  35. package/app/pages/users/index.js +22 -0
  36. package/app/pages/users/index.ts +29 -0
  37. package/app/pages/users.js +43 -0
  38. package/app/pages/users.ts +78 -0
  39. package/app/providers/ChildComponent.js +8 -2
  40. package/app/providers/ChildComponent.ts +47 -47
  41. package/app/providers/GrandchildComponent.js +19 -0
  42. package/app/providers/ParentComponent.js +5 -1
  43. package/app/routes/About.js +1 -1
  44. package/app/routes/About.ts +1 -1
  45. package/app/routes/AuthRoutes.ts +41 -0
  46. package/app/routes/Home.js +9 -0
  47. package/app/routes/NotFound.js +16 -0
  48. package/app/routes/NotFound.ts +16 -0
  49. package/app/routes/RouterExample.js +78 -0
  50. package/app/routes/RouterExample.ts +56 -0
  51. package/app/routes/TestResource.js +47 -0
  52. package/app/routes/WildcardDemo.js +35 -0
  53. package/app/routes/WildcardDemo.ts +35 -0
  54. package/app/services.js +15 -0
  55. package/auto-imports.d.ts +9 -0
  56. package/bin/create-fusee-linux +0 -0
  57. package/bin/create-fusee-mac-arm64 +0 -0
  58. package/bin/create-fusee-mac-intel +0 -0
  59. package/bin/create-fusee-win.exe +0 -0
  60. package/bin/create-fusee.exe +0 -0
  61. package/bin/index.js +38 -0
  62. package/create-fusee-go/cmd/generate.go +72 -0
  63. package/create-fusee-go/cmd/init.go +38 -0
  64. package/create-fusee-go/cmd/root.go +121 -0
  65. package/create-fusee-go/go.mod +10 -0
  66. package/create-fusee-go/go.sum +10 -0
  67. package/create-fusee-go/internal/assets/assets.go +78 -0
  68. package/create-fusee-go/internal/assets/embed/framework/auto-import-preset.js +9 -0
  69. package/create-fusee-go/internal/assets/embed/framework/core/compiler.js +253 -0
  70. package/create-fusee-go/internal/assets/embed/framework/core/component.js +266 -0
  71. package/create-fusee-go/internal/assets/embed/framework/core/composable.js +27 -0
  72. package/create-fusee-go/internal/assets/embed/framework/core/directives.js +875 -0
  73. package/create-fusee-go/internal/assets/embed/framework/core/evaluator.js +153 -0
  74. package/create-fusee-go/internal/assets/embed/framework/core/event-delegation.js +191 -0
  75. package/create-fusee-go/internal/assets/embed/framework/core/signal.js +412 -0
  76. package/create-fusee-go/internal/assets/embed/framework/core/store.js +81 -0
  77. package/create-fusee-go/internal/assets/embed/framework/index.d.ts +3 -0
  78. package/create-fusee-go/internal/assets/embed/framework/index.js +10 -0
  79. package/create-fusee-go/internal/assets/embed/framework/router/file-router.js +177 -0
  80. package/create-fusee-go/internal/assets/embed/framework/router/router.js +462 -0
  81. package/create-fusee-go/internal/assets/embed/framework/types/compiler.d.ts +15 -0
  82. package/create-fusee-go/internal/assets/embed/framework/types/component.d.ts +88 -0
  83. package/create-fusee-go/internal/assets/embed/framework/types/composable.d.ts +14 -0
  84. package/create-fusee-go/internal/assets/embed/framework/types/directives.d.ts +55 -0
  85. package/create-fusee-go/internal/assets/embed/framework/types/evaluator.d.ts +13 -0
  86. package/create-fusee-go/internal/assets/embed/framework/types/event-delegation.d.ts +32 -0
  87. package/create-fusee-go/internal/assets/embed/framework/types/index.d.ts +22 -0
  88. package/create-fusee-go/internal/assets/embed/framework/types/router.d.ts +32 -0
  89. package/create-fusee-go/internal/assets/embed/framework/types/signal.d.ts +79 -0
  90. package/create-fusee-go/internal/assets/embed/framework/types/store.d.ts +8 -0
  91. package/create-fusee-go/internal/assets/templates/Counter.tmpl +26 -0
  92. package/create-fusee-go/internal/assets/templates/component.tmpl +11 -0
  93. package/create-fusee-go/internal/assets/templates/components/Loading.tmpl +24 -0
  94. package/create-fusee-go/internal/assets/templates/index.html.tmpl +15 -0
  95. package/create-fusee-go/internal/assets/templates/main.tmpl +14 -0
  96. package/create-fusee-go/internal/assets/templates/package.json.tmpl +14 -0
  97. package/create-fusee-go/internal/assets/templates/page.tmpl +12 -0
  98. package/create-fusee-go/internal/assets/templates/pages/about.tmpl +20 -0
  99. package/create-fusee-go/internal/assets/templates/pages/index.tmpl +22 -0
  100. package/create-fusee-go/internal/assets/templates/pages/layout.tmpl +18 -0
  101. package/create-fusee-go/internal/assets/templates/tsconfig.json.tmpl +14 -0
  102. package/create-fusee-go/internal/assets/templates/vite.config.js.tmpl +18 -0
  103. package/create-fusee-go/main.go +7 -0
  104. package/framework/auto-import-preset.js +13 -9
  105. package/framework/core/compiler.js +252 -249
  106. package/framework/core/component.js +279 -260
  107. package/framework/core/di.js +187 -0
  108. package/framework/core/directives.js +819 -786
  109. package/framework/core/evaluator.js +153 -70
  110. package/framework/core/event-delegation.js +33 -14
  111. package/framework/core/reconcile.js +151 -0
  112. package/framework/core/signal.js +679 -403
  113. package/framework/core/store.js +322 -81
  114. package/framework/index.js +12 -11
  115. package/framework/router/file-router.js +177 -0
  116. package/framework/router/router.js +554 -80
  117. package/framework/types/component.d.ts +89 -88
  118. package/framework/types/di.d.ts +43 -0
  119. package/framework/types/event-delegation.d.ts +0 -58
  120. package/framework/types/index.d.ts +32 -22
  121. package/framework/types/reconcile.d.ts +7 -0
  122. package/framework/types/router.d.ts +42 -15
  123. package/framework/types/signal.d.ts +147 -78
  124. package/framework/types/store.d.ts +66 -8
  125. package/index.html +638 -626
  126. package/package.json +5 -3
  127. package/tsconfig.json +1 -0
  128. package/vite.config.js +7 -1
  129. /package/{framework → create-fusee-go/internal/assets/embed/framework}/bin/create-fusee.js +0 -0
@@ -1,403 +1,679 @@
1
- let currentEffect = null
2
- let onEffectCreated = null
3
-
4
- export function setEffectHook(fn) { onEffectCreated = fn }
5
-
6
- let batchDepth = 0
7
- const pendingEffects = new Set()
8
-
9
- export function batch(fn) {
10
- batchDepth++
11
- try {
12
- return fn()
13
- } finally {
14
- batchDepth--
15
- if (batchDepth === 0) {
16
- while (pendingEffects.size > 0) {
17
- const toRun = [...pendingEffects]
18
- pendingEffects.clear()
19
- for (const eff of toRun) eff()
20
- }
21
- }
22
- }
23
- }
24
-
25
- function scheduleEffect(effectFn) {
26
- if (batchDepth > 0) {
27
- pendingEffects.add(effectFn)
28
- } else {
29
- effectFn()
30
- }
31
- }
32
-
33
- export function signal(initialValue) {
34
- let value = initialValue
35
- const subscribers = new Set()
36
-
37
- function accessor() {
38
- if (arguments.length === 0) {
39
- if (currentEffect) {
40
- subscribers.add(currentEffect)
41
- currentEffect.deps.add(subscribers)
42
- }
43
- return value
44
- } else {
45
- const newValue = arguments[0]
46
- if (!Object.is(value, newValue)) {
47
- value = newValue
48
- for (const sub of [...subscribers]) scheduleEffect(sub)
49
- }
50
- }
51
- }
52
-
53
- accessor.isSignal = true
54
-
55
- if (Array.isArray(initialValue)) addMutatingArrayMethods(accessor)
56
- addReactiveArrayMethods(accessor)
57
-
58
- return accessor
59
- }
60
-
61
- export function effect(fn) {
62
- let active = true
63
- let running = false
64
- const nestedEffects = new Set()
65
-
66
- const run = () => {
67
- if (!active || running) {
68
- if (running) console.warn('[framework] Recursive effect detected')
69
- return
70
- }
71
-
72
- running = true
73
-
74
- for (const childCleanup of nestedEffects) {
75
- childCleanup()
76
- }
77
- nestedEffects.clear()
78
-
79
- for (const dep of run.deps) dep.delete(run)
80
- run.deps.clear()
81
-
82
- const prevEffect = currentEffect
83
- const prevOnEffectCreated = onEffectCreated
84
-
85
- onEffectCreated = (childCleanup) => {
86
- nestedEffects.add(childCleanup)
87
- if (prevOnEffectCreated) prevOnEffectCreated(childCleanup)
88
- }
89
-
90
- currentEffect = run
91
- try { fn() }
92
- finally {
93
- currentEffect = prevEffect
94
- onEffectCreated = prevOnEffectCreated
95
- running = false
96
- }
97
- }
98
-
99
- run.deps = new Set()
100
-
101
- const cleanup = () => {
102
- active = false
103
- pendingEffects.delete(run)
104
- for (const dep of run.deps) dep.delete(run)
105
- run.deps.clear()
106
-
107
- for (const childCleanup of nestedEffects) {
108
- childCleanup()
109
- }
110
- nestedEffects.clear()
111
- }
112
-
113
- if (onEffectCreated) onEffectCreated(cleanup)
114
- run()
115
-
116
- return cleanup
117
- }
118
-
119
- export function computed(fn) {
120
- let value
121
- let dirty = true
122
- let active = true
123
- const subscribers = new Set()
124
- const deps = new Set()
125
-
126
- const computedNode = () => {
127
- if (!active || dirty) return
128
-
129
- if (subscribers.size > 0) {
130
- if (!runRecompute()) return
131
- } else {
132
- dirty = true
133
- for (const d of deps) d.delete(computedNode)
134
- deps.clear()
135
- return
136
- }
137
-
138
- for (const sub of [...subscribers]) scheduleEffect(sub)
139
- }
140
-
141
- computedNode.deps = deps
142
-
143
- let computing = false
144
-
145
- function runRecompute() {
146
- if (computing) {
147
- console.warn('[framework] Circular dependency detected in computed()')
148
- return false
149
- }
150
-
151
- computing = true
152
- for (const d of deps) d.delete(computedNode)
153
- deps.clear()
154
-
155
- const prevEffect = currentEffect
156
- currentEffect = active ? computedNode : null
157
- try {
158
- const newValue = fn()
159
- const changed = !Object.is(newValue, value)
160
- value = newValue
161
- dirty = false
162
- return changed
163
- } finally {
164
- currentEffect = prevEffect
165
- computing = false
166
- }
167
- }
168
-
169
- function accessor() {
170
- if (arguments.length === 0) {
171
- if (active && currentEffect) {
172
- subscribers.add(currentEffect)
173
- currentEffect.deps.add(subscribers)
174
- }
175
-
176
- if (dirty) {
177
- runRecompute()
178
- }
179
-
180
- return value
181
- }
182
- console.warn('[framework] computed() is read-only')
183
- }
184
-
185
- accessor.dispose = () => {
186
- active = false
187
- dirty = true
188
- pendingEffects.delete(computedNode)
189
- for (const d of deps) d.delete(computedNode)
190
- deps.clear()
191
- subscribers.clear()
192
- }
193
-
194
- accessor.isSignal = true
195
- accessor.readonly = true
196
- addReactiveArrayMethods(accessor)
197
-
198
- return accessor
199
- }
200
-
201
- export function untrack(fn) {
202
- const prevEffect = currentEffect
203
- currentEffect = null
204
- try {
205
- return fn()
206
- } finally {
207
- currentEffect = prevEffect
208
- }
209
- }
210
-
211
- export function watch(source, callback, options = {}) {
212
- if (typeof callback !== 'function') {
213
- console.warn('[framework] watch() callback must be a function')
214
- return () => { }
215
- }
216
-
217
- const { immediate = false, equals = Object.is } = options
218
- const { getter, isMultiSource } = normalizeWatchSource(source)
219
-
220
- let initialized = false
221
- let oldValue
222
- const cleanupRef = { fn: null }
223
-
224
- const onCleanup = (fn) => {
225
- if (typeof fn !== 'function') {
226
- console.warn('[framework] onCleanup() expects a function')
227
- return
228
- }
229
- cleanupRef.fn = fn
230
- }
231
-
232
- const stopEffect = effect(() => {
233
- const newValue = getter()
234
-
235
- if (!initialized) {
236
- initialized = true
237
- oldValue = cloneWatchValue(newValue, isMultiSource)
238
- if (immediate) {
239
- untrack(() => callback(newValue, undefined, onCleanup))
240
- }
241
- return
242
- }
243
-
244
- if (!hasWatchChanged(newValue, oldValue, isMultiSource, equals)) {
245
- return
246
- }
247
-
248
- const previousValue = oldValue
249
- oldValue = cloneWatchValue(newValue, isMultiSource)
250
-
251
- runWatchCleanup(cleanupRef)
252
- untrack(() => callback(newValue, previousValue, onCleanup))
253
- })
254
-
255
- return () => {
256
- stopEffect()
257
- runWatchCleanup(cleanupRef)
258
- }
259
- }
260
-
261
- export function inspect(...args) {
262
- const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
263
- if (!isDev) return
264
-
265
- return effect(() => {
266
- const values = args.map(arg => {
267
- if (typeof arg === 'function') {
268
- return arg()
269
- }
270
- return arg
271
- })
272
- console.log('[inspect]', ...values)
273
- })
274
- }
275
-
276
- function addReactiveArrayMethods(accessor) {
277
- // Transformations
278
- accessor.map = (fn) => computed(() => accessor()?.map?.(fn) ?? [])
279
- accessor.filter = (fn) => computed(() => accessor()?.filter?.(fn) ?? [])
280
- accessor.slice = (...args) => computed(() => accessor()?.slice?.(...args) ?? [])
281
- accessor.concat = (...args) => computed(() => accessor()?.concat?.(...args) ?? [])
282
- accessor.flat = (depth) => computed(() => accessor()?.flat?.(depth) ?? [])
283
- accessor.flatMap = (fn) => computed(() => accessor()?.flatMap?.(fn) ?? [])
284
-
285
- // Searches
286
- accessor.find = (fn) => computed(() => accessor()?.find?.(fn))
287
- accessor.findLast = (fn) => computed(() => accessor()?.findLast?.(fn))
288
- accessor.findIndex = (fn) => computed(() => accessor()?.findIndex?.(fn))
289
- accessor.findLastIndex = (fn) => computed(() => accessor()?.findLastIndex?.(fn))
290
- accessor.indexOf = (searchElement, fromIdx) => computed(() => accessor()?.indexOf?.(searchElement, fromIdx))
291
- accessor.lastIndexOf = (searchElement, fromIdx) => computed(() => accessor()?.lastIndexOf?.(searchElement, fromIdx))
292
- accessor.includes = (searchElement, fromIdx) => computed(() => accessor()?.includes?.(searchElement, fromIdx))
293
-
294
- // Validations
295
- accessor.every = (fn) => computed(() => accessor()?.every?.(fn))
296
- accessor.some = (fn) => computed(() => accessor()?.some?.(fn))
297
-
298
- // Accumulators & Access
299
- accessor.reduce = (...args) => computed(() => accessor()?.reduce?.(...args))
300
- accessor.at = (index) => computed(() => accessor()?.at?.(index))
301
- accessor.join = (separator) => computed(() => accessor()?.join?.(separator) ?? '')
302
- }
303
-
304
- function addMutatingArrayMethods(accessor) {
305
- accessor.push = (...items) => {
306
- const next = [...accessor(), ...items]
307
- accessor(next)
308
- return next.length
309
- }
310
-
311
- accessor.pop = () => {
312
- const arr = accessor()
313
- if (arr.length === 0) return undefined
314
- const item = arr[arr.length - 1]
315
- accessor(arr.slice(0, -1))
316
- return item
317
- }
318
-
319
- accessor.shift = () => {
320
- const arr = accessor()
321
- if (arr.length === 0) return undefined
322
- const item = arr[0]
323
- accessor(arr.slice(1))
324
- return item
325
- }
326
-
327
- accessor.unshift = (...items) => {
328
- const next = [...items, ...accessor()]
329
- accessor(next)
330
- return next.length
331
- }
332
-
333
- accessor.splice = (start, deleteCount, ...items) => {
334
- const arr = [...accessor()]
335
- const removed = arr.splice(start, deleteCount, ...items)
336
- accessor(arr)
337
- return removed
338
- }
339
-
340
- accessor.remove = (predicate) => {
341
- accessor(accessor().filter((item, i) => !predicate(item, i)))
342
- }
343
-
344
- accessor.clear = () => {
345
- accessor([])
346
- }
347
-
348
- accessor.sort = (compareFn) => {
349
- const next = [...accessor()].sort(compareFn)
350
- accessor(next)
351
- return accessor
352
- }
353
-
354
- accessor.reverse = () => {
355
- const next = [...accessor()].reverse()
356
- accessor(next)
357
- return accessor
358
- }
359
- }
360
-
361
- function normalizeWatchSource(source) {
362
- if (Array.isArray(source)) {
363
- const getters = source.map(normalizeSingleWatchSource)
364
- return {
365
- getter: () => getters.map(get => get()),
366
- isMultiSource: true
367
- }
368
- }
369
- return {
370
- getter: normalizeSingleWatchSource(source),
371
- isMultiSource: false
372
- }
373
- }
374
-
375
- function normalizeSingleWatchSource(source) {
376
- if (typeof source === 'function') return source
377
- console.warn('[framework] watch() source should be a signal, getter, or an array of those')
378
- return () => source
379
- }
380
-
381
- function cloneWatchValue(value, isMultiSource) {
382
- if (isMultiSource && Array.isArray(value)) return value.slice()
383
- return value
384
- }
385
-
386
- function hasWatchChanged(newValue, oldValue, isMultiSource, equals) {
387
- if (isMultiSource) {
388
- if (!Array.isArray(oldValue) || newValue.length !== oldValue.length) return true
389
- for (let i = 0; i < newValue.length; i++) {
390
- if (!equals(newValue[i], oldValue[i])) return true
391
- }
392
- return false
393
- }
394
- return !equals(newValue, oldValue)
395
- }
396
-
397
- function runWatchCleanup(cleanupRef) {
398
- if (typeof cleanupRef.fn === 'function') {
399
- const fn = cleanupRef.fn
400
- cleanupRef.fn = null
401
- untrack(() => fn())
402
- }
403
- }
1
+ let currentEffect = null
2
+ let currentOwner = null
3
+ let onEffectCreated = null
4
+
5
+ export function setEffectHook(fn) { onEffectCreated = fn }
6
+
7
+ export function onCleanup(fn) {
8
+ if (!currentOwner) return
9
+ currentOwner.cleanups.add(fn)
10
+ }
11
+
12
+ let batchDepth = 0
13
+ const pendingEffects = new Set()
14
+
15
+ export function batch(fn) {
16
+ batchDepth++
17
+ try {
18
+ return fn()
19
+ } finally {
20
+ batchDepth--
21
+ if (batchDepth === 0) {
22
+ while (pendingEffects.size > 0) {
23
+ const toRun = [...pendingEffects]
24
+ pendingEffects.clear()
25
+ for (const eff of toRun) eff()
26
+ }
27
+ }
28
+ }
29
+ }
30
+
31
+ const asyncJobQueueHigh = []
32
+ const asyncJobQueueLow = []
33
+ let asyncJobFlushScheduled = false
34
+
35
+ function flushAsyncJobs() {
36
+ asyncJobFlushScheduled = false
37
+
38
+ while (asyncJobQueueHigh.length || asyncJobQueueLow.length) {
39
+ const job = asyncJobQueueHigh.shift() || asyncJobQueueLow.shift()
40
+ try {
41
+ job()
42
+ } catch (e) {
43
+ console.warn('[framework] Async job error:', e)
44
+ }
45
+ }
46
+ }
47
+
48
+ export function scheduleAsyncJob(fn, opts = {}) {
49
+ const priority = opts.priority || 'high'
50
+ if (priority === 'low') asyncJobQueueLow.push(fn)
51
+ else asyncJobQueueHigh.push(fn)
52
+
53
+ if (!asyncJobFlushScheduled) {
54
+ asyncJobFlushScheduled = true
55
+ setTimeout(flushAsyncJobs, 0)
56
+ }
57
+ }
58
+
59
+ function scheduleEffect(effectFn) {
60
+ if (batchDepth > 0) {
61
+ pendingEffects.add(effectFn)
62
+ } else {
63
+ effectFn()
64
+ }
65
+ }
66
+
67
+ export function signal(initialValue) {
68
+ let value = initialValue
69
+ const subscribers = new Set()
70
+
71
+ function accessor() {
72
+ if (arguments.length === 0) {
73
+ if (currentEffect) {
74
+ subscribers.add(currentEffect)
75
+ currentEffect.deps.add(subscribers)
76
+ }
77
+ return value
78
+ } else {
79
+ const newValue = arguments[0]
80
+ if (!Object.is(value, newValue)) {
81
+ value = newValue
82
+ for (const sub of [...subscribers]) scheduleEffect(sub)
83
+ }
84
+ }
85
+ }
86
+
87
+ accessor.isSignal = true
88
+
89
+ if (Array.isArray(initialValue)) addMutatingArrayMethods(accessor)
90
+ addReactiveArrayMethods(accessor)
91
+
92
+ return accessor
93
+ }
94
+
95
+ export function effect(fn) {
96
+ let active = true
97
+ let running = false
98
+
99
+ const owner = {
100
+ children: new Set(),
101
+ cleanups: new Set()
102
+ }
103
+
104
+ const run = () => {
105
+ if (!active || running) {
106
+ if (running) console.warn('[framework] Recursive effect detected')
107
+ return
108
+ }
109
+
110
+ running = true
111
+
112
+ for (const dep of run.deps) dep.delete(run)
113
+ run.deps.clear()
114
+
115
+ const prevEffect = currentEffect
116
+ const prevOwner = currentOwner
117
+
118
+ currentEffect = run
119
+ currentOwner = owner
120
+
121
+ try { fn() }
122
+ finally {
123
+ currentEffect = prevEffect
124
+ currentOwner = prevOwner
125
+ running = false
126
+ }
127
+ }
128
+
129
+ run.deps = new Set()
130
+
131
+ const cleanup = () => {
132
+ if (!active) return
133
+ active = false
134
+
135
+ pendingEffects.delete(run)
136
+
137
+ for (const dep of run.deps) dep.delete(run)
138
+ run.deps.clear()
139
+
140
+ for (const child of owner.children) child()
141
+ owner.children.clear()
142
+
143
+ for (const fn of owner.cleanups) fn()
144
+ owner.cleanups.clear()
145
+ }
146
+
147
+ if (onEffectCreated) onEffectCreated(cleanup)
148
+ if (currentOwner) {
149
+ currentOwner.children.add(cleanup)
150
+ }
151
+
152
+ run()
153
+ return cleanup
154
+ }
155
+
156
+ export function computed(fn) {
157
+ let value
158
+ let dirty = true
159
+ let active = true
160
+ const subscribers = new Set()
161
+ const deps = new Set()
162
+
163
+ const computedNode = () => {
164
+ if (!active || dirty) return
165
+
166
+ if (subscribers.size > 0) {
167
+ if (!runRecompute()) return
168
+ } else {
169
+ dirty = true
170
+ for (const d of deps) d.delete(computedNode)
171
+ deps.clear()
172
+ return
173
+ }
174
+
175
+ for (const sub of [...subscribers]) scheduleEffect(sub)
176
+ }
177
+
178
+ computedNode.deps = deps
179
+
180
+ let computing = false
181
+
182
+ function runRecompute() {
183
+ if (computing) {
184
+ console.warn('[framework] Circular dependency detected in computed()')
185
+ return false
186
+ }
187
+
188
+ computing = true
189
+ for (const d of deps) d.delete(computedNode)
190
+ deps.clear()
191
+
192
+ const prevEffect = currentEffect
193
+ currentEffect = active ? computedNode : null
194
+ try {
195
+ const newValue = fn()
196
+ const changed = !Object.is(newValue, value)
197
+ value = newValue
198
+ dirty = false
199
+ return changed
200
+ } finally {
201
+ currentEffect = prevEffect
202
+ computing = false
203
+ }
204
+ }
205
+
206
+ function accessor() {
207
+ if (arguments.length === 0) {
208
+ if (active && currentEffect) {
209
+ subscribers.add(currentEffect)
210
+ currentEffect.deps.add(subscribers)
211
+ }
212
+
213
+ if (dirty) {
214
+ runRecompute()
215
+ }
216
+
217
+ return value
218
+ }
219
+ console.warn('[framework] computed() is read-only')
220
+ }
221
+
222
+ accessor.dispose = () => {
223
+ active = false
224
+ dirty = true
225
+ pendingEffects.delete(computedNode)
226
+ for (const d of deps) d.delete(computedNode)
227
+ deps.clear()
228
+ subscribers.clear()
229
+ }
230
+
231
+ accessor.isSignal = true
232
+ accessor.readonly = true
233
+ addReactiveArrayMethods(accessor)
234
+
235
+ return accessor
236
+ }
237
+
238
+ export function untrack(fn) {
239
+ const prevEffect = currentEffect
240
+ currentEffect = null
241
+ try {
242
+ return fn()
243
+ } finally {
244
+ currentEffect = prevEffect
245
+ }
246
+ }
247
+
248
+ export function watch(source, callback, options = {}) {
249
+ if (typeof callback !== 'function') {
250
+ console.warn('[framework] watch() callback must be a function')
251
+ return () => { }
252
+ }
253
+
254
+ const { immediate = false, equals = Object.is } = options
255
+ const { getter, isMultiSource } = normalizeWatchSource(source)
256
+
257
+ let initialized = false
258
+ let oldValue
259
+ const cleanupRef = { fn: null }
260
+
261
+ const onCleanup = (fn) => {
262
+ if (typeof fn !== 'function') {
263
+ console.warn('[framework] onCleanup() expects a function')
264
+ return
265
+ }
266
+ cleanupRef.fn = fn
267
+ }
268
+
269
+ const stopEffect = effect(() => {
270
+ const newValue = getter()
271
+
272
+ if (!initialized) {
273
+ initialized = true
274
+ oldValue = cloneWatchValue(newValue, isMultiSource)
275
+ if (immediate) {
276
+ untrack(() => callback(newValue, undefined, onCleanup))
277
+ }
278
+ return
279
+ }
280
+
281
+ if (!hasWatchChanged(newValue, oldValue, isMultiSource, equals)) {
282
+ return
283
+ }
284
+
285
+ const previousValue = oldValue
286
+ oldValue = cloneWatchValue(newValue, isMultiSource)
287
+
288
+ runWatchCleanup(cleanupRef)
289
+ untrack(() => callback(newValue, previousValue, onCleanup))
290
+ })
291
+
292
+ return () => {
293
+ stopEffect()
294
+ runWatchCleanup(cleanupRef)
295
+ }
296
+ }
297
+
298
+ export function inspect(...args) {
299
+ const isDev = typeof import.meta.env !== 'undefined' ? !!import.meta.env.DEV : true
300
+ if (!isDev) return
301
+
302
+ return effect(() => {
303
+ const values = args.map(arg => {
304
+ if (typeof arg === 'function') {
305
+ return arg()
306
+ }
307
+ return arg
308
+ })
309
+ console.log('[inspect]', ...values)
310
+ })
311
+ }
312
+
313
+ function addReactiveArrayMethods(accessor) {
314
+ // Transformations
315
+ accessor.map = (fn) => computed(() => accessor()?.map?.(fn) ?? [])
316
+ accessor.filter = (fn) => computed(() => accessor()?.filter?.(fn) ?? [])
317
+ accessor.slice = (...args) => computed(() => accessor()?.slice?.(...args) ?? [])
318
+ accessor.concat = (...args) => computed(() => accessor()?.concat?.(...args) ?? [])
319
+ accessor.flat = (depth) => computed(() => accessor()?.flat?.(depth) ?? [])
320
+ accessor.flatMap = (fn) => computed(() => accessor()?.flatMap?.(fn) ?? [])
321
+
322
+ // Searches
323
+ accessor.find = (fn) => computed(() => accessor()?.find?.(fn))
324
+ accessor.findLast = (fn) => computed(() => accessor()?.findLast?.(fn))
325
+ accessor.findIndex = (fn) => computed(() => accessor()?.findIndex?.(fn))
326
+ accessor.findLastIndex = (fn) => computed(() => accessor()?.findLastIndex?.(fn))
327
+ accessor.indexOf = (searchElement, fromIdx) => computed(() => accessor()?.indexOf?.(searchElement, fromIdx))
328
+ accessor.lastIndexOf = (searchElement, fromIdx) => computed(() => accessor()?.lastIndexOf?.(searchElement, fromIdx))
329
+ accessor.includes = (searchElement, fromIdx) => computed(() => accessor()?.includes?.(searchElement, fromIdx))
330
+
331
+ // Validations
332
+ accessor.every = (fn) => computed(() => accessor()?.every?.(fn))
333
+ accessor.some = (fn) => computed(() => accessor()?.some?.(fn))
334
+
335
+ // Accumulators & Access
336
+ accessor.reduce = (...args) => computed(() => accessor()?.reduce?.(...args))
337
+ accessor.at = (index) => computed(() => accessor()?.at?.(index))
338
+ accessor.join = (separator) => computed(() => accessor()?.join?.(separator) ?? '')
339
+ }
340
+
341
+ function addMutatingArrayMethods(accessor) {
342
+ accessor.push = (...items) => {
343
+ const next = [...accessor(), ...items]
344
+ accessor(next)
345
+ return next.length
346
+ }
347
+
348
+ accessor.pop = () => {
349
+ const arr = accessor()
350
+ if (arr.length === 0) return undefined
351
+ const item = arr[arr.length - 1]
352
+ accessor(arr.slice(0, -1))
353
+ return item
354
+ }
355
+
356
+ accessor.shift = () => {
357
+ const arr = accessor()
358
+ if (arr.length === 0) return undefined
359
+ const item = arr[0]
360
+ accessor(arr.slice(1))
361
+ return item
362
+ }
363
+
364
+ accessor.unshift = (...items) => {
365
+ const next = [...items, ...accessor()]
366
+ accessor(next)
367
+ return next.length
368
+ }
369
+
370
+ accessor.splice = (start, deleteCount, ...items) => {
371
+ const arr = [...accessor()]
372
+ const removed = arr.splice(start, deleteCount, ...items)
373
+ accessor(arr)
374
+ return removed
375
+ }
376
+
377
+ accessor.remove = (predicate) => {
378
+ accessor(accessor().filter((item, i) => !predicate(item, i)))
379
+ }
380
+
381
+ accessor.clear = () => {
382
+ accessor([])
383
+ }
384
+
385
+ accessor.sort = (compareFn) => {
386
+ const next = [...accessor()].sort(compareFn)
387
+ accessor(next)
388
+ return accessor
389
+ }
390
+
391
+ accessor.reverse = () => {
392
+ const next = [...accessor()].reverse()
393
+ accessor(next)
394
+ return accessor
395
+ }
396
+ }
397
+
398
+ function normalizeWatchSource(source) {
399
+ if (Array.isArray(source)) {
400
+ const getters = source.map(normalizeSingleWatchSource)
401
+ return {
402
+ getter: () => getters.map(get => get()),
403
+ isMultiSource: true
404
+ }
405
+ }
406
+ return {
407
+ getter: normalizeSingleWatchSource(source),
408
+ isMultiSource: false
409
+ }
410
+ }
411
+
412
+ function normalizeSingleWatchSource(source) {
413
+ if (typeof source === 'function') return source
414
+ console.warn('[framework] watch() source should be a signal, getter, or an array of those')
415
+ return () => source
416
+ }
417
+
418
+ function cloneWatchValue(value, isMultiSource) {
419
+ if (isMultiSource && Array.isArray(value)) return value.slice()
420
+ return value
421
+ }
422
+
423
+ function hasWatchChanged(newValue, oldValue, isMultiSource, equals) {
424
+ if (isMultiSource) {
425
+ if (!Array.isArray(oldValue) || newValue.length !== oldValue.length) return true
426
+ for (let i = 0; i < newValue.length; i++) {
427
+ if (!equals(newValue[i], oldValue[i])) return true
428
+ }
429
+ return false
430
+ }
431
+ return !equals(newValue, oldValue)
432
+ }
433
+
434
+ const globalInFlightByKey = new Map()
435
+ const globalInFlightByFetcher = new WeakMap()
436
+
437
+ export function resource(sourceOrFetcher, fetcherOrOptions, optionsObj) {
438
+ let source = null
439
+ let actualFetcher = null
440
+ let options = {}
441
+
442
+ if (arguments.length === 1) {
443
+ actualFetcher = sourceOrFetcher
444
+ } else if (arguments.length === 2) {
445
+ if (typeof fetcherOrOptions === 'function') {
446
+ source = sourceOrFetcher
447
+ actualFetcher = fetcherOrOptions
448
+ } else {
449
+ actualFetcher = sourceOrFetcher
450
+ options = fetcherOrOptions || {}
451
+ }
452
+ } else if (arguments.length >= 3) {
453
+ source = sourceOrFetcher
454
+ actualFetcher = fetcherOrOptions
455
+ options = optionsObj || {}
456
+ }
457
+
458
+ const data = signal(undefined)
459
+ const loading = signal(false)
460
+ const isFetching = signal(false)
461
+ const error = signal(undefined)
462
+
463
+ let currentPromiseId = 0
464
+ let currentPromise = null
465
+ const cache = new Map()
466
+ const staleTime = typeof options.staleTime === 'number' ? options.staleTime : 0
467
+
468
+ function serializeKey(input) {
469
+ if (input === undefined) return 'undefined'
470
+ if (input === null) return 'null'
471
+ if (typeof input === 'object') return JSON.stringify(input)
472
+ return String(input)
473
+ }
474
+
475
+ function makeScheduledPromise(runFetcher) {
476
+ try {
477
+ return Promise.resolve(runFetcher())
478
+ } catch (e) {
479
+ return Promise.reject(e)
480
+ }
481
+ }
482
+
483
+ async function load(input, force = false) {
484
+ const key = serializeKey(input)
485
+ const cached = force ? null : cache.get(key)
486
+ const isStale = !cached || (Date.now() - cached.updatedAt > staleTime)
487
+
488
+ if (cached) {
489
+ batch(() => {
490
+ data(cached.data)
491
+ error(undefined)
492
+ loading(false)
493
+ })
494
+ if (!isStale && !force) return
495
+ batch(() => { isFetching(true) })
496
+ } else {
497
+ batch(() => {
498
+ loading(true)
499
+ isFetching(true)
500
+ error(undefined)
501
+ })
502
+ }
503
+
504
+ const id = ++currentPromiseId
505
+
506
+ batch(() => {
507
+ isFetching(true)
508
+ })
509
+
510
+ let promise
511
+
512
+ if (options.key !== undefined) {
513
+ const dedupeKey = options.key ? `key_${options.key}_${key}` : `fn_${actualFetcher.name}_${key}`;
514
+ promise = globalInFlightByKey.get(dedupeKey)
515
+
516
+ if (!promise) {
517
+ promise = makeScheduledPromise(() => actualFetcher(input))
518
+ globalInFlightByKey.set(dedupeKey, promise)
519
+
520
+ promise.finally(() => {
521
+ if (globalInFlightByKey.get(dedupeKey) === promise) {
522
+ globalInFlightByKey.delete(dedupeKey)
523
+ }
524
+ }).catch(() => {})
525
+ }
526
+ } else {
527
+ let inFlightMap = globalInFlightByFetcher.get(actualFetcher)
528
+ if (!inFlightMap) {
529
+ inFlightMap = new Map()
530
+ globalInFlightByFetcher.set(actualFetcher, inFlightMap)
531
+ }
532
+
533
+ promise = inFlightMap.get(key)
534
+ if (!promise) {
535
+ promise = makeScheduledPromise(() => actualFetcher(input))
536
+ inFlightMap.set(key, promise)
537
+
538
+ promise.finally(() => {
539
+ if (inFlightMap.get(key) === promise) {
540
+ inFlightMap.delete(key)
541
+ }
542
+ }).catch(() => {})
543
+ }
544
+ }
545
+
546
+ currentPromise = promise
547
+
548
+ try {
549
+ const result = await promise
550
+
551
+ if (id !== currentPromiseId) return
552
+
553
+ cache.set(key, { data: result, updatedAt: Date.now() })
554
+
555
+ batch(() => {
556
+ data(result)
557
+ loading(false)
558
+ isFetching(false)
559
+ })
560
+
561
+ currentPromise = null
562
+ } catch (err) {
563
+ if (id !== currentPromiseId) return
564
+
565
+ batch(() => {
566
+ error(err)
567
+ loading(false)
568
+ isFetching(false)
569
+ })
570
+
571
+ currentPromise = null
572
+ }
573
+ }
574
+
575
+ if (source) {
576
+ effect(() => {
577
+ const input = typeof source === 'function' ? source() : source
578
+ if (input !== null && input !== false && input !== undefined) {
579
+ load(input)
580
+ }
581
+ })
582
+ } else {
583
+ load()
584
+ }
585
+
586
+ const accessor = () => data()
587
+ accessor.isSignal = true
588
+ accessor.loading = loading
589
+ accessor.isFetching = isFetching
590
+ accessor.error = error
591
+ accessor.read = () => {
592
+ if (loading() && currentPromise) {
593
+ throw currentPromise
594
+ }
595
+ const err = error()
596
+ if (err !== undefined) {
597
+ throw err
598
+ }
599
+ return data()
600
+ }
601
+
602
+ const mutate = (val) => {
603
+ const input = source ? (typeof source === 'function' ? source() : source) : undefined
604
+ const key = serializeKey(input)
605
+ cache.set(key, { data: val, updatedAt: Date.now() })
606
+ data(val)
607
+ }
608
+
609
+ const refetch = () => {
610
+ const input = source ? (typeof source === 'function' ? source() : source) : undefined
611
+ load(input, true)
612
+ }
613
+
614
+ return [accessor, { mutate, refetch }]
615
+ }
616
+
617
+ function runWatchCleanup(cleanupRef) {
618
+ if (typeof cleanupRef.fn === 'function') {
619
+ const fn = cleanupRef.fn
620
+ cleanupRef.fn = null
621
+ untrack(() => fn())
622
+ }
623
+ }
624
+
625
+ export function createSuspense(renderFn, fallbackFn, options = {}) {
626
+ const out = signal(undefined)
627
+ const pending = signal(false)
628
+ const errSig = signal(undefined)
629
+
630
+ const retryTick = signal(0)
631
+ let token = 0
632
+
633
+ const dispose = effect(() => {
634
+ retryTick()
635
+ const myToken = ++token
636
+ let canceled = false
637
+ onCleanup(() => { canceled = true })
638
+
639
+ try {
640
+ pending(false)
641
+ errSig(undefined)
642
+ const v = renderFn()
643
+ out(v)
644
+ } catch (e) {
645
+ if (e instanceof Promise) {
646
+ pending(true)
647
+ errSig(undefined)
648
+ out(typeof fallbackFn === 'function' ? fallbackFn() : fallbackFn)
649
+
650
+ e.then(
651
+ () => {
652
+ if (canceled) return
653
+ if (myToken !== token) return
654
+ retryTick(retryTick() + 1)
655
+ },
656
+ (err) => {
657
+ if (canceled) return
658
+ if (myToken !== token) return
659
+ errSig(err)
660
+ pending(false)
661
+ if (options.onError) options.onError(err)
662
+ retryTick(retryTick() + 1)
663
+ }
664
+ )
665
+
666
+ return
667
+ }
668
+
669
+ errSig(e)
670
+ pending(false)
671
+ if (options.onError) options.onError(e)
672
+ }
673
+ })
674
+
675
+ out.pending = pending
676
+ out.error = errSig
677
+
678
+ return [out, dispose]
679
+ }