@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.
Files changed (75) hide show
  1. package/README.md +34 -10
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/client/SKILL.md +25 -11
  4. package/build/components/modal.d.ts.map +1 -1
  5. package/build/components/modal.js.map +1 -1
  6. package/build/components/toggle.d.ts.map +1 -1
  7. package/build/context.d.ts.map +1 -1
  8. package/build/context.js +1 -3
  9. package/build/context.js.map +1 -1
  10. package/build/debug.d.ts.map +1 -1
  11. package/build/entrypoint.d.ts +3 -0
  12. package/build/entrypoint.d.ts.map +1 -0
  13. package/build/entrypoint.js +4 -0
  14. package/build/entrypoint.js.map +1 -0
  15. package/build/errors.d.ts.map +1 -1
  16. package/build/errors.js.map +1 -1
  17. package/build/helper.d.ts.map +1 -1
  18. package/build/helper.js +3 -3
  19. package/build/helper.js.map +1 -1
  20. package/build/index.d.ts +1 -1
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +1 -1
  23. package/build/index.js.map +1 -1
  24. package/build/navigate.d.ts.map +1 -1
  25. package/build/navigate.js +12 -15
  26. package/build/navigate.js.map +1 -1
  27. package/build/router.d.ts.map +1 -1
  28. package/build/router.js +12 -6
  29. package/build/router.js.map +1 -1
  30. package/build/services/debug.d.ts.map +1 -1
  31. package/build/services/debug.js.map +1 -1
  32. package/build/store.d.ts +18 -3
  33. package/build/store.d.ts.map +1 -1
  34. package/build/store.js +56 -55
  35. package/build/store.js.map +1 -1
  36. package/build/types.d.ts +11 -2
  37. package/build/types.d.ts.map +1 -1
  38. package/build/utils/entrypoint.d.ts +3 -0
  39. package/build/utils/entrypoint.d.ts.map +1 -0
  40. package/build/utils/{module.js → entrypoint.js} +2 -2
  41. package/build/utils/entrypoint.js.map +1 -0
  42. package/build/utils/index.d.ts +1 -1
  43. package/build/utils/index.d.ts.map +1 -1
  44. package/build/utils/index.js +1 -1
  45. package/build/utils/index.js.map +1 -1
  46. package/build/utils/route.d.ts.map +1 -1
  47. package/build/utils/route.js +3 -3
  48. package/build/utils/route.js.map +1 -1
  49. package/build/utils/router.d.ts +4 -4
  50. package/build/utils/router.d.ts.map +1 -1
  51. package/build/utils/router.js +8 -8
  52. package/build/utils/router.js.map +1 -1
  53. package/build/value.d.ts.map +1 -1
  54. package/package.json +12 -12
  55. package/src/context.ts +1 -4
  56. package/src/entrypoint.ts +6 -0
  57. package/src/helper.tsx +6 -6
  58. package/src/index.ts +1 -1
  59. package/src/navigate.ts +12 -15
  60. package/src/router.ts +14 -10
  61. package/src/services/debug.ts +2 -2
  62. package/src/store.ts +70 -59
  63. package/src/types.ts +12 -2
  64. package/src/utils/{module.ts → entrypoint.ts} +2 -2
  65. package/src/utils/index.ts +1 -1
  66. package/src/utils/route.tsx +5 -5
  67. package/src/utils/router.ts +11 -11
  68. package/build/module.d.ts +0 -3
  69. package/build/module.d.ts.map +0 -1
  70. package/build/module.js +0 -4
  71. package/build/module.js.map +0 -1
  72. package/build/utils/module.d.ts +0 -3
  73. package/build/utils/module.d.ts.map +0 -1
  74. package/build/utils/module.js.map +0 -1
  75. package/src/module.ts +0 -6
package/src/store.ts CHANGED
@@ -1,73 +1,84 @@
1
- import { useEffect, useId, useMemo, useState } from 'react'
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
- export const useStoreModel: UseStoreHelper = (id, opts) => {
7
- const list = useStoreList(id, opts)
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
- if (list.length < 1) {
10
- throw new SyntaxError('Store always return model and create it if not exists')
20
+ const capture = (notify?: () => void) => (value: V) => {
21
+ current = value
22
+ seeded = true
23
+ notify?.()
11
24
  }
12
25
 
13
- return list[0]
14
- }
26
+ return {
27
+ subscribe: (notify: () => void) => subscribe(capture(notify)),
28
+ snapshot: (): V => {
29
+ if (!seeded) {
30
+ subscribe(capture())()
31
+ }
15
32
 
16
- export const useStoreList: UseStoreListHelper = (ids, opts) => {
17
- if (typeof ids === 'object' && !Array.isArray(ids)) {
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
- params.listen = params.listen ?? true
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(params.resource), [params.resource])
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
- const deps = [
35
- Array.isArray(params.id) ? params.id.join(',') : params.id,
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
- return unsubscribe
68
- }, deps)
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
- useEffect(() => unsubscribe, deps)
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 _return as StateModel<any>[]
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<ModuleContextParams & ExtraProps>> {
39
+ export interface RoutedComponent<ExtraProps = {}> extends FC<PropsWithChildren<EntrypointContextParams & ExtraProps>> {
39
40
  }
40
41
 
41
- export interface ModuleContextParams<T extends {} = {}> {
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 { ModuleContextParams, ClientContext } from '../types.js'
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 ModuleContext = createContext<ModuleContextParams>({
9
+ export const EntrypointContext = createContext<EntrypointContextParams>({
10
10
  alias: '',
11
11
  params: {},
12
12
  path: '',
@@ -1,4 +1,4 @@
1
1
 
2
2
  export * from './router.js'
3
3
  export * from './route.js'
4
- export * from './module.js'
4
+ export * from './entrypoint.js'
@@ -1,5 +1,5 @@
1
1
  import type { FC, PropsWithChildren, ReactElement } from 'react'
2
- import type { ModuleContextParams, RoutedComponent, ClientContext } from '../types.js'
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.getPath())
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.getPath(),
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: ModuleContextParams = {
59
- context, params, alias: module.getAlias(), path: module.getPath()
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>
@@ -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 buildModuleTree = <R, C extends Config = Config, T extends Context<C> = Context<C>>(context: T): ModuleTree<R> => {
11
- const modules = context.entrypoints<ClientEntrypoint<R>>().filter(
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
- modules.forEach(module => {
21
- const parentAlias = module.getParentAlias()
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 reduceModules = (modules: ClientEntrypoint<R>[]): ModuleTree<R> => modules.reduce(
33
- (tree, module) => tree.set(module, reduceModules(flatTree.get(module) ?? [])), new Map()
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 reduceModules(roots)
36
+ return reduceEntrypoints(roots)
37
37
  }
38
38
 
39
- export const visitModuleTree = async <T, R>(tree: ModuleTree<T>, visitor: ModuleTreeVisitor<T, R>): Promise<R[]> =>
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 visitModuleTree(tree, visitor), source.length === 1)
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 ModuleTreeVisitor<T, R> {
55
+ export interface EntrypointTreeVisitor<T, R> {
56
56
  (module: ClientEntrypoint<T>, children: R[], alone: boolean): Promise<R>
57
57
  }
58
58
 
59
- interface ModuleTree<T> extends Map<ClientEntrypoint<T>, ModuleTree<T>> {
59
+ interface EntrypointTree<T> extends Map<ClientEntrypoint<T>, EntrypointTree<T>> {
60
60
  }
package/build/module.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import type { ModuleContextParams } from './types.js';
2
- export declare const useModule: <T extends {} = {}>() => ModuleContextParams<T>;
3
- //# sourceMappingURL=module.d.ts.map
@@ -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
@@ -1,4 +0,0 @@
1
- import { useContext } from 'react';
2
- import { ModuleContext } from './utils/index.js';
3
- export const useModule = () => useContext(ModuleContext);
4
- //# sourceMappingURL=module.js.map
@@ -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"}
@@ -1,3 +0,0 @@
1
- import type { ModuleContextParams } from '../types.js';
2
- export declare const ModuleContext: import("react").Context<ModuleContextParams<{}>>;
3
- //# sourceMappingURL=module.d.ts.map
@@ -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
@@ -1,6 +0,0 @@
1
-
2
- import { useContext } from 'react'
3
- import { ModuleContext } from './utils/index.js'
4
- import type { ModuleContextParams } from './types.js'
5
-
6
- export const useModule = <T extends {} = {}>() => useContext(ModuleContext) as ModuleContextParams<T>