@fictjs/runtime 0.1.0 → 0.2.1

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 (49) hide show
  1. package/dist/advanced.cjs +8 -8
  2. package/dist/advanced.d.cts +3 -3
  3. package/dist/advanced.d.ts +3 -3
  4. package/dist/advanced.js +3 -3
  5. package/dist/{chunk-YQ4IB7NC.cjs → chunk-7EAEROZ5.cjs} +7 -7
  6. package/dist/{chunk-YQ4IB7NC.cjs.map → chunk-7EAEROZ5.cjs.map} +1 -1
  7. package/dist/{chunk-CF3OHML2.js → chunk-7TPCESQS.js} +2 -2
  8. package/dist/{chunk-BWZFJXUI.js → chunk-FOLRR3NZ.js} +84 -10
  9. package/dist/chunk-FOLRR3NZ.js.map +1 -0
  10. package/dist/{chunk-7WAGAQLT.cjs → chunk-MWI3USXB.cjs} +84 -10
  11. package/dist/chunk-MWI3USXB.cjs.map +1 -0
  12. package/dist/{chunk-V62XZLDU.js → chunk-VVNMIER7.js} +2 -2
  13. package/dist/{chunk-V62XZLDU.js.map → chunk-VVNMIER7.js.map} +1 -1
  14. package/dist/{chunk-Q4EN6BXV.cjs → chunk-Z5WRKD7Y.cjs} +16 -16
  15. package/dist/{chunk-Q4EN6BXV.cjs.map → chunk-Z5WRKD7Y.cjs.map} +1 -1
  16. package/dist/{context-B7UYnfzM.d.ts → context-4woHo7-L.d.ts} +1 -1
  17. package/dist/{context-UXySaqI_.d.cts → context-9gFXOdJl.d.cts} +1 -1
  18. package/dist/{effect-Auji1rz9.d.cts → effect-ClARNUCc.d.cts} +23 -2
  19. package/dist/{effect-Auji1rz9.d.ts → effect-ClARNUCc.d.ts} +23 -2
  20. package/dist/index.cjs +51 -58
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +5 -5
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.dev.js +71 -28
  25. package/dist/index.dev.js.map +1 -1
  26. package/dist/index.js +15 -22
  27. package/dist/index.js.map +1 -1
  28. package/dist/internal.cjs +64 -42
  29. package/dist/internal.cjs.map +1 -1
  30. package/dist/internal.d.cts +4 -4
  31. package/dist/internal.d.ts +4 -4
  32. package/dist/internal.js +32 -10
  33. package/dist/internal.js.map +1 -1
  34. package/dist/{props-BfmSLuyp.d.cts → props-CBwuh35e.d.cts} +4 -4
  35. package/dist/{props-BBi8Tkks.d.ts → props-DAyeRPwH.d.ts} +4 -4
  36. package/dist/{scope-S6eAzBJZ.d.ts → scope-DvgMquEy.d.ts} +1 -1
  37. package/dist/{scope-DKYzWfTn.d.cts → scope-xmdo6lVU.d.cts} +1 -1
  38. package/package.json +1 -1
  39. package/src/binding.ts +58 -5
  40. package/src/context.ts +1 -1
  41. package/src/effect.ts +9 -2
  42. package/src/error-boundary.ts +9 -9
  43. package/src/lifecycle.ts +13 -3
  44. package/src/signal.ts +44 -4
  45. package/src/store.ts +42 -20
  46. package/src/suspense.ts +14 -15
  47. package/dist/chunk-7WAGAQLT.cjs.map +0 -1
  48. package/dist/chunk-BWZFJXUI.js.map +0 -1
  49. /package/dist/{chunk-CF3OHML2.js.map → chunk-7TPCESQS.js.map} +0 -0
package/src/signal.ts CHANGED
@@ -1,6 +1,13 @@
1
1
  import { beginFlushGuard, beforeEffectRunGuard, endFlushGuard } from './cycle-guard'
2
2
  import { getDevtoolsHook } from './devtools'
3
- import { registerRootCleanup } from './lifecycle'
3
+ import {
4
+ getCurrentRoot,
5
+ handleError,
6
+ handleSuspend,
7
+ registerRootCleanup,
8
+ type RootContext,
9
+ } from './lifecycle'
10
+ import type { SuspenseToken } from './types'
4
11
 
5
12
  const isDev =
6
13
  typeof __DEV__ !== 'undefined'
@@ -103,6 +110,8 @@ export interface EffectNode extends BaseNode {
103
110
  depsTail: Link | undefined
104
111
  /** Optional cleanup runner to be called before checkDirty */
105
112
  runCleanup?: () => void
113
+ /** Root context for error/suspense handling */
114
+ root?: RootContext
106
115
  /** Devtools ID */
107
116
  __id?: number | undefined
108
117
  }
@@ -808,7 +817,25 @@ function runEffect(e: EffectNode): void {
808
817
  inCleanup = false
809
818
  }
810
819
  }
811
- if (checkDirty(e.deps, e)) {
820
+ let isDirty = false
821
+ try {
822
+ isDirty = checkDirty(e.deps, e)
823
+ } catch (err) {
824
+ if (handleSuspend(err as SuspenseToken, e.root)) {
825
+ if (e.flags !== 0) {
826
+ e.flags = Watching
827
+ }
828
+ return
829
+ }
830
+ if (handleError(err, { source: 'effect' }, e.root)) {
831
+ if (e.flags !== 0) {
832
+ e.flags = Watching
833
+ }
834
+ return
835
+ }
836
+ throw err
837
+ }
838
+ if (isDirty) {
812
839
  ++cycle
813
840
  effectRunDevtools(e)
814
841
  e.depsTail = undefined
@@ -1031,7 +1058,7 @@ function computedOper<T>(this: ComputedNode<T>): T {
1031
1058
  * @returns An effect disposer function
1032
1059
  */
1033
1060
  export function effect(fn: () => void): EffectDisposer {
1034
- const e = {
1061
+ const e: EffectNode = {
1035
1062
  fn,
1036
1063
  subs: undefined,
1037
1064
  subsTail: undefined,
@@ -1040,6 +1067,10 @@ export function effect(fn: () => void): EffectDisposer {
1040
1067
  flags: WatchingRunning,
1041
1068
  __id: undefined as number | undefined,
1042
1069
  }
1070
+ const root = getCurrentRoot()
1071
+ if (root) {
1072
+ e.root = root
1073
+ }
1043
1074
 
1044
1075
  registerEffectDevtools(e)
1045
1076
 
@@ -1066,9 +1097,14 @@ export function effect(fn: () => void): EffectDisposer {
1066
1097
  * cleanup functions to access the previous values of signals.
1067
1098
  * @param fn - The effect function
1068
1099
  * @param cleanupRunner - Function to run cleanups before signal value commit
1100
+ * @param root - Root context for error/suspense handling (defaults to current root)
1069
1101
  * @returns An effect disposer function
1070
1102
  */
1071
- export function effectWithCleanup(fn: () => void, cleanupRunner: () => void): EffectDisposer {
1103
+ export function effectWithCleanup(
1104
+ fn: () => void,
1105
+ cleanupRunner: () => void,
1106
+ root?: RootContext,
1107
+ ): EffectDisposer {
1072
1108
  const e: EffectNode = {
1073
1109
  fn,
1074
1110
  subs: undefined,
@@ -1079,6 +1115,10 @@ export function effectWithCleanup(fn: () => void, cleanupRunner: () => void): Ef
1079
1115
  runCleanup: cleanupRunner,
1080
1116
  __id: undefined as number | undefined,
1081
1117
  }
1118
+ const resolvedRoot = root ?? getCurrentRoot()
1119
+ if (resolvedRoot) {
1120
+ e.root = resolvedRoot
1121
+ }
1082
1122
 
1083
1123
  registerEffectDevtools(e)
1084
1124
 
package/src/store.ts CHANGED
@@ -35,17 +35,17 @@ export function createStore<T extends object>(
35
35
  }
36
36
 
37
37
  // Map of target object -> Proxy
38
- const proxyCache = new WeakMap<object, any>()
38
+ const proxyCache = new WeakMap<object, unknown>()
39
39
  // Map of target object -> Map<key, Signal>
40
- const signalCache = new WeakMap<object, Map<string | symbol, SignalAccessor<any>>>()
40
+ const signalCache = new WeakMap<object, Map<string | symbol, SignalAccessor<unknown>>>()
41
41
 
42
42
  function wrap<T>(value: T): T {
43
43
  if (value === null || typeof value !== 'object') return value
44
- if ((value as any)[PROXY]) return value
44
+ if (Reflect.get(value, PROXY)) return value
45
45
 
46
- if (proxyCache.has(value)) return proxyCache.get(value)
46
+ if (proxyCache.has(value)) return proxyCache.get(value) as T
47
47
 
48
- const handler: ProxyHandler<any> = {
48
+ const handler: ProxyHandler<object> = {
49
49
  get(target, prop, receiver) {
50
50
  if (prop === PROXY) return true
51
51
  if (prop === TARGET) return target
@@ -74,6 +74,8 @@ function wrap<T>(value: T): T {
74
74
  set(target, prop, value, receiver) {
75
75
  if (prop === PROXY || prop === TARGET) return false
76
76
 
77
+ const isArrayLength = Array.isArray(target) && prop === 'length'
78
+ const oldLength = isArrayLength ? target.length : undefined
77
79
  const hadKey = Object.prototype.hasOwnProperty.call(target, prop)
78
80
  const oldValue = Reflect.get(target, prop, receiver)
79
81
  if (oldValue === value) return true
@@ -84,6 +86,23 @@ function wrap<T>(value: T): T {
84
86
  if (!hadKey) {
85
87
  trigger(target, ITERATE_KEY)
86
88
  }
89
+ if (isArrayLength) {
90
+ const nextLength = target.length
91
+ if (typeof oldLength === 'number' && nextLength < oldLength) {
92
+ const signals = signalCache.get(target)
93
+ if (signals) {
94
+ for (const key of signals.keys()) {
95
+ if (typeof key !== 'string') continue
96
+ const index = Number(key)
97
+ if (!Number.isInteger(index) || String(index) !== key) continue
98
+ if (index >= nextLength && index < oldLength) {
99
+ trigger(target, key)
100
+ }
101
+ }
102
+ }
103
+ }
104
+ trigger(target, ITERATE_KEY)
105
+ }
87
106
  }
88
107
  return result
89
108
  },
@@ -106,8 +125,8 @@ function wrap<T>(value: T): T {
106
125
  }
107
126
 
108
127
  function unwrap<T>(value: T): T {
109
- if (value && typeof value === 'object' && (value as any)[PROXY]) {
110
- return (value as any)[TARGET]
128
+ if (value && typeof value === 'object' && Reflect.get(value, PROXY)) {
129
+ return Reflect.get(value, TARGET) as T
111
130
  }
112
131
  return value
113
132
  }
@@ -143,14 +162,14 @@ function trigger(target: object, prop: string | symbol) {
143
162
  }
144
163
  }
145
164
 
146
- function getLastValue(target: any, prop: string | symbol) {
147
- return target[prop]
165
+ function getLastValue(target: object, prop: string | symbol) {
166
+ return Reflect.get(target, prop)
148
167
  }
149
168
 
150
169
  /**
151
170
  * Reconcile a store path with a new value (shallow merge/diff)
152
171
  */
153
- function reconcile(target: any, value: any) {
172
+ function reconcile(target: object, value: unknown) {
154
173
  if (target === value) return
155
174
  if (value === null || typeof value !== 'object') {
156
175
  throw new Error(
@@ -165,16 +184,19 @@ function reconcile(target: any, value: any) {
165
184
 
166
185
  const keys = new Set([...Object.keys(realTarget), ...Object.keys(realValue)])
167
186
  for (const key of keys) {
168
- if (realValue[key] === undefined && realTarget[key] !== undefined) {
187
+ const rTarget = realTarget as Record<string, unknown>
188
+ const rValue = realValue as Record<string, unknown>
189
+
190
+ if (rValue[key] === undefined && rTarget[key] !== undefined) {
169
191
  // deleted
170
- delete target[key] // Triggers proxy trap
171
- } else if (realTarget[key] !== realValue[key]) {
172
- target[key] = realValue[key] // Triggers proxy trap
192
+ delete (target as Record<string, unknown>)[key] // Triggers proxy trap
193
+ } else if (rTarget[key] !== rValue[key]) {
194
+ ;(target as Record<string, unknown>)[key] = rValue[key] // Triggers proxy trap
173
195
  }
174
196
  }
175
197
 
176
198
  // Fix array length if needed
177
- if (Array.isArray(target) && target.length !== realValue.length) {
199
+ if (Array.isArray(target) && Array.isArray(realValue) && target.length !== realValue.length) {
178
200
  target.length = realValue.length
179
201
  }
180
202
  }
@@ -190,13 +212,13 @@ function reconcile(target: any, value: any) {
190
212
  */
191
213
  export function createDiffingSignal<T extends object>(initialValue: T) {
192
214
  let currentValue = unwrap(initialValue)
193
- const signals = new Map<string | symbol, SignalAccessor<any>>()
215
+ const signals = new Map<string | symbol, SignalAccessor<unknown>>()
194
216
  let iterateSignal: SignalAccessor<number> | undefined
195
217
 
196
218
  const getPropSignal = (prop: string | symbol) => {
197
219
  let s = signals.get(prop)
198
220
  if (!s) {
199
- s = signal((currentValue as any)[prop])
221
+ s = signal(Reflect.get(currentValue as object, prop))
200
222
  signals.set(prop, s)
201
223
  }
202
224
  return s
@@ -250,7 +272,7 @@ export function createDiffingSignal<T extends object>(initialValue: T) {
250
272
  // Same ref update: re-evaluate all tracked signals
251
273
  // This is necessary for in-place mutations
252
274
  for (const [prop, s] of signals) {
253
- const newVal = (next as any)[prop]
275
+ const newVal = Reflect.get(next as object, prop)
254
276
  s(newVal)
255
277
  }
256
278
  updateIterate(next)
@@ -261,8 +283,8 @@ export function createDiffingSignal<T extends object>(initialValue: T) {
261
283
  // We only trigger signals for properties that exist in our cache (tracked)
262
284
  // and have changed.
263
285
  for (const [prop, s] of signals) {
264
- const oldVal = (prev as any)[prop]
265
- const newVal = (next as any)[prop]
286
+ const oldVal = Reflect.get(prev as object, prop)
287
+ const newVal = Reflect.get(next as object, prop)
266
288
  if (oldVal !== newVal) {
267
289
  s(newVal)
268
290
  }
package/src/suspense.ts CHANGED
@@ -49,7 +49,6 @@ const isThenable = (value: unknown): value is PromiseLike<unknown> =>
49
49
  typeof (value as PromiseLike<unknown>).then === 'function'
50
50
 
51
51
  export function Suspense(props: SuspenseProps): FictNode {
52
- const currentView = createSignal<FictNode | null>(props.children ?? null)
53
52
  const pending = createSignal(0)
54
53
  let resolvedOnce = false
55
54
  let epoch = 0
@@ -60,11 +59,6 @@ export function Suspense(props: SuspenseProps): FictNode {
60
59
  ? (props.fallback as (e?: unknown) => FictNode)(err)
61
60
  : props.fallback
62
61
 
63
- const switchView = (view: FictNode | null) => {
64
- currentView(view)
65
- renderView(view)
66
- }
67
-
68
62
  const renderView = (view: FictNode | null) => {
69
63
  if (cleanup) {
70
64
  cleanup()
@@ -88,8 +82,9 @@ export function Suspense(props: SuspenseProps): FictNode {
88
82
  // Suspended view: child threw a suspense token and was handled upstream.
89
83
  // Avoid replacing existing fallback content; tear down this attempt.
90
84
  const suspendedAttempt =
91
- nodes.length > 0 &&
92
- nodes.every(node => node instanceof Comment && (node as Comment).data === 'fict:suspend')
85
+ root.suspended ||
86
+ (nodes.length > 0 &&
87
+ nodes.every(node => node instanceof Comment && (node as Comment).data === 'fict:suspend'))
93
88
  if (suspendedAttempt) {
94
89
  popRoot(prev)
95
90
  destroyRoot(root)
@@ -101,7 +96,6 @@ export function Suspense(props: SuspenseProps): FictNode {
101
96
  }
102
97
  } catch (err) {
103
98
  popRoot(prev)
104
- flushOnMount(root)
105
99
  destroyRoot(root)
106
100
  if (!handleError(err, { source: 'render' }, hostRoot)) {
107
101
  throw err
@@ -134,7 +128,9 @@ export function Suspense(props: SuspenseProps): FictNode {
134
128
  registerSuspenseHandler(token => {
135
129
  const tokenEpoch = epoch
136
130
  pending(pending() + 1)
137
- switchView(toFallback())
131
+ // Directly render fallback instead of using switchView to avoid
132
+ // triggering the effect which would cause duplicate renders
133
+ renderView(toFallback())
138
134
 
139
135
  const thenable = (token as SuspenseToken).then
140
136
  ? (token as SuspenseToken)
@@ -157,7 +153,8 @@ export function Suspense(props: SuspenseProps): FictNode {
157
153
  const newPending = Math.max(0, pending() - 1)
158
154
  pending(newPending)
159
155
  if (newPending === 0) {
160
- switchView(props.children ?? null)
156
+ // Directly render children instead of using switchView
157
+ renderView(props.children ?? null)
161
158
  onResolveMaybe()
162
159
  }
163
160
  },
@@ -180,9 +177,10 @@ export function Suspense(props: SuspenseProps): FictNode {
180
177
  return false
181
178
  })
182
179
 
183
- createEffect(() => {
184
- renderView(currentView())
185
- })
180
+ // Initial render - render children directly
181
+ // Note: This will be called synchronously during component creation.
182
+ // If children suspend, the handler above will be called and switch to fallback.
183
+ renderView(props.children ?? null)
186
184
 
187
185
  if (props.resetKeys !== undefined) {
188
186
  const isGetter =
@@ -195,7 +193,8 @@ export function Suspense(props: SuspenseProps): FictNode {
195
193
  prev = next
196
194
  epoch++
197
195
  pending(0)
198
- switchView(props.children ?? null)
196
+ // Directly render children instead of using switchView
197
+ renderView(props.children ?? null)
199
198
  }
200
199
  })
201
200
  }