fusee-framework 1.7.0 → 1.9.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 (77) hide show
  1. package/README.md +206 -139
  2. package/app/benchmark/benchmark.js +2 -2
  3. package/app/benchmark/vite.config.js +23 -0
  4. package/app/main.js +24 -3
  5. package/app/main.ts +7 -1
  6. package/app/pages/_layout.js +27 -0
  7. package/app/pages/async-work.js +65 -0
  8. package/app/pages/index.js +25 -5
  9. package/app/pages/index.ts +9 -0
  10. package/app/pages/router-example.js +24 -1
  11. package/app/pages/scroll-demo.ts +102 -0
  12. package/app/providers/ChildComponent.js +8 -2
  13. package/app/providers/ChildComponent.ts +47 -47
  14. package/app/providers/GrandchildComponent.js +19 -0
  15. package/app/providers/ParentComponent.js +5 -1
  16. package/app/routes/RouterExample.js +23 -1
  17. package/app/routes/TestResource.js +47 -0
  18. package/app/services.js +15 -0
  19. package/auto-imports.d.ts +12 -0
  20. package/framework/auto-import-preset.js +15 -9
  21. package/framework/core/async-context.js +27 -0
  22. package/framework/core/compiler/ast.js +167 -0
  23. package/framework/core/compiler/errors-list.js +47 -0
  24. package/framework/core/compiler/errors.js +107 -0
  25. package/framework/core/compiler/generator.js +363 -0
  26. package/framework/core/compiler/lexer.js +347 -0
  27. package/framework/core/compiler/main-compiler.js +115 -0
  28. package/framework/core/compiler/parser.js +414 -0
  29. package/framework/core/compiler/plugins/actions-plugin.js +263 -0
  30. package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
  31. package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
  32. package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
  33. package/framework/core/compiler/plugins/router-plugin.js +196 -0
  34. package/framework/core/compiler/transformer.js +398 -0
  35. package/framework/core/component.js +61 -23
  36. package/framework/core/di.js +187 -0
  37. package/framework/core/directives.js +40 -96
  38. package/framework/core/evaluator.js +1 -1
  39. package/framework/core/h.js +430 -0
  40. package/framework/core/hydration.js +83 -0
  41. package/framework/core/reconcile.js +151 -0
  42. package/framework/core/signal.js +269 -0
  43. package/framework/core/store.js +322 -81
  44. package/framework/index.js +5 -3
  45. package/framework/router/file-router.js +1 -1
  46. package/framework/router/router.js +113 -21
  47. package/framework/server/actions.js +173 -0
  48. package/framework/server/actions.server.js +88 -0
  49. package/framework/server/index.js +18 -0
  50. package/framework/types/actions.d.ts +50 -0
  51. package/framework/types/actions.server.d.ts +36 -0
  52. package/framework/types/compiler/ast.d.ts +166 -0
  53. package/framework/types/compiler/errors-codes.d.ts +23 -0
  54. package/framework/types/compiler/errors.d.ts +43 -0
  55. package/framework/types/compiler/generator.d.ts +8 -0
  56. package/framework/types/compiler/h.d.ts +46 -0
  57. package/framework/types/compiler/lexer.d.ts +27 -0
  58. package/framework/types/compiler/main-compiler.d.ts +60 -0
  59. package/framework/types/compiler/parser.d.ts +4 -0
  60. package/framework/types/compiler/plugins/actions.d.ts +30 -0
  61. package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
  62. package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
  63. package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
  64. package/framework/types/compiler/transformer.d.ts +28 -0
  65. package/framework/types/component.d.ts +89 -88
  66. package/framework/types/di.d.ts +43 -0
  67. package/framework/types/hydration.d.ts +30 -0
  68. package/framework/types/index.d.ts +38 -22
  69. package/framework/types/reconcile.d.ts +7 -0
  70. package/framework/types/router.d.ts +11 -1
  71. package/framework/types/server.d.ts +11 -0
  72. package/framework/types/signal.d.ts +142 -79
  73. package/framework/types/store.d.ts +66 -8
  74. package/ghid_go_fusee_integrat_go1_26.txt +276 -0
  75. package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
  76. package/package.json +44 -36
  77. package/vite.config.js +65 -47
@@ -1,10 +1,12 @@
1
1
  // ─── Framework Public API ─────────────────────────────────────────────────────
2
- export { signal, computed, effect, batch, untrack, inspect, watch, onCleanup } from './core/signal.js'
2
+ export { signal, computed, effect, batch, untrack, inspect, watch, onCleanup, resource, createSuspense, scheduleAsyncJob } from './core/signal.js'
3
3
  export { defineComponent, defineAsyncComponent, onMount, onUnmount, parseSlots, provide, inject, getCurrentInstance } from './core/component.js'
4
- export { createRouter, navigate, mountOutlet, currentRoute, routeParams, matchedRoutes } from './router/router.js'
4
+ export { InjectionToken, provideGlobal } from './core/di.js'
5
+ export { createRouter, navigate, mountOutlet, currentRoute, routeParams, routeQuery, matchedRoutes } from './router/router.js'
5
6
  export { generateRoutes } from './router/file-router.js'
6
7
  export { mountTemplate } from './core/compiler.js'
7
8
  export { defineComposable, assertSetupContext } from './core/composable.js'
8
- export { defineStore, resetStore, clearStores, registerStorePlugin } from './core/store.js'
9
+ export { defineStore, resetStore, clearStores, registerStorePlugin, storeToRefs, storeToState, storeToGetters, useNestedStore, MutationType } from './core/store.js'
9
10
  export { directive } from './core/directives.js'
11
+ export { reconcile } from './core/reconcile.js'
10
12
  export const emit = () => console.warn('[framework] emit() can only be used inside component setup()')
@@ -173,5 +173,5 @@ function _compareRoutes(a, b) {
173
173
  const bSegs = bPath.split('/').filter(Boolean).length;
174
174
  if (aSegs !== bSegs) return bSegs - aSegs;
175
175
 
176
- return bPath.length - aPath.length;
176
+ return aPath.localeCompare(bPath);
177
177
  }
@@ -2,6 +2,7 @@ import { signal } from '../core/signal.js'
2
2
 
3
3
  export const currentRoute = signal('/')
4
4
  export const routeParams = signal({})
5
+ export const routeQuery = signal({})
5
6
  export const matchedRoutes = signal([])
6
7
 
7
8
  let _routes = []
@@ -11,9 +12,26 @@ let _activeChain = []
11
12
  let _routeCache = new Map()
12
13
  let _cacheMaxSize = 100
13
14
  let _routerViewTimeout = 10000
15
+ let _scrollBehaviorOptions = null
16
+ let _scrollPositions = new Map()
17
+ let _currentPath = null
14
18
 
15
19
  function _getPath() {
16
- return window.location.pathname || '/'
20
+ const pathname = window.location.pathname || '/'
21
+ const search = window.location.search || ''
22
+ const hash = window.location.hash || ''
23
+ return pathname + search + hash
24
+ }
25
+
26
+ function _extractQuery(path) {
27
+ const queryStr = path.split('?')[1]?.split('#')[0]
28
+ if (!queryStr) return {}
29
+ const params = new URLSearchParams(queryStr)
30
+ const query = {}
31
+ for (const [key, value] of params.entries()) {
32
+ query[key] = value
33
+ }
34
+ return query
17
35
  }
18
36
 
19
37
  function _getRoutePaths(routePath) {
@@ -90,10 +108,9 @@ function _matchRouteTree(routes, urlSegments) {
90
108
  }
91
109
 
92
110
  function _matchSingleFlat(routePath, actualPath) {
93
- if (routePath === '*') return true
94
-
111
+ const actualPathWithoutParams = actualPath.split(/[?#]/)[0]
95
112
  const routeParts = routePath.split('/').filter(Boolean)
96
- const actualParts = actualPath.split('/').filter(Boolean)
113
+ const actualParts = actualPathWithoutParams.split('/').filter(Boolean)
97
114
 
98
115
  const hasWildcard = routeParts.length > 0 && routeParts[routeParts.length - 1] === '*'
99
116
 
@@ -121,8 +138,9 @@ function _extractParamsFromPath(singlePath, actualPath) {
121
138
  const params = {}
122
139
  if (singlePath === '*') return params
123
140
 
141
+ const actualPathWithoutParams = actualPath.split(/[?#]/)[0]
124
142
  const routeParts = singlePath.split('/').filter(Boolean)
125
- const actualParts = actualPath.split('/').filter(Boolean)
143
+ const actualParts = actualPathWithoutParams.split('/').filter(Boolean)
126
144
 
127
145
  const hasWildcard = routeParts.length > 0 && routeParts[routeParts.length - 1] === '*'
128
146
  const checkLen = hasWildcard ? routeParts.length - 1 : routeParts.length
@@ -142,14 +160,16 @@ function _extractParamsFlat(routePath, actualPath) {
142
160
  }
143
161
 
144
162
  function _findMatchingChain(path) {
145
- if (_routeCache.has(path)) {
146
- const cached = _routeCache.get(path)
147
- _routeCache.delete(path)
148
- _routeCache.set(path, cached)
163
+ const pathWithoutParams = path.split(/[?#]/)[0]
164
+
165
+ if (_routeCache.has(pathWithoutParams)) {
166
+ const cached = _routeCache.get(pathWithoutParams)
167
+ _routeCache.delete(pathWithoutParams)
168
+ _routeCache.set(pathWithoutParams, cached)
149
169
  return cached
150
170
  }
151
171
 
152
- const urlSegments = path.split('/').filter(Boolean)
172
+ const urlSegments = pathWithoutParams.split('/').filter(Boolean)
153
173
  const hasNested = _routes.some(r => r.children && r.children.length > 0)
154
174
 
155
175
  let chain = null
@@ -157,7 +177,7 @@ function _findMatchingChain(path) {
157
177
  if (hasNested) {
158
178
  chain = _matchRouteTree(_routes, urlSegments)
159
179
  if (chain) {
160
- _cacheResult(path, chain)
180
+ _cacheResult(pathWithoutParams, chain)
161
181
  return chain
162
182
  }
163
183
  }
@@ -166,11 +186,11 @@ function _findMatchingChain(path) {
166
186
  const routePaths = _getRoutePaths(r.path)
167
187
  return !routePaths.includes('*') && !routePaths.some(p => p.endsWith('/*')) && !r.children
168
188
  })
169
- const exactMatch = paths.find(r => _matchFlat(r.path, path))
189
+ const exactMatch = paths.find(r => _matchFlat(r.path, pathWithoutParams))
170
190
  if (exactMatch) {
171
- const matchedPath = _findMatchingPath(exactMatch.path, path)
172
- chain = [{ route: exactMatch, params: _extractParamsFlat(exactMatch.path, path), matchedSegments: urlSegments, matchedPath }]
173
- _cacheResult(path, chain)
191
+ const matchedPath = _findMatchingPath(exactMatch.path, pathWithoutParams)
192
+ chain = [{ route: exactMatch, params: _extractParamsFlat(exactMatch.path, pathWithoutParams), matchedSegments: urlSegments, matchedPath }]
193
+ _cacheResult(pathWithoutParams, chain)
174
194
  return chain
175
195
  }
176
196
 
@@ -178,11 +198,11 @@ function _findMatchingChain(path) {
178
198
  const routePaths = _getRoutePaths(r.path)
179
199
  return !r.children && routePaths.some(p => p.endsWith('/*'))
180
200
  })
181
- const wildcardMatch = wildcardPaths.find(r => _matchFlat(r.path, path))
201
+ const wildcardMatch = wildcardPaths.find(r => _matchFlat(r.path, pathWithoutParams))
182
202
  if (wildcardMatch) {
183
- const matchedPath = _findMatchingPath(wildcardMatch.path, path)
184
- chain = [{ route: wildcardMatch, params: _extractParamsFlat(wildcardMatch.path, path), matchedSegments: urlSegments, matchedPath }]
185
- _cacheResult(path, chain)
203
+ const matchedPath = _findMatchingPath(wildcardMatch.path, pathWithoutParams)
204
+ chain = [{ route: wildcardMatch, params: _extractParamsFlat(wildcardMatch.path, pathWithoutParams), matchedSegments: urlSegments, matchedPath }]
205
+ _cacheResult(pathWithoutParams, chain)
186
206
  return chain
187
207
  }
188
208
 
@@ -192,7 +212,7 @@ function _findMatchingChain(path) {
192
212
  })
193
213
  if (catchAll) {
194
214
  chain = [{ route: catchAll, params: {}, matchedSegments: urlSegments, matchedPath: '*' }]
195
- _cacheResult(path, chain)
215
+ _cacheResult(pathWithoutParams, chain)
196
216
  return chain
197
217
  }
198
218
 
@@ -211,6 +231,63 @@ function _clearRouteCache() {
211
231
  _routeCache.clear()
212
232
  }
213
233
 
234
+ function _saveScrollPosition(path) {
235
+ if (!_scrollBehaviorOptions?.saveScrollPosition) return
236
+ _scrollPositions.set(path, {
237
+ left: window.scrollX,
238
+ top: window.scrollY
239
+ })
240
+ }
241
+
242
+ function _getSavedScrollPosition(path) {
243
+ return _scrollPositions.get(path) || null
244
+ }
245
+
246
+ function _applyScrollBehavior(to, from) {
247
+ const options = _scrollBehaviorOptions
248
+ if (!options) return
249
+
250
+ const savedPosition = _getSavedScrollPosition(to)
251
+
252
+ if (options.custom) {
253
+ const result = options.custom(to, from, savedPosition)
254
+ if (result && typeof result.then === 'function') {
255
+ result.then(pos => _performScroll(pos))
256
+ } else {
257
+ _performScroll(result)
258
+ }
259
+ return
260
+ }
261
+
262
+ if (options.scrollToAnchor && to.includes('#')) {
263
+ const anchor = to.split('#')[1]
264
+ const element = document.getElementById(anchor)
265
+ if (element) {
266
+ element.scrollIntoView({ behavior: 'smooth' })
267
+ return
268
+ }
269
+ }
270
+
271
+ if (options.scrollToTop !== false) {
272
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
273
+ } else if (savedPosition && options.saveScrollPosition) {
274
+ window.scrollTo(savedPosition.left, savedPosition.top)
275
+ }
276
+ }
277
+
278
+ function _performScroll(position) {
279
+ if (!position || position === false) return
280
+
281
+ if (typeof position === 'object' && 'selector' in position) {
282
+ const element = document.querySelector(position.selector)
283
+ if (element) {
284
+ element.scrollIntoView({ behavior: 'smooth' })
285
+ }
286
+ } else if (typeof position === 'object' && 'left' in position) {
287
+ window.scrollTo({ left: position.left, top: position.top, behavior: 'smooth' })
288
+ }
289
+ }
290
+
214
291
  function _unmountFromLevel(level) {
215
292
  for (let i = _activeChain.length - 1; i >= level; i--) {
216
293
  const entry = _activeChain[i]
@@ -362,10 +439,21 @@ function _resolveRoute() {
362
439
  }
363
440
 
364
441
  _renderChain(chain)
442
+
443
+ const from = _currentPath
444
+ _currentPath = path
445
+ _applyScrollBehavior(path, from)
446
+ routeQuery(_extractQuery(path))
365
447
  }
366
448
 
367
449
  export function navigate(path) {
368
- if (path === _getPath()) return
450
+ const currentPath = _getPath().split(/[?#]/)[0]
451
+ const targetPath = path.split(/[?#]/)[0]
452
+ if (targetPath === currentPath && path === _getPath()) {
453
+ _applyScrollBehavior(path, _getPath())
454
+ return
455
+ }
456
+ _saveScrollPosition(_getPath())
369
457
  window.history.pushState({}, '', path)
370
458
  _updateRoute()
371
459
  _resolveRoute()
@@ -440,6 +528,7 @@ export function createRouter(routes, options = {}) {
440
528
  _activeChain = []
441
529
  _cacheMaxSize = options.cacheSize || 100
442
530
  _routerViewTimeout = options.routerViewTimeout || 10000
531
+ _scrollBehaviorOptions = options.scrollBehavior || null
443
532
 
444
533
  _updateRoute()
445
534
 
@@ -457,6 +546,9 @@ export function createRouter(routes, options = {}) {
457
546
  _rootOutlet = null
458
547
  _activeChain = []
459
548
  _clearRouteCache()
549
+ _scrollPositions.clear()
550
+ _scrollBehaviorOptions = null
551
+ _currentPath = null
460
552
  }
461
553
  }
462
554
  }
@@ -0,0 +1,173 @@
1
+ import { signal, batch } from '../core/signal.js'
2
+ import { getCurrentInstance } from '../core/component.js'
3
+
4
+ const ACTION_BASE_URL = '/__fusee/actions'
5
+ const _actionHydrationRegistry = new Map()
6
+
7
+ export function defineAction(fnOrName, opts = {}) {
8
+ const name = opts.name
9
+ || (typeof fnOrName === 'function' ? fnOrName.name : fnOrName)
10
+
11
+ if (!name) {
12
+ throw new Error('[fusee] defineAction() client stub: nu s-a putut determina numele acțiunii')
13
+ }
14
+
15
+ return createActionProxy(name, opts)
16
+ }
17
+
18
+ export function createActionProxy(name, opts = {}) {
19
+ const baseUrl = opts.baseUrl ?? ACTION_BASE_URL
20
+
21
+ async function actionProxy(...args) {
22
+ const res = await fetch(`${baseUrl}/${name}`, {
23
+ method: 'POST',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ ...(opts.headers ?? {})
27
+ },
28
+ body: JSON.stringify({ args })
29
+ })
30
+
31
+ const json = await res.json()
32
+
33
+ if (!res.ok) {
34
+ const err = new Error(json.error ?? `Action "${name}" failed (${res.status})`)
35
+ err.status = res.status
36
+ err.code = json.code
37
+ throw err
38
+ }
39
+
40
+ return json.data
41
+ }
42
+
43
+ actionProxy._isAction = true
44
+ actionProxy._actionName = name
45
+
46
+ return actionProxy
47
+ }
48
+
49
+ export function useAction(action, opts = {}) {
50
+ if (typeof action !== 'function') {
51
+ throw new Error('[fusee] useAction() requires an action function (result of defineAction or createActionProxy)')
52
+ }
53
+
54
+ const pending = signal(false)
55
+ const data = signal(undefined)
56
+ const error = signal(undefined)
57
+
58
+ const { onSuccess, onError, resetOnCall = true } = opts
59
+
60
+ let callId = 0
61
+
62
+ async function execute(...args) {
63
+ const id = ++callId
64
+
65
+ batch(() => {
66
+ pending(true)
67
+ if (resetOnCall) {
68
+ data(undefined)
69
+ error(undefined)
70
+ }
71
+ })
72
+
73
+ try {
74
+ const result = await action(...args)
75
+
76
+ if (id !== callId) return result
77
+
78
+ batch(() => {
79
+ data(result)
80
+ pending(false)
81
+ })
82
+
83
+ if (typeof onSuccess === 'function') {
84
+ onSuccess(result)
85
+ }
86
+
87
+ return result
88
+ } catch (err) {
89
+ if (id !== callId) return
90
+
91
+ batch(() => {
92
+ error(err)
93
+ pending(false)
94
+ })
95
+
96
+ if (typeof onError === 'function') {
97
+ onError(err)
98
+ } else {
99
+ console.error(`[fusee] useAction("${action._actionName ?? '?'}") error:`, err)
100
+ }
101
+ }
102
+ }
103
+
104
+ function reset() {
105
+ batch(() => {
106
+ pending(false)
107
+ data(undefined)
108
+ error(undefined)
109
+ })
110
+ callId++
111
+ }
112
+
113
+ const instance = getCurrentInstance()
114
+ if (instance) {
115
+ instance._unmountHooks.push(() => {
116
+ callId++
117
+ })
118
+ }
119
+
120
+ return { execute, pending, data, error, reset }
121
+ }
122
+
123
+ export function hydrateAction(actionName, data, opts = {}) {
124
+ const key = `action:${actionName}`
125
+ _actionHydrationRegistry.set(key, {
126
+ data,
127
+ timestamp: Date.now(),
128
+ staleTime: opts.staleTime ?? 0
129
+ })
130
+ }
131
+
132
+ export function getHydratedAction(actionName, staleTime) {
133
+ const key = `action:${actionName}`
134
+ const entry = _actionHydrationRegistry.get(key)
135
+ if (!entry) return undefined
136
+
137
+ if (staleTime !== undefined && (Date.now() - entry.timestamp) > staleTime) {
138
+ return undefined
139
+ }
140
+ if (entry.staleTime && (Date.now() - entry.timestamp) > entry.staleTime) {
141
+ return undefined
142
+ }
143
+
144
+ return entry.data
145
+ }
146
+
147
+ export function clearActionHydration() {
148
+ _actionHydrationRegistry.clear()
149
+ }
150
+
151
+ export function extractActionHydration() {
152
+ const snapshot = {}
153
+ for (const [key, entry] of _actionHydrationRegistry) {
154
+ snapshot[key] = {
155
+ data: entry.data,
156
+ timestamp: entry.timestamp,
157
+ staleTime: entry.staleTime
158
+ }
159
+ }
160
+ return snapshot
161
+ }
162
+
163
+ export function loadActionHydration(snapshot) {
164
+ if (!snapshot || typeof snapshot !== 'object') return
165
+
166
+ for (const [key, entry] of Object.entries(snapshot)) {
167
+ _actionHydrationRegistry.set(key, {
168
+ data: entry.data,
169
+ timestamp: entry.timestamp,
170
+ staleTime: entry.staleTime
171
+ })
172
+ }
173
+ }
@@ -0,0 +1,88 @@
1
+ import { withRequestContext, createRequestContext } from './index.js'
2
+
3
+ const _actionRegistry = new Map()
4
+
5
+ export function defineAction(fn, opts = {}) {
6
+ if (typeof fn !== 'function') {
7
+ throw new Error('[fusee] defineAction() requires a function')
8
+ }
9
+
10
+ const name = opts.name || fn.name
11
+ if (!name) {
12
+ throw new Error(
13
+ '[fusee] defineAction(): funcția trebuie să aibă un nume sau să specifici opts.name'
14
+ )
15
+ }
16
+
17
+ const wrapped = async function (...args) {
18
+ if (opts.authorize) {
19
+ await opts.authorize(...args)
20
+ }
21
+ return fn(...args)
22
+ }
23
+
24
+ wrapped._isAction = true
25
+ wrapped._actionName = name
26
+ _actionRegistry.set(name, { fn: wrapped, opts })
27
+
28
+ return wrapped
29
+ }
30
+
31
+ export async function handleActionRequest(req, res) {
32
+ let actionName
33
+ if (req.params?.name) {
34
+ actionName = req.params.name
35
+ } else {
36
+ const url = new URL(req.url, 'http://localhost')
37
+ const parts = url.pathname.split('/')
38
+ actionName = parts[parts.length - 1]
39
+ }
40
+
41
+ const entry = _actionRegistry.get(actionName)
42
+ if (!entry) {
43
+ return _sendJson(res, 404, { error: `Action "${actionName}" not found` })
44
+ }
45
+
46
+ let body
47
+ try {
48
+ body = req.body ?? (await _parseBody(req))
49
+ } catch {
50
+ return _sendJson(res, 400, { error: 'Invalid JSON body' })
51
+ }
52
+
53
+ const args = Array.isArray(body?.args) ? body.args : body != null ? [body] : []
54
+ const ctx = createRequestContext()
55
+
56
+ try {
57
+ const data = await withRequestContext(ctx, () => entry.fn(...args))
58
+ return _sendJson(res, 200, { data: data ?? null })
59
+ } catch (err) {
60
+ const isPublic = err?.expose === true
61
+ console.error(`[fusee] Action "${actionName}" threw:`, err)
62
+ return _sendJson(res, err?.status ?? 500, {
63
+ error: isPublic ? err.message : 'Internal server error',
64
+ code: err?.code
65
+ })
66
+ }
67
+ }
68
+
69
+ export function getRegisteredActions() {
70
+ return [..._actionRegistry.keys()]
71
+ }
72
+
73
+ async function _parseBody(req) {
74
+ const chunks = []
75
+ for await (const chunk of req) chunks.push(chunk)
76
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'))
77
+ }
78
+
79
+ function _sendJson(res, status, body) {
80
+ if (typeof res.status === 'function') {
81
+ return res.status(status).json(body)
82
+ }
83
+
84
+ return new Response(JSON.stringify(body), {
85
+ status,
86
+ headers: { 'Content-Type': 'application/json' }
87
+ })
88
+ }
@@ -0,0 +1,18 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { _registerALS } from '../core/async-context.js'
3
+
4
+ const _als = new AsyncLocalStorage()
5
+ _registerALS(_als)
6
+
7
+ export function createRequestContext() {
8
+ return {
9
+ inFlightMaps: {
10
+ byKey: new Map(),
11
+ byFetcher: new WeakMap()
12
+ }
13
+ }
14
+ }
15
+
16
+ export function withRequestContext(ctx, fn) {
17
+ return _als.run(ctx, fn)
18
+ }
@@ -0,0 +1,50 @@
1
+ export interface ActionOptions {
2
+ name?: string
3
+ baseUrl?: string
4
+ headers?: Record<string, string>
5
+ }
6
+
7
+ export interface UseActionOptions {
8
+ onSuccess?: (data: any) => void
9
+ onError?: (error: Error) => void
10
+ resetOnCall?: boolean
11
+ }
12
+
13
+ export interface ActionReturn {
14
+ execute: (...args: any[]) => Promise<any>
15
+ pending: Signal<boolean>
16
+ data: Signal<any>
17
+ error: Signal<any>
18
+ reset: () => void
19
+ }
20
+
21
+ export interface ActionProxy extends Function {
22
+ _isAction: boolean
23
+ _actionName: string
24
+ }
25
+
26
+ export declare function defineAction(fnOrName: Function | string, opts?: ActionOptions): ActionProxy
27
+ export declare function createActionProxy(name: string, opts?: ActionOptions): ActionProxy
28
+ export declare function useAction(action: ActionProxy, opts?: UseActionOptions): ActionReturn
29
+
30
+ // ─── Hydration Support ─────────────────────────────────────────────────────
31
+
32
+ export interface ActionHydrationOptions {
33
+ staleTime?: number
34
+ }
35
+
36
+ export interface ActionHydrationEntry {
37
+ data: any
38
+ timestamp: number
39
+ staleTime: number
40
+ }
41
+
42
+ export interface ActionHydrationSnapshot {
43
+ [key: string]: ActionHydrationEntry
44
+ }
45
+
46
+ export declare function hydrateAction(actionName: string, data: any, opts?: ActionHydrationOptions): void
47
+ export declare function getHydratedAction(actionName: string, staleTime?: number): any
48
+ export declare function clearActionHydration(): void
49
+ export declare function extractActionHydration(): ActionHydrationSnapshot
50
+ export declare function loadActionHydration(snapshot: ActionHydrationSnapshot): void
@@ -0,0 +1,36 @@
1
+ export interface ActionServerOptions {
2
+ name?: string
3
+ authorize?: (...args: any[]) => Promise<void> | void
4
+ }
5
+
6
+ export interface RegisteredAction {
7
+ fn: Function
8
+ opts: ActionServerOptions
9
+ }
10
+
11
+ export interface ActionRegistry {
12
+ get(name: string): RegisteredAction | undefined
13
+ set(name: string, entry: RegisteredAction): void
14
+ keys(): IterableIterator<string>
15
+ }
16
+
17
+ export interface RequestLike {
18
+ params?: { name?: string }
19
+ body?: any
20
+ url?: string
21
+ }
22
+
23
+ export interface ResponseLike {
24
+ status(code: number): ResponseLike
25
+ json(body: any): any
26
+ }
27
+
28
+ export interface ActionError extends Error {
29
+ status?: number
30
+ code?: string
31
+ expose?: boolean
32
+ }
33
+
34
+ export declare function defineAction(fn: Function, opts?: ActionServerOptions): Function
35
+ export declare function handleActionRequest(req: RequestLike, res: ResponseLike): Promise<void>
36
+ export declare function getRegisteredActions(): string[]