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.
- package/README.md +206 -139
- package/app/benchmark/benchmark.js +2 -2
- package/app/benchmark/vite.config.js +23 -0
- package/app/main.js +24 -3
- package/app/main.ts +7 -1
- package/app/pages/_layout.js +27 -0
- package/app/pages/async-work.js +65 -0
- package/app/pages/index.js +25 -5
- package/app/pages/index.ts +9 -0
- package/app/pages/router-example.js +24 -1
- package/app/pages/scroll-demo.ts +102 -0
- package/app/providers/ChildComponent.js +8 -2
- package/app/providers/ChildComponent.ts +47 -47
- package/app/providers/GrandchildComponent.js +19 -0
- package/app/providers/ParentComponent.js +5 -1
- package/app/routes/RouterExample.js +23 -1
- package/app/routes/TestResource.js +47 -0
- package/app/services.js +15 -0
- package/auto-imports.d.ts +12 -0
- package/framework/auto-import-preset.js +15 -9
- package/framework/core/async-context.js +27 -0
- package/framework/core/compiler/ast.js +167 -0
- package/framework/core/compiler/errors-list.js +47 -0
- package/framework/core/compiler/errors.js +107 -0
- package/framework/core/compiler/generator.js +363 -0
- package/framework/core/compiler/lexer.js +347 -0
- package/framework/core/compiler/main-compiler.js +115 -0
- package/framework/core/compiler/parser.js +414 -0
- package/framework/core/compiler/plugins/actions-plugin.js +263 -0
- package/framework/core/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/core/compiler/plugins/compiler-plugin.js +45 -0
- package/framework/core/compiler/plugins/file-router-plugin.js +301 -0
- package/framework/core/compiler/plugins/router-plugin.js +196 -0
- package/framework/core/compiler/transformer.js +398 -0
- package/framework/core/component.js +61 -23
- package/framework/core/di.js +187 -0
- package/framework/core/directives.js +40 -96
- package/framework/core/evaluator.js +1 -1
- package/framework/core/h.js +430 -0
- package/framework/core/hydration.js +83 -0
- package/framework/core/reconcile.js +151 -0
- package/framework/core/signal.js +269 -0
- package/framework/core/store.js +322 -81
- package/framework/index.js +5 -3
- package/framework/router/file-router.js +1 -1
- package/framework/router/router.js +113 -21
- package/framework/server/actions.js +173 -0
- package/framework/server/actions.server.js +88 -0
- package/framework/server/index.js +18 -0
- package/framework/types/actions.d.ts +50 -0
- package/framework/types/actions.server.d.ts +36 -0
- package/framework/types/compiler/ast.d.ts +166 -0
- package/framework/types/compiler/errors-codes.d.ts +23 -0
- package/framework/types/compiler/errors.d.ts +43 -0
- package/framework/types/compiler/generator.d.ts +8 -0
- package/framework/types/compiler/h.d.ts +46 -0
- package/framework/types/compiler/lexer.d.ts +27 -0
- package/framework/types/compiler/main-compiler.d.ts +60 -0
- package/framework/types/compiler/parser.d.ts +4 -0
- package/framework/types/compiler/plugins/actions.d.ts +30 -0
- package/framework/types/compiler/plugins/compiler-plugin.d.ts +5 -0
- package/framework/types/compiler/plugins/file-router-plugins.d.ts +32 -0
- package/framework/types/compiler/plugins/router-plugin.d.ts +24 -0
- package/framework/types/compiler/transformer.d.ts +28 -0
- package/framework/types/component.d.ts +89 -88
- package/framework/types/di.d.ts +43 -0
- package/framework/types/hydration.d.ts +30 -0
- package/framework/types/index.d.ts +38 -22
- package/framework/types/reconcile.d.ts +7 -0
- package/framework/types/router.d.ts +11 -1
- package/framework/types/server.d.ts +11 -0
- package/framework/types/signal.d.ts +142 -79
- package/framework/types/store.d.ts +66 -8
- package/ghid_go_fusee_integrat_go1_26.txt +276 -0
- package/ghid_rust_fusee_rust_1_95_0.txt +1118 -0
- package/package.json +44 -36
- package/vite.config.js +65 -47
package/framework/index.js
CHANGED
|
@@ -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 {
|
|
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()')
|
|
@@ -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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
111
|
+
const actualPathWithoutParams = actualPath.split(/[?#]/)[0]
|
|
95
112
|
const routeParts = routePath.split('/').filter(Boolean)
|
|
96
|
-
const actualParts =
|
|
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 =
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
_routeCache.
|
|
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 =
|
|
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(
|
|
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,
|
|
189
|
+
const exactMatch = paths.find(r => _matchFlat(r.path, pathWithoutParams))
|
|
170
190
|
if (exactMatch) {
|
|
171
|
-
const matchedPath = _findMatchingPath(exactMatch.path,
|
|
172
|
-
chain = [{ route: exactMatch, params: _extractParamsFlat(exactMatch.path,
|
|
173
|
-
_cacheResult(
|
|
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,
|
|
201
|
+
const wildcardMatch = wildcardPaths.find(r => _matchFlat(r.path, pathWithoutParams))
|
|
182
202
|
if (wildcardMatch) {
|
|
183
|
-
const matchedPath = _findMatchingPath(wildcardMatch.path,
|
|
184
|
-
chain = [{ route: wildcardMatch, params: _extractParamsFlat(wildcardMatch.path,
|
|
185
|
-
_cacheResult(
|
|
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(
|
|
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
|
-
|
|
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[]
|