rxfy 0.2.1 → 0.3.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 CHANGED
@@ -1,29 +1,298 @@
1
1
  # rxfy
2
2
 
3
- rxfy (/ɑɹ ɪks faɪ/) stream-based data management. it utilizes rxjs under the hood.
3
+ rxfy (/ɑɹ ɪks faɪ/) is a small library that lets you declare typed models and the states that query them, then access their data as reactive observables. Normalization keeps your app consistent and reactive at no extra cost. Built on RxJS.
4
4
 
5
- # install
5
+ ## Install
6
6
 
7
- `pnpm install rxfy`
7
+ ```bash
8
+ npm install rxfy
9
+ # or: pnpm add rxfy / yarn add rxfy
10
+ ```
11
+
12
+ ## Peer dependencies
13
+
14
+ ```json
15
+ {
16
+ "rxjs": "^7.0.0",
17
+ "zod": "^3.0.0",
18
+ "lodash": "^4.0.0"
19
+ }
20
+ ```
21
+
22
+ ---
23
+
24
+ ## High-level API
25
+
26
+ Define typed state shapes, normalize entities, and mutate state without manual bookkeeping.
27
+
28
+ ### `defineState`
29
+
30
+ Defines a typed state shape: fetch params schema, model fields, and optional mutations.
31
+
32
+ ```ts
33
+ import { z } from "zod";
34
+ import { defineState, array } from "rxfy";
35
+
36
+ const todosState = defineState({
37
+ key: "todos", // stable string identity for the SSR query cache
38
+ params: z.object({ filter: z.enum(["all", "active", "done"]) }),
39
+ model: { todos: array(TodoModel) },
40
+ mutations: {
41
+ addTodo: (prev, todo: Todo) => ({ ...prev, todos: [...prev.todos, todo] }),
42
+ },
43
+ });
44
+ ```
45
+
46
+ Mutation reducers operate on the full fetch shape (entities). When invoked through `useStateData`, rxfy denormalizes the current ids into fresh entities, runs the reducer, and normalizes the result back into model stores and ids.
47
+
48
+ **Signature:**
49
+
50
+ ```ts
51
+ function defineState<TParams, TFields, TMutations>(def: {
52
+ key?: string; // states without a key opt out of SSR caching
53
+ params: z.ZodType<TParams>;
54
+ model: TFields;
55
+ mutations?: TMutations;
56
+ }): StateDescriptor<TParams, ShapeFromFields<TFields>, TMutations>
57
+ ```
58
+
59
+ The normalized query shape (what `data$` emits in `rxfy-react`) is derived as `QueryShapeOf<TShape>`: array fields become `string[]` (entity keys), single fields become `string`.
60
+
61
+ ### `createModel`
62
+
63
+ Creates a typed model descriptor for normalizing and sharing entities across state slices.
64
+
65
+ ```ts
66
+ import { z } from "zod";
67
+ import { createModel } from "rxfy";
68
+
69
+ const TodoModel = createModel(
70
+ z.object({ id: z.string(), title: z.string(), done: z.boolean() }),
71
+ { getKey: (todo) => todo.id, name: "todo" },
72
+ );
73
+ ```
74
+
75
+ **Signature:**
76
+
77
+ ```ts
78
+ function createModel<T>(
79
+ schema: z.ZodType<T>,
80
+ opts: { getKey: (item: T) => string; name?: string },
81
+ ): ModelDescriptor<T>
82
+ ```
83
+
84
+ `name` is the model's stable string identity for SSR; symbols can't cross the server/client boundary, so only named models are included in `dehydrate` output. Models without a name work normally but opt out of SSR serialization (a dev warning fires if they hold data at dehydrate time).
85
+
86
+ ### `array` / `single`
87
+
88
+ Field descriptor helpers that declare whether a `defineState` model field holds an array or a single item.
89
+
90
+ ```ts
91
+ import { array, single } from "rxfy";
92
+
93
+ const userPageState = defineState({
94
+ params: z.object({ userId: z.string() }),
95
+ model: {
96
+ user: single(UserModel), // one item
97
+ friends: array(UserModel), // array of items
98
+ },
99
+ });
100
+ ```
101
+
102
+ **Signatures:**
103
+
104
+ ```ts
105
+ function array<T>(model: ModelDescriptor<T>): FieldDescriptor<T[]>
106
+ function single<T>(model: ModelDescriptor<T>): FieldDescriptor<T>
107
+ ```
108
+
109
+ ### `createModelRegistry` / `createModelStore`
110
+
111
+ Low-level normalized storage. In React apps these are wired automatically by `StoreProvider` from `rxfy-react`; use directly for non-React or custom setups.
112
+
113
+ ```ts
114
+ import { z } from "zod";
115
+ import { createModel, createModelRegistry } from "rxfy";
116
+
117
+ const UserModel = createModel(
118
+ z.object({ id: z.string(), name: z.string() }),
119
+ { getKey: (u) => u.id },
120
+ );
121
+
122
+ const registry = createModelRegistry();
123
+ const users = registry.model(UserModel);
124
+
125
+ users.set("1", { id: "1", name: "Alice" });
126
+ users.get("1").subscribe(console.log); // emits { id: "1", name: "Alice" }
127
+ // Note: get() on a key that has never had set() called returns an Observable that
128
+ // never emits until set() or setMany() is called for that key.
129
+ ```
130
+
131
+ **Signatures:**
132
+
133
+ ```ts
134
+ function createModelRegistry(): IModelRegistry
135
+ // IModelRegistry: {
136
+ // model<T>(descriptor: ModelDescriptor<T>): ModelStore<T>;
137
+ // queries: QueryCache; // SSR query cache (fulfilled/rejected entries)
138
+ // namedStores(): ReadonlyMap<string, ModelStore<any>>;
139
+ // stores(): { descriptor; store }[];
140
+ // stashHydration(name: string, entities: Record<string, unknown>): void;
141
+ // }
142
+
143
+ function createModelStore<T>(descriptor: ModelDescriptor<T>): ModelStore<T>
144
+ // ModelStore<T>: {
145
+ // get(key: string): Observable<T>; // reactive read; emits on every change
146
+ // set(key, val): void; // write one entity
147
+ // setMany(items): void; // write many; key derived via getKey
148
+ // getValue(key: string): T | undefined; // synchronous read of the latest value
149
+ // entity(key: string): IAtom<T>; // writable handle over one entity's cell
150
+ // valueEntries(): [string, T][]; // snapshot of all loaded [key, value] pairs
151
+ // added$: Observable<string>; // a key, the first time its entity appears
152
+ // }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## SSR
158
+
159
+ The registry round-trips across the server/client boundary: queries serialize as normalized ids, named model stores serialize their entities. The React side (`rxfy-react`) drives fetching and ingestion; these are the core primitives it builds on.
160
+
161
+ ### `dehydrate` / `hydrate`
8
162
 
9
- # api
163
+ ```ts
164
+ import { createModelRegistry, dehydrate, hydrate } from "rxfy";
165
+
166
+ // server: after rendering settles
167
+ const state = dehydrate(registry);
168
+ // { queries: { "todos:{...}": { status: "fulfilled", value: { todos: ["1"] } } },
169
+ // models: { todo: { "1": { id: "1", title: "..." } } } }
10
170
 
11
- - Atom BehaviorSubject successor
12
- - Lens — lensed atom with getter and setter
13
- - Store — where all the data stored
14
- - Edge — data handler and accessor
171
+ // client: into a fresh registry before first render
172
+ hydrate(clientRegistry, state);
173
+ ```
15
174
 
16
- # example
175
+ **Signatures:**
17
176
 
18
177
  ```ts
19
- import PQueue from "p-queue";
20
- import { of } from "rxjs";
21
- import { createAtom, createState, createStore } from "rxfy";
178
+ function dehydrate(registry: IModelRegistry): DehydratedState
179
+ function hydrate(registry: IModelRegistry, state: DehydratedState): void
22
180
 
23
- const queue = new PQueue({ concurrency: 5 });
24
- const state = createAtom(createState({}));
25
- const store = createStore(queue, state);
26
- const userStore = store.factory("users", (id) => of({ id }));
27
- const user$ = userStore.get("42").toObservable();
28
- user$.subscribe((x) => console.log(x));
181
+ type DehydratedState = {
182
+ queries: Record<string, QueryEntry>;
183
+ models: Record<string, Record<string, unknown>>;
184
+ };
29
185
  ```
186
+
187
+ ### `hydrationScript`
188
+
189
+ Complete inline `<script>` tag pushing a snapshot onto `window.__RXFY_SSR__`, the queue the client `StoreProvider` drains automatically, so the client side needs no hydration wiring. Inject it into the served HTML before the client entry script.
190
+
191
+ ```ts
192
+ import { dehydrate, hydrationScript } from "rxfy";
193
+
194
+ const html = template.replace("<!--app-state-->", hydrationScript(dehydrate(registry)));
195
+ ```
196
+
197
+ **Signature:**
198
+
199
+ ```ts
200
+ function hydrationScript(state: DehydratedState): string
201
+ // '<script>(window.__RXFY_SSR__=window.__RXFY_SSR__||[]).push({...})</script>'
202
+ ```
203
+
204
+ The payload is embedded via `serializeForHtml` (also exported): JSON with `<` and U+2028/U+2029 escaped so it cannot break out of the script tag.
205
+
206
+ ### Internal primitives
207
+
208
+ `stableStringify`, `normalizeResult`, `denormalizeValue`, `createQueryCache`, `markSync`, `isSyncMarked`, `attachReload`, `getAttachedReload`, `serializeError`, `rehydrateError` are exported because `rxfy-react` consumes them across the package boundary. They are implementation plumbing, not the intended app-facing surface; prefer the APIs above.
209
+
210
+ ---
211
+
212
+ ## Primitive API
213
+
214
+ Lower-level building blocks. Use these for custom reactive patterns or when the high-level API doesn't fit.
215
+
216
+ ### `Atom` / `createAtom`
217
+
218
+ A reactive cell that extends `Observable<T>` with synchronous `get()`, `set()`, and `modify()`, backed by a `BehaviorSubject`.
219
+
220
+ ```ts
221
+ import { createAtom } from "rxfy";
222
+
223
+ const count = createAtom(0);
224
+
225
+ count.get(); // 0
226
+ count.set(5);
227
+ count.modify((n) => n + 1);
228
+ count.get(); // 6
229
+
230
+ count.subscribe((n) => console.log(n)); // emits current value then future changes
231
+ ```
232
+
233
+ **Signature:**
234
+
235
+ ```ts
236
+ function createAtom<T>(value: T): Atom<T>
237
+ // Atom<T>: Observable<T> & { get(): T; set(val: T): void; modify(fn: (val: T) => T): void }
238
+ ```
239
+
240
+ ### `Lens` / `createLens` / `keyLens`
241
+
242
+ A focused, bidirectional view into an `Atom`. Reads and writes propagate in both directions. Uses `lodash.isEqual` for change detection.
243
+
244
+ ```ts
245
+ import { createAtom, createLens, keyLens } from "rxfy";
246
+
247
+ const user = createAtom({ id: "1", name: "Alice" });
248
+ const name = createLens(user, keyLens("name"));
249
+
250
+ name.get(); // "Alice"
251
+ name.set("Bob");
252
+ user.get(); // { id: "1", name: "Bob" }
253
+ ```
254
+
255
+ **Signatures:**
256
+
257
+ ```ts
258
+ function createLens<S, T>(source$: IAtom<S>, lens: ILens<S, T>): Lens<S, T>
259
+
260
+ function keyLens<S, K extends keyof S>(key: K): ILens<S, S[K]>
261
+ // ILens<S, T>: { get(source: S): T; set(current: T, source: S): S }
262
+ ```
263
+
264
+ ### `IWrapped` / `StatusEnum` / helpers
265
+
266
+ Discriminated union for async state, the `IDLE / PENDING / FULFILLED / REJECTED` pattern used throughout rxfy. It is what the query cache holds per key and what `usePending` returns in `rxfy-react`; also available for custom async primitives.
267
+
268
+ ```ts
269
+ import { createIdle, createPending, createFulfilled, createRejected } from "rxfy";
270
+
271
+ createIdle<string>(); // { type: "IDLE" }
272
+ createPending<string>(); // { type: "PENDING" }
273
+ createFulfilled("hi"); // { type: "FULFILLED", value: "hi" }
274
+ createRejected("oops"); // { type: "REJECTED", error: "oops" }
275
+ ```
276
+
277
+ **Signatures:**
278
+
279
+ ```ts
280
+ function createIdle<T>(): IWrapped<T, StatusEnum.IDLE>
281
+ function createPending<T>(): IWrapped<T, StatusEnum.PENDING>
282
+ function createFulfilled<T>(value: T): IWrapped<T, StatusEnum.FULFILLED>
283
+ function createRejected<T>(error: unknown): IWrapped<T, StatusEnum.REJECTED>
284
+ ```
285
+
286
+ | Helper | Returns |
287
+ |---|---|
288
+ | `createIdle<T>()` | `{ type: "IDLE" }` |
289
+ | `createPending<T>()` | `{ type: "PENDING" }` |
290
+ | `createFulfilled<T>(value)` | `{ type: "FULFILLED", value }` |
291
+ | `createRejected<T>(error)` | `{ type: "REJECTED", error }` |
292
+
293
+ ---
294
+
295
+ ## See also
296
+
297
+ - [rxfy-react](../rxfy-react/README.md): React bindings
298
+ - [examples/vite-todo](../../examples/vite-todo): full working example