@owlmeans/state 0.1.18-rc.0 → 0.1.18-rc.10

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 (45) hide show
  1. package/README.md +90 -44
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/state/SKILL.md +202 -19
  4. package/build/errors.d.ts +12 -5
  5. package/build/errors.d.ts.map +1 -1
  6. package/build/errors.js +16 -13
  7. package/build/errors.js.map +1 -1
  8. package/build/helper.d.ts +16 -0
  9. package/build/helper.d.ts.map +1 -0
  10. package/build/helper.js +14 -0
  11. package/build/helper.js.map +1 -0
  12. package/build/index.d.ts +2 -1
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +2 -1
  15. package/build/index.js.map +1 -1
  16. package/build/resource.d.ts +11 -4
  17. package/build/resource.d.ts.map +1 -1
  18. package/build/resource.js +332 -151
  19. package/build/resource.js.map +1 -1
  20. package/build/types.d.ts +98 -29
  21. package/build/types.d.ts.map +1 -1
  22. package/build/utils/model.d.ts +22 -2
  23. package/build/utils/model.d.ts.map +1 -1
  24. package/build/utils/model.js +26 -22
  25. package/build/utils/model.js.map +1 -1
  26. package/package.json +5 -4
  27. package/src/errors.ts +16 -13
  28. package/src/helper.ts +17 -0
  29. package/src/index.ts +2 -1
  30. package/src/resource.ts +407 -166
  31. package/src/types.ts +103 -30
  32. package/src/utils/model.ts +48 -28
  33. package/tests/resource.spec.ts +419 -0
  34. package/tsconfig.json +6 -1
  35. package/build/.gitkeep +0 -0
  36. package/build/consts.d.ts +0 -3
  37. package/build/consts.d.ts.map +0 -1
  38. package/build/consts.js +0 -3
  39. package/build/consts.js.map +0 -1
  40. package/build/utils/index.d.ts +0 -2
  41. package/build/utils/index.d.ts.map +0 -1
  42. package/build/utils/index.js +0 -2
  43. package/build/utils/index.js.map +0 -1
  44. package/src/consts.ts +0 -3
  45. package/src/utils/index.ts +0 -2
package/README.md CHANGED
@@ -1,85 +1,131 @@
1
1
  # @owlmeans/state
2
2
 
3
- In-memory reactive state management with subscription-based updates for OwlMeans apps.
3
+ The framework's client store: an in-memory `Resource` with live subscriptions.
4
4
 
5
5
  ## Overview
6
6
 
7
- - `createStateResource()` creates an in-memory store that implements the `Resource<T>` interface
8
- - Subscriptions trigger listeners whenever records are created, updated, or deleted
9
- - Used on the client to hold UI state (projects, stories, thinking journal entries) as reactive records
10
- - `DEFAULT_ID` (`'_default'`) is the conventional ID for single-record resources
7
+ - `appendStateResource(context, alias, config?)` registers a store ON the context, so a screen, a
8
+ service and a guard all reach the same records through the same container
9
+ - Reads and writes are the ordinary `Resource<T>` vocabulary `get`, `load`, `list`, `count`,
10
+ `create`, `update`, `save`, `delete`, `take`, `purge` — with the same criteria language the
11
+ server resources speak
12
+ - `watch` follows one record and `query` follows a live set; both hand their listener a value
13
+ synchronously, which is what lets React render from them without a loading frame
14
+ - A subscription READS the store. Watching an id the store knows nothing about creates nothing;
15
+ the model it answers with is `empty`
11
16
 
12
17
  ## Installation
13
18
 
14
19
  ```bash
15
- bun add @owlmeans/state
20
+ bun add @owlmeans/state@^0.1.18-rc.9
16
21
  ```
17
22
 
18
23
  ## Usage
19
24
 
20
- Create and register a state resource in context setup:
25
+ Register a store per record type, and name it once with the type attached:
21
26
 
22
27
  ```typescript
23
- import { createStateResource } from '@owlmeans/state'
28
+ import { appendStateResource, stateAlias } from '@owlmeans/state'
24
29
 
25
- // In context.ts
26
- context.registerResource(createStateResource<ProjectState>('project-state'))
30
+ export const TASKS = stateAlias<Task>('tasks')
31
+
32
+ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T => {
33
+ const context = makeClientContext<C, T>(cfg)
34
+ appendStateResource<C, T, Task>(context, TASKS)
35
+
36
+ return context
37
+ }
38
+
39
+ const tasks = context.getStateResource(TASKS) // StateResource<Task>
27
40
  ```
28
41
 
29
- Subscribe to state changes in a service or component:
42
+ Every client context already carries one default `state` resource, so a store is only registered
43
+ when records of different kinds must not share an id space.
44
+
45
+ Read it from React with the hooks in [`@owlmeans/client`](../client):
30
46
 
31
47
  ```typescript
32
- import { DEFAULT_ID } from '@owlmeans/state'
33
- import type { StateModel, StateResource } from '@owlmeans/state'
34
-
35
- const resource = context.resource<StateResource<ProjectState>>('project-state')
36
-
37
- const [unsubscribe] = resource.subscribe({
38
- id: DEFAULT_ID,
39
- listener: (models) => {
40
- const [model] = models
41
- console.log('project updated:', model.record)
42
- }
43
- })
48
+ import { useStoreList, useStoreModel } from '@owlmeans/client'
49
+
50
+ const task = useStoreModel<Task>(id, 'tasks') // one record, live
51
+ const open = useStoreList<Task>({ query: { status: 'open' }, resource: 'tasks' })
44
52
  ```
45
53
 
46
- Update state and commit:
54
+ Write through the resource, or through the model a subscription handed you:
47
55
 
48
56
  ```typescript
49
- const [model] = resource.subscribe({ id: projectId, listener: ... })[1]
50
- model.update({ status: 'active' })
51
- model.commit() // triggers listeners
57
+ const tasks = context.getStateResource(TASKS)
58
+
59
+ await tasks.save(record) // create or replace
60
+ await tasks.replace(fromTheServer) // write these, drop everything else
61
+ await tasks.purge({ status: 'done' })
62
+ await tasks.clear()
63
+
64
+ model.update({ status: 'done' }) // merge and write in one step
52
65
  ```
53
66
 
54
67
  ## API
55
68
 
56
- ### `createStateResource<T>(alias?): StateResource<T>`
69
+ ### `createStateResource<T>(alias?, config?): StateResource<T>`
70
+
71
+ The bare factory, when the resource is registered by hand. `appendStateResource` is the usual way.
72
+
73
+ ### `StateConfig<T>`
57
74
 
58
- Creates an in-memory resource with subscription support. Registers under `alias` (default: `'state'`).
75
+ | Field | Meaning |
76
+ |-------|---------|
77
+ | `id` | The field records are keyed by. Defaults to `id` |
78
+ | `single` | The resource holds exactly ONE record, which needs no id — the current user, the active session, a wizard being filled in |
79
+ | `default` | What `StateModel.record` shows while the model is empty |
59
80
 
60
- ### `StateResource<T>` (extends `Resource<T>`)
81
+ ### `StateResource<T>` (extends `Resource<T>`, `PubSubResource<StateEvent<T>>`)
61
82
 
62
- - `subscribe(params): [unsubscribe, StateModel<T>[]]` — subscribe to record changes; returns current records
63
- - `listen(listener)` global listener for any change in the resource
64
- - `erase()` — clear all records
83
+ - `replace(records)` — write every record given and drop every record the list does not name, which
84
+ is the shape of "the server just told us what exists"
85
+ - `clear()` — drop everything
86
+ - `watch(id, listener): () => void` — follow one record. `undefined` addresses the one record of a
87
+ `single` resource and throws `StateConfigError.NonSingle` on any other
88
+ - `query(where, listener, opts?): () => void` — follow a live set, re-evaluated on every write that
89
+ changes the answer. `undefined` matches everything
90
+ - `publish(event, channel?)` / `subscribe(handler, opts?)` — the change stream. Every write
91
+ announces itself as a `StateEvent` on the default channel
92
+
93
+ Reads are unpaged: `list()` returns the whole store, and `list(where, { page })` without a `size`
94
+ is refused rather than silently answering with everything. Writes take no `ttl` — nothing here
95
+ expires.
65
96
 
66
97
  ### `StateModel<T>`
67
98
 
68
- - `record: T` — the current record
69
- - `update(data?)` merge partial data into the record
70
- - `commit(force?)` — apply changes and notify subscribers
71
- - `clear()` — remove the record
99
+ - `id` / `empty` / `record` `empty` is what "nothing loaded yet" looks like; `record` is the
100
+ configured `default` while it is true
101
+ - `update(patch)` — merge and write in one step
102
+ - `commit()` — write what `record` currently holds, including a default not yet stored
103
+ - `clear()` — delete the record
72
104
 
73
- ### `DEFAULT_ID`
105
+ `record` is a snapshot: assigning into it changes nothing anyone else can see. `update` is how a
106
+ change reaches the store and every other subscriber.
74
107
 
75
- ```typescript
76
- const DEFAULT_ID = '_default' // conventional ID for single-item resources
77
- ```
108
+ ### `stateAlias<T>(alias)`
109
+
110
+ An alias that remembers the record type it addresses, so `getStateResource(TASKS)` is typed without
111
+ repeating `<Task>` at every call site. It is the plain string at runtime.
112
+
113
+ ### `StateConfigError`
114
+
115
+ `NonSingle` — a record was addressed without an id on a resource that holds many.
116
+ `NoId` — a write carried no value for the id field, and nothing here mints one.
117
+
118
+ ## Criteria
119
+
120
+ The criteria language, the operators and the in-memory engine (`matchCriteria`, `filterRecords`,
121
+ `sortRecords`, `firstMatch`, `applyQuery`) all live in
122
+ [`@owlmeans/resource`](../resource) — one filter object means the same thing whether it is
123
+ evaluated here or by a relational store.
78
124
 
79
125
  ## Related Packages
80
126
 
81
- - [`@owlmeans/resource`](../resource) — `Resource<T>` interface implemented by StateResource
82
- - [`@owlmeans/client`](../client) — `useStoreModel` / `useStoreList` React hooks for state resources
127
+ - [`@owlmeans/resource`](../resource) — the `Resource<T>` contract and the criteria engine
128
+ - [`@owlmeans/client`](../client) — `useStoreModel` / `useStoreList` React hooks
83
129
 
84
130
  <!-- owlmeans:agent-guidance:start -->
85
131
  ## Agent guidance
@@ -89,7 +135,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
89
135
  your project's skill store (`.agents/skills/`):
90
136
 
91
137
  ```sh
92
- npx @owlmeans/agent-skills
138
+ npx @owlmeans/agent-skills@^0.1.18-rc.12
93
139
  ```
94
140
 
95
141
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/state",
4
- "version": "0.1.18-rc.0",
5
- "generatedAt": "2026-08-16T22:20:50.505Z",
4
+ "version": "0.1.18-rc.10",
5
+ "generatedAt": "2026-09-04T22:43:25.442Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: state
3
- description: How to use @owlmeans/state — appendStateResource() to register a state resource on a context, then ctx.getStateResource(alias) to read/write typed application state. Auto-invoked when importing state primitives.
3
+ description: How to use @owlmeans/state — appendStateResource() to register a client state resource on a context, useStoreModel/useStoreList to read it from React, watch/query live subscriptions, and the StateModel commit semantics. Auto-invoked when importing state primitives or building client-side application state.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,40 +8,223 @@ user-invocable: false
8
8
  # @owlmeans/state
9
9
 
10
10
  **Layer:** Core
11
- **Install:** `"@owlmeans/state": "^0.1.18-rc.0"` in `dependencies`
11
+ **Install:** `"@owlmeans/state": "^0.1.18-rc.10"` in `dependencies`
12
+
13
+ The framework's client store. A state resource is a `Resource` like any other, registered **on the
14
+ context** — which is what separates it from a store held beside the app: a screen, a service and a
15
+ guard all reach the same records through the same container. Reads and writes are the resource
16
+ vocabulary of [[resource]]; `watch` and `query` are the live half.
12
17
 
13
18
  ## Key Exports
14
19
 
15
20
  | Export | Description |
16
21
  |--------|-------------|
17
- | `appendStateResource<C, T>(context, alias)` | Register a state resource on the context |
18
- | `StateResource<T>` types | Typed state container interface |
19
- | Errors | Typed state errors |
20
- | Constants | Default state aliases |
22
+ | `appendStateResource<C, T, R>(context, alias?, cfg?)` | Register a state resource on the context |
23
+ | `createStateResource<T>(alias?, cfg?)` | The bare factory, when you register it yourself |
24
+ | `stateAlias<T>(alias)` | Name a store once with the record type attached — `StateAlias<T>` |
25
+ | `StateResource<T>` | The resource interface — full CRUD plus `replace`, `clear`, `watch`, `query`, `publish`/`subscribe` |
26
+ | `StateModel<T>` | The subscribed wrapper — `id`, `empty`, `record`, `update`, `commit`, `clear` |
27
+ | `StateConfig<T>` | How the store is keyed — `id`, `single`, `default`. Readable back as `resource.config` |
28
+ | `StateEvent<T>` | What a change looks like on the wire — `{ type: 'set' \| 'remove', records }` |
29
+ | `createStateModel(binding)` / `StateModelBinding<T>` | Wrap a record — or its absence — as a model, for a store of your own |
30
+ | `StateResourceAppend` / `GetStateResource` | The `getStateResource` mixin `appendStateResource` installs |
31
+ | `StateConfigError` | `NoId` — a write with no value for the key field on a store that holds many records. `NonSingle` is declared beside it for an id-less address on a many-record store, but every such path either answers an empty model (`watch`) or raises `NoId`, so `NoId` is the one a caller meets |
32
+
33
+ The criteria evaluator (`matchCriteria`, `filterRecords`, `sortRecords`, `applyQuery`) lives in
34
+ **`@owlmeans/resource`** — the same engine the store runs on, for filtering a list you already hold.
21
35
 
22
- ## Usage
36
+ The React hooks live in **`@owlmeans/client`** — `useStoreModel`, `useStoreList`. They are not
37
+ re-exported by `@owlmeans/web-client`, so import them from `@owlmeans/client` directly.
23
38
 
24
- Append a state resource at context construction time, expose a typed accessor:
39
+ ## Registering
40
+
41
+ Every client context already carries one default state resource, so `useStoreModel(id)` works with
42
+ no setup at all. Register a named one per entity when records of different kinds must not share an
43
+ id space:
25
44
 
26
45
  ```typescript
27
- import { appendStateResource } from '@owlmeans/state'
46
+ import { appendStateResource, stateAlias } from '@owlmeans/state'
28
47
 
29
- export const VIB_PROJECT_STATE = 'vib-project-state'
48
+ export const TASKS = stateAlias<Task>('task-state')
30
49
 
31
50
  export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T => {
32
- const context = makeBasicContext<C, T>(cfg)
33
- appendStateResource<C, T>(context, VIB_PROJECT_STATE)
34
- context.projectStore = () => context.getStateResource(VIB_PROJECT_STATE)
51
+ const context = makeClientContext<C, T>(cfg)
52
+ appendStateResource<C, T, Task>(context, TASKS)
35
53
  return context
36
54
  }
55
+ ```
56
+
57
+ Reach it with `context.getStateResource(TASKS)` — a `StateAlias<T>` carries the record type, so the
58
+ accessor is typed without repeating `<Task>` at every call site. `getStateResource()` with no alias
59
+ answers the context's own default store (`state`), so always name the alias for a store you
60
+ appended. `appendStateResource` is idempotent: appending the same alias twice keeps the resource
61
+ already there, so a setup that runs more than once does not drop what the store has collected.
62
+
63
+ `StateConfig` decides how the store is keyed and what it shows before anything is loaded — all of it
64
+ optional:
65
+
66
+ | Field | Meaning |
67
+ |---|---|
68
+ | `id` | The field records are keyed by. Defaults to `id`. |
69
+ | `single` | The store holds exactly ONE record, which therefore needs no id — the current user, the active session, a wizard being filled in. It is what makes `watch(undefined, …)` (and so `useStoreModel()` with no id) answerable. |
70
+ | `default` | `() => T` — what `model.record` shows while the model is empty. A screen binds to it instead of guarding every field, and the store still holds nothing. |
71
+
72
+ ## Reading it from React
73
+
74
+ ```typescript
75
+ import { useStoreList, useStoreModel } from '@owlmeans/client'
76
+
77
+ // One record, by id. Re-renders whenever that record changes.
78
+ const task = useStoreModel<Task>(id, TASKS)
79
+ task.record.title
80
+
81
+ // A LIVE QUERY. Re-renders whenever any write changes which records match.
82
+ const open = useStoreList<Task>({ query: { status: 'open' }, resource: TASKS })
83
+ open.map(model => model.record.title)
84
+
85
+ // Everything in the store, newest first.
86
+ const all = useStoreList<Task>({ sort: [{ field: 'createdAt', order: 'desc' }], resource: TASKS })
87
+ ```
88
+
89
+ A query subscription creates nothing and re-evaluates on every create, update and delete — a list
90
+ screen never recomputes ids and never re-subscribes to keep up. The criteria object is compared by
91
+ content, so a filter that changes narrows the list. An omitted `query` matches everything.
92
+
93
+ **"Nothing loaded yet" is `model.empty`.** An id the store knows nothing about yields a model whose
94
+ `empty` is true, and nothing is written into the store on the way — so `useStoreModel` never throws
95
+ for missing data, and a screen bound to an unknown id does not put a blank row into every list
96
+ reading the same store:
97
+
98
+ ```typescript
99
+ if (task.empty) {
100
+ return <Spinner/>
101
+ }
102
+ ```
103
+
104
+ `model.record` is still readable while empty: it holds the resource's configured `default`, or `{}`
105
+ when there is none. Calling `model.update(...)` or `model.commit()` on an empty model writes it —
106
+ including the default it was showing.
37
107
 
38
- // Later, in a component or handler:
39
- const store = ctx.projectStore()
40
- const current = await store.get('current')
108
+ An ABSENT id answers the same way. A screen binds to `useStoreModel(project.record.id)` while the
109
+ project is still loading, so a missing id is a rendering state rather than a mistake: on a listed
110
+ store it watches nothing and reports an empty model, and on a `single` store it addresses that
111
+ store's sole record. The empty model it hands back is one shared instance, so a React subscriber
112
+ does not see a new value on every render — and writing through it throws `StateConfigError`
113
+ (`NoId`), because a caller writing with no id has lost track of which record it meant.
114
+
115
+ ## Writing to it
116
+
117
+ The server is the source of truth; the store is what the screen reads. Fetch, then write what came
118
+ back into the store, and let the subscriptions render it:
119
+
120
+ ```typescript
121
+ const store = ctx.getStateResource(TASKS)
122
+
123
+ const tasks = await ctx.entrypoint<ClientEntrypoint<Task[]>>(TASK_LIST).call()
124
+ await store.replace(tasks)
125
+ ```
126
+
127
+ `replace(records)` makes the store agree with an authoritative list: every record given is written,
128
+ and every record the list does not name is dropped. That is the shape of "the server just told us
129
+ what exists" — saving each record one by one leaves the ones deleted elsewhere behind, and one
130
+ write wakes the subscribers once instead of once per record. The store is rewritten before anything
131
+ is told about it, so a subscriber never sees the half-applied set.
132
+
133
+ For single records: `save` creates or replaces, `create` refuses an id already there, `update`
134
+ requires the record to exist, `delete(id)` removes it and answers with what it removed, `take(id)`
135
+ is the same read but throws when the record is absent, `purge(where)` bulk-deletes (and refuses an
136
+ empty criteria object rather than emptying the store), and `clear()` drops everything. Each one
137
+ notifies every subscriber that cares.
138
+
139
+ On a store that holds many records, a write carrying no value for the key field throws
140
+ `StateConfigError` (`NoId`) — nothing here mints ids. A write carrying a `ttl` throws
141
+ `UnsupportedArgumentError`: the store keeps no expiring records, so a ttl would be silently
142
+ dropped.
143
+
144
+ On a `single` store every write lands in the one slot, so `replace([a, b])` keeps only the last of
145
+ them. The key field is never consulted on the way in: an id-less `save`, `create`, `update` or
146
+ `replace` is filed there normally and the record keeps whatever id it arrived with, or none.
147
+ `create` still refuses a slot already filled and `update` still requires it filled, both naming the
148
+ resource alias rather than an id.
149
+
150
+ `get(id)` / `load(id)` answer the sole record unless it carries a DIFFERENT id: a record stored with
151
+ `id: 'sid'` is a miss for any other name, while a record stored without an id at all — the shape a
152
+ single store invites, since it needs none — answers to every id asked for. Give the record an id
153
+ whenever a screen reads it by one, and treat an id-keyed read on an id-less single store as an
154
+ unconditional hit.
155
+
156
+ ### The commit rule
157
+
158
+ `StateModel.record` is a SNAPSHOT. Assigning to it changes nothing anyone else can see:
159
+
160
+ ```typescript
161
+ model.record.title = 'renamed' // WRONG — a silent no-op, nothing re-renders
162
+ await model.update({ title: 'renamed' }) // RIGHT — merges and commits
163
+ ```
164
+
165
+ `update(patch)` merges and commits in one step — batch several fields into one patch rather than
166
+ writing them one at a time. `commit()` writes what `record` currently holds, which is how an empty
167
+ model bound to a `default` is persisted as it stands. `clear()` deletes the record and leaves the
168
+ model empty again. The working copy is replaced rather than mutated on every write, so the record a
169
+ caller is holding never changes underneath it and two models of the same record stay comparable by
170
+ reference — which is what lets a React subscriber tell a real change from an unrelated one.
171
+
172
+ ## Querying
173
+
174
+ Reads take the same criteria language as the server resources, so a filter written for an endpoint
175
+ means the same thing applied locally:
176
+
177
+ ```typescript
178
+ await store.get(id) // the record, or UnknownRecordError
179
+ await store.load({ status: 'open' }) // the first match, or null
180
+ await store.list({ status: ['open', 'blocked'] }) // { items, total }
181
+ await store.list({ status: 'open' }, { sort: ['createdAt'], size: 20 })
182
+ await store.count({ status: 'open' })
183
+ ```
184
+
185
+ - A bare value is equality; a bare **array means "any of these"**.
186
+ - Operators: `$eq $ne $gt $gte $lt $lte $in $nin $exists $null $like $ilike $regex $startsWith
187
+ $endsWith $between $contains $contained $overlaps`, and `$and $or $not` to combine.
188
+ - A dotted key reaches into the record (`'owner.team'`).
189
+ - `null` matches absence; a criteria value of `undefined` is SKIPPED — an untouched filter must not
190
+ empty the list.
191
+ - `Sort<T>` is a field name (ascending) or `{ field, order: 'asc' | 'desc' }`.
192
+
193
+ `list()` returns `{ items, total }` and is **unpaged**: the store is already in memory and a screen
194
+ reading it expects all of it. Ask for a `size` to page, and `size: 0` still means no limit. A `page`
195
+ with no `size` throws `UnsupportedArgumentError('page-without-size')` — there is no default page
196
+ size to count against.
197
+
198
+ ## Subscribing outside React
199
+
200
+ ```typescript
201
+ const stopOne = store.watch(id, model => { … }) // one record
202
+ const stopMany = store.query({ status: 'open' }, models => { … }) // a live query
203
+ const stopAll = store.query(undefined, models => { … }, { sort: ['createdAt'] })
204
+ ```
205
+
206
+ Both are **synchronous** and both are seeded before they return: the listener is called with the
207
+ current value straight away, then again on every change — including a removal, which reaches a
208
+ `watch` listener as an empty model. `watch(undefined, …)` on a listed store seeds an empty model
209
+ and subscribes to nothing. Each returns its unsubscribe, and a `query` listener is called again
210
+ only when the set of matching models actually changed, so an unrelated write re-renders nothing.
211
+
212
+ Writes announce themselves on the default channel, so `publish` is for what the store cannot know
213
+ it did — a change that arrived from elsewhere, or a channel of a caller's own:
214
+
215
+ ```typescript
216
+ const stop = await store.subscribe(event => { … }) // every write: StateEvent<T>
217
+ await store.publish({ type: 'set', records: [task] }, 'from-socket')
218
+ const once = await store.subscribe(handler, { channel: 'from-socket', once: true, ttl: 60 })
41
219
  ```
42
220
 
43
221
  ## Depends On
44
222
 
45
- - `@owlmeans/resource` — `StateResource` extends `Resource`
46
- - `@owlmeans/context` — for `getStateResource`
47
- - `@owlmeans/error`, `@owlmeans/i18n`
223
+ - `@owlmeans/resource` — `StateResource` extends `Resource` and `PubSubResource`
224
+ - `@owlmeans/context` — `appendContextual`, and the `getStateResource` mixin
225
+
226
+ ## Related
227
+
228
+ - `resource` — the criteria language, paging and the base contract this implements
229
+ - `client` — where the React hooks live; it depends on this package, not the other way round
230
+ - `client-job` — a worked store: a socket feed folded into a state resource
package/build/errors.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  import { ResourceError } from '@owlmeans/resource';
2
- export declare class StateToolingError extends ResourceError {
3
- static typeName: string;
4
- constructor(msg: string);
5
- }
6
- export declare class StateListenerError extends StateToolingError {
2
+ /**
3
+ * The resource was asked for something its {@link StateConfig} does not allow. Both cases are
4
+ * wiring mistakes rather than missing data, so they throw instead of answering with nothing.
5
+ */
6
+ export declare class StateConfigError extends ResourceError {
7
7
  static typeName: string;
8
+ /**
9
+ * A record was addressed without an id on a resource that holds many of them. Only a `single`
10
+ * resource has a record that needs no naming.
11
+ */
12
+ static readonly NonSingle: string;
13
+ /** A write carried no value for the resource's id field, and nothing here mints one. */
14
+ static readonly NoId: string;
8
15
  constructor(msg: string);
9
16
  }
10
17
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,qBAAa,iBAAkB,SAAQ,aAAa;IAClD,OAAuB,QAAQ,SAAqC;IAEpE,YAAY,GAAG,EAAE,MAAM,EAGtB;CACF;AAED,qBAAa,kBAAmB,SAAQ,iBAAiB;IACvD,OAAuB,QAAQ,SAA0C;IAEzE,YAAY,GAAG,EAAE,MAAM,EAGtB;CACF"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,aAAa;IACjD,OAAuB,QAAQ,SAAyC;IAExE;;;OAGG;IACH,gBAAuB,SAAS,EAAE,MAAM,CAAe;IAEvD,wFAAwF;IACxF,gBAAuB,IAAI,EAAE,MAAM,CAAU;IAE7C,YAAY,GAAG,EAAE,MAAM,EAGtB;CACF"}
package/build/errors.js CHANGED
@@ -1,18 +1,21 @@
1
1
  import { ResourceError } from '@owlmeans/resource';
2
- export class StateToolingError extends ResourceError {
3
- static typeName = `${ResourceError.typeName}Tooling`;
2
+ /**
3
+ * The resource was asked for something its {@link StateConfig} does not allow. Both cases are
4
+ * wiring mistakes rather than missing data, so they throw instead of answering with nothing.
5
+ */
6
+ export class StateConfigError extends ResourceError {
7
+ static typeName = `${ResourceError.typeName}StateConfig`;
8
+ /**
9
+ * A record was addressed without an id on a resource that holds many of them. Only a `single`
10
+ * resource has a record that needs no naming.
11
+ */
12
+ static NonSingle = 'non-single';
13
+ /** A write carried no value for the resource's id field, and nothing here mints one. */
14
+ static NoId = 'no-id';
4
15
  constructor(msg) {
5
- super(`tooling:${msg}`);
6
- this.type = StateToolingError.typeName;
16
+ super(`state-config:${msg}`);
17
+ this.type = StateConfigError.typeName;
7
18
  }
8
19
  }
9
- export class StateListenerError extends StateToolingError {
10
- static typeName = `${StateToolingError.typeName}Listener`;
11
- constructor(msg) {
12
- super(`listener:${msg}`);
13
- this.type = StateListenerError.typeName;
14
- }
15
- }
16
- ResourceError.registerErrorClass(StateToolingError);
17
- ResourceError.registerErrorClass(StateListenerError);
20
+ ResourceError.registerErrorClass(StateConfigError);
18
21
  //# sourceMappingURL=errors.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD,MAAM,OAAO,iBAAkB,SAAQ,aAAa;IAC3C,MAAM,CAAU,QAAQ,GAAG,GAAG,aAAa,CAAC,QAAQ,SAAS,CAAA;IAEpE,YAAY,GAAW;QACrB,KAAK,CAAC,WAAW,GAAG,EAAE,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC,QAAQ,CAAA;IACxC,CAAC;CACF;AAED,MAAM,OAAO,kBAAmB,SAAQ,iBAAiB;IAChD,MAAM,CAAU,QAAQ,GAAG,GAAG,iBAAiB,CAAC,QAAQ,UAAU,CAAA;IAEzE,YAAY,GAAW;QACrB,KAAK,CAAC,YAAY,GAAG,EAAE,CAAC,CAAA;QACxB,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC,QAAQ,CAAA;IACzC,CAAC;CACF;AAED,aAAa,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,CAAA;AACnD,aAAa,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,CAAA"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAElD;;;GAGG;AACH,MAAM,OAAO,gBAAiB,SAAQ,aAAa;IAC1C,MAAM,CAAU,QAAQ,GAAG,GAAG,aAAa,CAAC,QAAQ,aAAa,CAAA;IAExE;;;OAGG;IACI,MAAM,CAAU,SAAS,GAAW,YAAY,CAAA;IAEvD,wFAAwF;IACjF,MAAM,CAAU,IAAI,GAAW,OAAO,CAAA;IAE7C,YAAY,GAAW;QACrB,KAAK,CAAC,gBAAgB,GAAG,EAAE,CAAC,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC,QAAQ,CAAA;IACvC,CAAC;CACF;AAED,aAAa,CAAC,kBAAkB,CAAC,gBAAgB,CAAC,CAAA"}
@@ -0,0 +1,16 @@
1
+ import type { ResourceRecord } from '@owlmeans/resource';
2
+ import type { StateAlias } from './types.js';
3
+ /**
4
+ * Name a state resource once, with the record type it holds attached:
5
+ *
6
+ * ```typescript
7
+ * export const TASKS = stateAlias<Task>('tasks')
8
+ * const tasks = context.getStateResource(TASKS) // StateResource<Task>
9
+ * ```
10
+ *
11
+ * The handle is the string itself at runtime — the type rides along only so that every reader of
12
+ * the alias gets the record type without repeating it, and so that a mismatch is a compile error
13
+ * instead of a record shaped like nothing anyone expected.
14
+ */
15
+ export declare const stateAlias: <T extends ResourceRecord>(alias: string) => StateAlias<T>;
16
+ //# sourceMappingURL=helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAE5C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,SAAS,cAAc,SAAS,MAAM,KAAG,UAAU,CAAC,CAAC,CACzD,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Name a state resource once, with the record type it holds attached:
3
+ *
4
+ * ```typescript
5
+ * export const TASKS = stateAlias<Task>('tasks')
6
+ * const tasks = context.getStateResource(TASKS) // StateResource<Task>
7
+ * ```
8
+ *
9
+ * The handle is the string itself at runtime — the type rides along only so that every reader of
10
+ * the alias gets the record type without repeating it, and so that a mismatch is a compile error
11
+ * instead of a record shaped like nothing anyone expected.
12
+ */
13
+ export const stateAlias = (alias) => alias;
14
+ //# sourceMappingURL=helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helper.js","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAA2B,KAAa,EAAiB,EAAE,CACnF,KAAsB,CAAA"}
package/build/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type * from './types.js';
2
- export * from './consts.js';
3
2
  export * from './errors.js';
3
+ export * from './helper.js';
4
4
  export * from './resource.js';
5
+ export * from './utils/model.js';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAE/B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA"}
package/build/index.js CHANGED
@@ -1,4 +1,5 @@
1
- export * from './consts.js';
2
1
  export * from './errors.js';
2
+ export * from './helper.js';
3
3
  export * from './resource.js';
4
+ export * from './utils/model.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,kBAAkB,CAAA"}
@@ -1,6 +1,13 @@
1
+ import type { BasicConfig as Config, BasicContext as Context } from '@owlmeans/context';
1
2
  import type { ResourceRecord } from '@owlmeans/resource';
2
- import type { StateResource, StateResourceAppend } from './types.js';
3
- import type { BasicContext as Context, BasicConfig as Config } from '@owlmeans/context';
4
- export declare const createStateResource: <R extends ResourceRecord>(alias?: string) => StateResource<R>;
5
- export declare const appendStateResource: <C extends Config, T extends Context<C>>(ctx: T, alias?: string) => T & StateResourceAppend;
3
+ import type { StateConfig, StateResource, StateResourceAppend } from './types.js';
4
+ export declare const createStateResource: <T extends ResourceRecord>(alias?: string, cfg?: StateConfig<T>) => StateResource<T>;
5
+ /**
6
+ * Register a state resource on the context and expose `getStateResource`.
7
+ *
8
+ * Idempotent: appending the same alias twice keeps the resource that is already there, so a
9
+ * setup that runs more than once does not drop what the store has collected. The first alias
10
+ * appended is the one `getStateResource()` answers with when it is called without one.
11
+ */
12
+ export declare const appendStateResource: <C extends Config, T extends Context<C>, R extends ResourceRecord = ResourceRecord>(ctx: T, alias?: string, cfg?: StateConfig<R>) => T & StateResourceAppend;
6
13
  //# sourceMappingURL=resource.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAgC,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACtF,OAAO,KAAK,EAAiB,aAAa,EAAE,mBAAmB,EAA2B,MAAM,YAAY,CAAA;AAI5G,OAAO,KAAK,EAAE,YAAY,IAAI,OAAO,EAAE,WAAW,IAAI,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAEvF,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,cAAc,UAAS,MAAM,KAAmB,aAAa,CAAC,CAAC,CA6N5G,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,OACnE,CAAC,UAAS,MAAM,KACpB,CAAC,GAAG,mBAWN,CAAA"}
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,IAAI,MAAM,EAAE,YAAY,IAAI,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAKvF,OAAO,KAAK,EAC2B,cAAc,EAEpD,MAAM,oBAAoB,CAAA;AAG3B,OAAO,KAAK,EACV,WAAW,EAA0B,aAAa,EAAE,mBAAmB,EACxE,MAAM,YAAY,CAAA;AAgCnB,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,cAAc,UACnD,MAAM,QAAgB,WAAW,CAAC,CAAC,CAAC,KAC1C,aAAa,CAAC,CAAC,CA8ZjB,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC9B,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,SAAS,cAAc,GAAG,cAAc,OAC5E,CAAC,UAAS,MAAM,QAAgB,WAAW,CAAC,CAAC,CAAC,KAAG,CAAC,GAAG,mBAa3D,CAAA"}