@owlmeans/client 0.1.18-rc.12 → 0.1.18-rc.13
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 +34 -10
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client/SKILL.md +25 -11
- package/build/components/modal.d.ts.map +1 -1
- package/build/components/modal.js.map +1 -1
- package/build/components/toggle.d.ts.map +1 -1
- package/build/context.d.ts.map +1 -1
- package/build/context.js +1 -3
- package/build/context.js.map +1 -1
- package/build/debug.d.ts.map +1 -1
- package/build/entrypoint.d.ts +3 -0
- package/build/entrypoint.d.ts.map +1 -0
- package/build/entrypoint.js +4 -0
- package/build/entrypoint.js.map +1 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js.map +1 -1
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +3 -3
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/navigate.d.ts.map +1 -1
- package/build/navigate.js +12 -15
- package/build/navigate.js.map +1 -1
- package/build/router.d.ts.map +1 -1
- package/build/router.js +12 -6
- package/build/router.js.map +1 -1
- package/build/services/debug.d.ts.map +1 -1
- package/build/services/debug.js.map +1 -1
- package/build/store.d.ts +18 -3
- package/build/store.d.ts.map +1 -1
- package/build/store.js +56 -55
- package/build/store.js.map +1 -1
- package/build/types.d.ts +11 -2
- package/build/types.d.ts.map +1 -1
- package/build/utils/entrypoint.d.ts +3 -0
- package/build/utils/entrypoint.d.ts.map +1 -0
- package/build/utils/{module.js → entrypoint.js} +2 -2
- package/build/utils/entrypoint.js.map +1 -0
- package/build/utils/index.d.ts +1 -1
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +1 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/route.d.ts.map +1 -1
- package/build/utils/route.js +3 -3
- package/build/utils/route.js.map +1 -1
- package/build/utils/router.d.ts +4 -4
- package/build/utils/router.d.ts.map +1 -1
- package/build/utils/router.js +8 -8
- package/build/utils/router.js.map +1 -1
- package/build/value.d.ts.map +1 -1
- package/package.json +12 -12
- package/src/context.ts +1 -4
- package/src/entrypoint.ts +6 -0
- package/src/helper.tsx +6 -6
- package/src/index.ts +1 -1
- package/src/navigate.ts +12 -15
- package/src/router.ts +14 -10
- package/src/services/debug.ts +2 -2
- package/src/store.ts +70 -59
- package/src/types.ts +12 -2
- package/src/utils/{module.ts → entrypoint.ts} +2 -2
- package/src/utils/index.ts +1 -1
- package/src/utils/route.tsx +5 -5
- package/src/utils/router.ts +11 -11
- package/build/module.d.ts +0 -3
- package/build/module.d.ts.map +0 -1
- package/build/module.js +0 -4
- package/build/module.js.map +0 -1
- package/build/utils/module.d.ts +0 -3
- package/build/utils/module.d.ts.map +0 -1
- package/build/utils/module.js.map +0 -1
- package/src/module.ts +0 -6
package/src/store.ts
CHANGED
|
@@ -1,73 +1,84 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { StateModel, UseStoreHelper, UseStoreHelperOptions, UseStoreListHelper } from '@owlmeans/state'
|
|
3
|
-
import { useContext } from './context.js'
|
|
1
|
+
import { useMemo, useSyncExternalStore } from 'react'
|
|
4
2
|
import type { ResourceRecord } from '@owlmeans/resource'
|
|
3
|
+
import type { StateModel } from '@owlmeans/state'
|
|
4
|
+
import { useContext } from './context.js'
|
|
5
|
+
import type { UseStoreListOptions } from './types.js'
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Turn one state subscription into the pair `useSyncExternalStore` wants.
|
|
9
|
+
*
|
|
10
|
+
* The value is cached rather than rebuilt per render: the hook is asked for a snapshot on every
|
|
11
|
+
* render and must answer with the same reference until something actually changed, or React
|
|
12
|
+
* re-renders forever. A state subscription hands its value over synchronously, so the first
|
|
13
|
+
* snapshot is taken by subscribing and unsubscribing again — no render ever runs without one,
|
|
14
|
+
* and no subscription outlives the call.
|
|
15
|
+
*/
|
|
16
|
+
const bind = <V>(subscribe: (listener: (value: V) => void) => () => void) => {
|
|
17
|
+
let current: V | undefined
|
|
18
|
+
let seeded = false
|
|
8
19
|
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
const capture = (notify?: () => void) => (value: V) => {
|
|
21
|
+
current = value
|
|
22
|
+
seeded = true
|
|
23
|
+
notify?.()
|
|
11
24
|
}
|
|
12
25
|
|
|
13
|
-
return
|
|
14
|
-
|
|
26
|
+
return {
|
|
27
|
+
subscribe: (notify: () => void) => subscribe(capture(notify)),
|
|
28
|
+
snapshot: (): V => {
|
|
29
|
+
if (!seeded) {
|
|
30
|
+
subscribe(capture())()
|
|
31
|
+
}
|
|
15
32
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
opts = ids
|
|
19
|
-
}
|
|
20
|
-
const params: UseStoreHelperOptions<ResourceRecord> = typeof opts === 'object' ? opts : {}
|
|
21
|
-
if (typeof ids === 'string' || Array.isArray(ids)) {
|
|
22
|
-
params.id = ids
|
|
23
|
-
}
|
|
24
|
-
if (typeof opts === 'string') {
|
|
25
|
-
params.resource = opts
|
|
26
|
-
} else if (typeof opts === 'boolean') {
|
|
27
|
-
params.listen = opts
|
|
33
|
+
return current as V
|
|
34
|
+
}
|
|
28
35
|
}
|
|
29
|
-
|
|
36
|
+
}
|
|
30
37
|
|
|
38
|
+
/**
|
|
39
|
+
* One record from a state resource, live.
|
|
40
|
+
*
|
|
41
|
+
* It never throws for missing data: an id the store knows nothing about yields a model whose
|
|
42
|
+
* `empty` is true — reading `record` gives the resource's configured default, and nothing is
|
|
43
|
+
* written into the store on the way. Pass no id to address the one record of a `single`
|
|
44
|
+
* resource; on any other that is a configuration error and throws.
|
|
45
|
+
*/
|
|
46
|
+
export const useStoreModel = <T extends ResourceRecord = ResourceRecord>(
|
|
47
|
+
id?: string, alias?: string
|
|
48
|
+
): StateModel<T> => {
|
|
31
49
|
const context = useContext()
|
|
32
|
-
const resource = useMemo(() => context.getStateResource(
|
|
50
|
+
const resource = useMemo(() => context.getStateResource<T>(alias), [context, alias])
|
|
51
|
+
const store = useMemo(
|
|
52
|
+
() => bind<StateModel<T>>(listener => resource.watch(id, listener)),
|
|
53
|
+
[resource, id]
|
|
54
|
+
)
|
|
33
55
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The query's CONTENT, not merely whether one was given. A subscription is to the criteria,
|
|
38
|
-
* so a screen that narrows its filter has to re-subscribe — keyed on `query == null` it kept
|
|
39
|
-
* answering the first filter it ever saw, and nothing indicated the list had gone stale.
|
|
40
|
-
*/
|
|
41
|
-
JSON.stringify(params.query),
|
|
42
|
-
JSON.stringify(params.default),
|
|
43
|
-
params.listen
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
const id = useId()
|
|
47
|
-
const [models, setModels] = useState<StateModel<ResourceRecord>[]>([])
|
|
48
|
-
|
|
49
|
-
let _return = [...models]
|
|
50
|
-
|
|
51
|
-
const unsubscribe = useMemo(() => {
|
|
52
|
-
const [unsubscribe, _initialModels] = resource.subscribe({
|
|
53
|
-
_systemId: id,
|
|
54
|
-
id: params.id,
|
|
55
|
-
listener: models => {
|
|
56
|
-
params.listen && setModels(models)
|
|
57
|
-
},
|
|
58
|
-
default: params.default,
|
|
59
|
-
query: params.query,
|
|
60
|
-
})
|
|
61
|
-
setModels(_initialModels)
|
|
62
|
-
|
|
63
|
-
if (_return.length < 1) {
|
|
64
|
-
_return.push(..._initialModels)
|
|
65
|
-
}
|
|
56
|
+
return useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot)
|
|
57
|
+
}
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
/**
|
|
60
|
+
* A live LIST from a state resource: every record the query accepts, re-evaluated on every write
|
|
61
|
+
* that changes the answer, so a screen never recomputes ids and never re-subscribes to keep up.
|
|
62
|
+
* Omitting `query` matches everything.
|
|
63
|
+
*/
|
|
64
|
+
export const useStoreList = <T extends ResourceRecord = ResourceRecord>(
|
|
65
|
+
opts?: UseStoreListOptions<T>
|
|
66
|
+
): StateModel<T>[] => {
|
|
67
|
+
const context = useContext()
|
|
68
|
+
const resource = useMemo(() => context.getStateResource<T>(opts?.resource), [context, opts?.resource])
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
/**
|
|
71
|
+
* The subscription is keyed on the query's CONTENT, not on whether one was given: a screen
|
|
72
|
+
* that narrows its filter has to re-subscribe, and keyed on identity it kept answering the
|
|
73
|
+
* first filter it ever saw with nothing to say the list had gone stale.
|
|
74
|
+
*/
|
|
75
|
+
const key = JSON.stringify([opts?.query ?? null, opts?.sort ?? null])
|
|
76
|
+
const store = useMemo(
|
|
77
|
+
() => bind<StateModel<T>[]>(
|
|
78
|
+
listener => resource.query(opts?.query, listener, { sort: opts?.sort })
|
|
79
|
+
),
|
|
80
|
+
[resource, key]
|
|
81
|
+
)
|
|
71
82
|
|
|
72
|
-
return
|
|
83
|
+
return useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot)
|
|
73
84
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PropsWithChildren, FC, DependencyList } from 'react'
|
|
2
2
|
import type { AbstractRequest } from '@owlmeans/entrypoint'
|
|
3
3
|
import type { ClientConfig, ClientContext as BasicClientContext } from '@owlmeans/client-context'
|
|
4
|
+
import type { Criteria, ResourceRecord, Sort } from '@owlmeans/resource'
|
|
4
5
|
import type { StateResourceAppend } from '@owlmeans/state'
|
|
5
6
|
import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
|
|
6
7
|
import type { DebugServiceAppend, ModalServiceAppend } from './components/types.js'
|
|
@@ -35,10 +36,10 @@ export interface AppProps extends PropsWithChildren {
|
|
|
35
36
|
noRouter?: boolean
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
export interface RoutedComponent<ExtraProps = {}> extends FC<PropsWithChildren<
|
|
39
|
+
export interface RoutedComponent<ExtraProps = {}> extends FC<PropsWithChildren<EntrypointContextParams & ExtraProps>> {
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
export interface
|
|
42
|
+
export interface EntrypointContextParams<T extends {} = {}> {
|
|
42
43
|
alias: string
|
|
43
44
|
params: AbstractRequest<T>['params']
|
|
44
45
|
path: string
|
|
@@ -79,3 +80,12 @@ export interface UseValueParams<T> {
|
|
|
79
80
|
default?: T
|
|
80
81
|
deps?: DependencyList
|
|
81
82
|
}
|
|
83
|
+
|
|
84
|
+
/** What `useStoreList` subscribes to. Every field is optional; nothing at all means everything. */
|
|
85
|
+
export interface UseStoreListOptions<T extends ResourceRecord = ResourceRecord> {
|
|
86
|
+
/** The live query. Omitted or `{}` matches every record the resource holds. */
|
|
87
|
+
query?: Criteria<T>
|
|
88
|
+
sort?: Sort<T>[]
|
|
89
|
+
/** Which state resource to read; the context's default one when omitted. */
|
|
90
|
+
resource?: string
|
|
91
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
|
|
2
2
|
import { createContext } from 'react'
|
|
3
|
-
import type {
|
|
3
|
+
import type { EntrypointContextParams, ClientContext } from '../types.js'
|
|
4
4
|
import type { ClientConfig } from '@owlmeans/client-context'
|
|
5
5
|
|
|
6
6
|
type Config = ClientConfig
|
|
7
7
|
interface Context<C extends Config = Config> extends ClientContext<C> { }
|
|
8
8
|
|
|
9
|
-
export const
|
|
9
|
+
export const EntrypointContext = createContext<EntrypointContextParams>({
|
|
10
10
|
alias: '',
|
|
11
11
|
params: {},
|
|
12
12
|
path: '',
|
package/src/utils/index.ts
CHANGED
package/src/utils/route.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FC, PropsWithChildren, ReactElement } from 'react'
|
|
2
|
-
import type {
|
|
2
|
+
import type { EntrypointContextParams, RoutedComponent, ClientContext } from '../types.js'
|
|
3
3
|
import { isValidElement, memo, useEffect } from 'react'
|
|
4
4
|
import type { ClientEntrypoint } from '@owlmeans/client-entrypoint'
|
|
5
5
|
import { provideRequest } from '@owlmeans/client-entrypoint'
|
|
@@ -18,7 +18,7 @@ export const createRouteRenderer: (params: RendererParams) => FC = ({ context, m
|
|
|
18
18
|
useEffect(() => {
|
|
19
19
|
if (module.guards != null) {
|
|
20
20
|
const guards = module.getGuards().map(guard => context.service<GuardService>(guard))
|
|
21
|
-
const request = provideRequest(module.alias, module.
|
|
21
|
+
const request = provideRequest(module.alias, module.path())
|
|
22
22
|
const reply = provideResponse()
|
|
23
23
|
let canceled = false
|
|
24
24
|
Promise.all(guards.map(async guard => {
|
|
@@ -41,7 +41,7 @@ export const createRouteRenderer: (params: RendererParams) => FC = ({ context, m
|
|
|
41
41
|
}, module.guards ?? [])
|
|
42
42
|
|
|
43
43
|
let Renderer: HandledRenderer<{}> = module.handle?.({
|
|
44
|
-
alias: module.alias, path: module.
|
|
44
|
+
alias: module.alias, path: module.path(),
|
|
45
45
|
params, body: {}, headers: {}, query: {},
|
|
46
46
|
}, reply) as HandledRenderer<{}>
|
|
47
47
|
|
|
@@ -55,8 +55,8 @@ export const createRouteRenderer: (params: RendererParams) => FC = ({ context, m
|
|
|
55
55
|
const Outlet = context.router().outlet()
|
|
56
56
|
if (isComponent(Renderer)) {
|
|
57
57
|
const EnsuredRenderer = memo(Renderer) as RoutedComponent
|
|
58
|
-
const props:
|
|
59
|
-
context, params, alias: module.
|
|
58
|
+
const props: EntrypointContextParams = {
|
|
59
|
+
context, params, alias: module.alias, path: module.path()
|
|
60
60
|
}
|
|
61
61
|
if (hasChildren) {
|
|
62
62
|
return <EnsuredRenderer {...props}><Outlet /></EnsuredRenderer>
|
package/src/utils/router.ts
CHANGED
|
@@ -7,8 +7,8 @@ import type { ClientContext } from '../types.js'
|
|
|
7
7
|
type Config = ClientConfig
|
|
8
8
|
interface Context<C extends Config = Config> extends ClientContext<C> { }
|
|
9
9
|
|
|
10
|
-
export const
|
|
11
|
-
const
|
|
10
|
+
export const buildEntrypointTree = <R, C extends Config = Config, T extends Context<C> = Context<C>>(context: T): EntrypointTree<R> => {
|
|
11
|
+
const entrypoints = context.entrypoints<ClientEntrypoint<R>>().filter(
|
|
12
12
|
module => module.route.route.type === AppType.Frontend
|
|
13
13
|
&& (module.sticky || module.route.route.service == null
|
|
14
14
|
|| module.route.route.service === context.cfg.service)
|
|
@@ -17,8 +17,8 @@ export const buildModuleTree = <R, C extends Config = Config, T extends Context<
|
|
|
17
17
|
const flatTree = new Map<ClientEntrypoint<R>, ClientEntrypoint<R>[]>()
|
|
18
18
|
const roots: ClientEntrypoint<R>[] = []
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
const parentAlias = module.
|
|
20
|
+
entrypoints.forEach(module => {
|
|
21
|
+
const parentAlias = module.route.route.parent
|
|
22
22
|
if (parentAlias == null) {
|
|
23
23
|
roots.push(module)
|
|
24
24
|
} else {
|
|
@@ -29,18 +29,18 @@ export const buildModuleTree = <R, C extends Config = Config, T extends Context<
|
|
|
29
29
|
}
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
(tree, module) => tree.set(module,
|
|
32
|
+
const reduceEntrypoints = (entrypoints: ClientEntrypoint<R>[]): EntrypointTree<R> => entrypoints.reduce(
|
|
33
|
+
(tree, module) => tree.set(module, reduceEntrypoints(flatTree.get(module) ?? [])), new Map()
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
return
|
|
36
|
+
return reduceEntrypoints(roots)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export const
|
|
39
|
+
export const visitEntrypointTree = async <T, R>(tree: EntrypointTree<T>, visitor: EntrypointTreeVisitor<T, R>): Promise<R[]> =>
|
|
40
40
|
Array.from(tree.entries()).reduce<Promise<R[]>>(
|
|
41
41
|
async (result, [module, tree], _, source) => [
|
|
42
42
|
...(await result),
|
|
43
|
-
await visitor(module, await
|
|
43
|
+
await visitor(module, await visitEntrypointTree(tree, visitor), source.length === 1)
|
|
44
44
|
], Promise.resolve([]))
|
|
45
45
|
|
|
46
46
|
export const initializeRouter = async (context: Context) => {
|
|
@@ -52,9 +52,9 @@ export const initializeRouter = async (context: Context) => {
|
|
|
52
52
|
return await model.resolve(context)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export interface
|
|
55
|
+
export interface EntrypointTreeVisitor<T, R> {
|
|
56
56
|
(module: ClientEntrypoint<T>, children: R[], alone: boolean): Promise<R>
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
interface
|
|
59
|
+
interface EntrypointTree<T> extends Map<ClientEntrypoint<T>, EntrypointTree<T>> {
|
|
60
60
|
}
|
package/build/module.d.ts
DELETED
package/build/module.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAErD,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,EAAE,GAAG,EAAE,OAAoC,mBAAmB,CAAC,CAAC,CAAC,CAAA"}
|
package/build/module.js
DELETED
package/build/module.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAClC,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAGhD,MAAM,CAAC,MAAM,SAAS,GAAG,GAAsB,EAAE,CAAC,UAAU,CAAC,aAAa,CAA2B,CAAA"}
|
package/build/utils/module.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../src/utils/module.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAiB,MAAM,aAAa,CAAA;AAMrE,eAAO,MAAM,aAAa,kDAKxB,CAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"module.js","sourceRoot":"","sources":["../../src/utils/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAOrC,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAsB;IAC9D,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;IACV,IAAI,EAAE,EAAE;IACR,OAAO,EAAE,EAAa;CACvB,CAAC,CAAA"}
|
package/src/module.ts
DELETED