@flexsurfer/reflex 0.1.23 → 0.1.25

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
@@ -32,11 +32,39 @@ After many years of building applications with re-frame in the ClojureScript wor
32
32
  - [Step-by-Step Tutorial](https://reflex.js.org/docs/quick-start.html)
33
33
  - [Best Practices](https://reflex.js.org/docs/api-reference.html)
34
34
  - [API Reference](https://reflex.js.org/docs/best-practices.html)
35
+ - [AI Context (llms.txt)](./llms.txt) - Compact guide for AI-assisted Reflex project scaffolding and state architecture
35
36
  - [re-frame Documentation](https://day8.github.io/re-frame/re-frame/) - The original and comprehensive guide to understanding the philosophy and patterns
36
37
 
37
38
  - Examples
38
39
  - [TodoMVC](https://github.com/flexsurfer/reflex/tree/main/examples/todomvc) - Classic todo app implementation showcasing core reflex patterns
39
- - [Einbürgerungstest](https://github.com/flexsurfer/einburgerungstest/) - German citizenship test app built with reflex ([Live Demo](https://www.ebtest.org/))
40
+ - [Einbürgerungstest](https://github.com/flexsurfer/einburgerungstest/) - Cross-platform web/mobile app built with reflex ([Live Demo](https://www.ebtest.org/))
41
+ - [StarRupture Planner](https://github.com/flexsurfer/starrupture-planner) - Production planning tool built with reflex ([Live Demo](https://www.starrupture-planner.com/))
42
+
43
+ ## 🤖 Using with AI Assistants
44
+
45
+ Reflex ships an [`llms.txt`](./llms.txt) file — a compact, AI-readable guide covering state architecture, event/effect/subscription patterns, and code generation rules. Point your AI tool at it so it generates idiomatic Reflex code from the start.
46
+
47
+ **Claude Code** — add to `CLAUDE.md` in your project root:
48
+ ```bash
49
+ curl -o CLAUDE.md https://raw.githubusercontent.com/flexsurfer/reflex/main/llms.txt
50
+ ```
51
+
52
+ **Codex (OpenAI)** — add to `AGENTS.md` in your project root:
53
+ ```bash
54
+ curl -o AGENTS.md https://raw.githubusercontent.com/flexsurfer/reflex/main/llms.txt
55
+ ```
56
+ Codex reads project instructions from `AGENTS.md`, so this gives it Reflex-specific architecture and code generation rules for your repo.
57
+
58
+ **Cursor** — create `.cursor/rules/reflex.mdc` and paste the contents, or reference via project rules.
59
+
60
+ **GitHub Copilot** — add to `.github/copilot-instructions.md`:
61
+ ```bash
62
+ curl -o .github/copilot-instructions.md https://raw.githubusercontent.com/flexsurfer/reflex/main/llms.txt
63
+ ```
64
+
65
+ **ChatGPT / Claude.ai Projects** — upload `llms.txt` from your `node_modules/@flexsurfer/reflex/` as a project file, or paste the raw URL into the conversation.
66
+
67
+ The file is also included in the npm package, so after installing Reflex you can find it at `node_modules/@flexsurfer/reflex/llms.txt`.
40
68
 
41
69
  ## 🤝 Contributing
42
70
 
package/dist/index.cjs CHANGED
@@ -127,12 +127,16 @@ function clearHandlers(kind, id) {
127
127
  for (const k in kindToIdToHandler) {
128
128
  kindToIdToHandler[k] = {};
129
129
  }
130
+ clearRootSubSources();
130
131
  } else if (id == null) {
131
132
  if (!(kind in kindToIdToHandler)) {
132
133
  consoleLog("error", `[reflex] Unknown kind: ${kind}`);
133
134
  return;
134
135
  }
135
136
  kindToIdToHandler[kind] = {};
137
+ if (kind === "sub") {
138
+ clearRootSubSources();
139
+ }
136
140
  } else {
137
141
  if (kindToIdToHandler[kind][id]) {
138
142
  delete kindToIdToHandler[kind][id];
@@ -161,6 +165,16 @@ function clearReactions(id) {
161
165
  reactionsRegistry.delete(id);
162
166
  }
163
167
  }
168
+ var rootSubIdBySource = /* @__PURE__ */ new Map();
169
+ function setRootSubSource(subId, sourceKey) {
170
+ rootSubIdBySource.set(sourceKey, subId);
171
+ }
172
+ function getRootSubIdBySource(sourceKey) {
173
+ return rootSubIdBySource.get(sourceKey);
174
+ }
175
+ function clearRootSubSources() {
176
+ rootSubIdBySource.clear();
177
+ }
164
178
  function clearSubs() {
165
179
  clearReactions();
166
180
  clearHandlers("sub");
@@ -230,7 +244,14 @@ function updateAppDbWithPatches(newDb, patches) {
230
244
  const pathSegments = patch.path;
231
245
  if (pathSegments.length > 0) {
232
246
  const rootKey = pathSegments[0];
233
- const subVectorKey = JSON.stringify([rootKey]);
247
+ if (typeof rootKey !== "string") {
248
+ continue;
249
+ }
250
+ const subId = getRootSubIdBySource(rootKey);
251
+ if (!subId) {
252
+ continue;
253
+ }
254
+ const subVectorKey = JSON.stringify([subId]);
234
255
  const reaction = getReaction(subVectorKey);
235
256
  if (reaction) {
236
257
  if (!reaction.isRoot) {
@@ -1089,16 +1110,23 @@ var Reaction = class _Reaction {
1089
1110
  // src/subs.ts
1090
1111
  var KIND4 = "sub";
1091
1112
  var KIND_DEPS = "subDeps";
1113
+ function registerRootSub(id, sourceKey) {
1114
+ const conflictingSubId = getRootSubIdBySource(sourceKey);
1115
+ if (conflictingSubId && conflictingSubId !== id) {
1116
+ consoleLog("error", `[reflex] Subscription with id '${id}' will be overridden. Root key '${sourceKey}' is already used by subscription '${conflictingSubId}'.`);
1117
+ }
1118
+ setRootSubSource(id, sourceKey);
1119
+ registerHandler(KIND4, id, () => getAppDb()[sourceKey]);
1120
+ registerHandler(KIND_DEPS, id, () => []);
1121
+ }
1092
1122
  function regSub(id, computeFn, depsFn, config) {
1093
1123
  if (hasHandler(KIND4, id)) {
1094
1124
  consoleLog("warn", `[reflex] Overriding. Subscription '${id}' already registered.`);
1095
1125
  }
1096
1126
  if (!computeFn) {
1097
- registerHandler(KIND4, id, () => getAppDb()[id]);
1098
- registerHandler(KIND_DEPS, id, () => []);
1127
+ registerRootSub(id, id);
1099
1128
  } else if (typeof computeFn === "string") {
1100
- registerHandler(KIND4, id, () => getAppDb()[computeFn]);
1101
- registerHandler(KIND_DEPS, id, () => []);
1129
+ registerRootSub(id, computeFn);
1102
1130
  } else {
1103
1131
  if (!depsFn) {
1104
1132
  consoleLog("error", `[reflex] Subscription '${id}' has computeFn but missing depsFn. Computed subscriptions must specify their dependencies.`);
package/dist/index.mjs CHANGED
@@ -49,12 +49,16 @@ function clearHandlers(kind, id) {
49
49
  for (const k in kindToIdToHandler) {
50
50
  kindToIdToHandler[k] = {};
51
51
  }
52
+ clearRootSubSources();
52
53
  } else if (id == null) {
53
54
  if (!(kind in kindToIdToHandler)) {
54
55
  consoleLog("error", `[reflex] Unknown kind: ${kind}`);
55
56
  return;
56
57
  }
57
58
  kindToIdToHandler[kind] = {};
59
+ if (kind === "sub") {
60
+ clearRootSubSources();
61
+ }
58
62
  } else {
59
63
  if (kindToIdToHandler[kind][id]) {
60
64
  delete kindToIdToHandler[kind][id];
@@ -83,6 +87,16 @@ function clearReactions(id) {
83
87
  reactionsRegistry.delete(id);
84
88
  }
85
89
  }
90
+ var rootSubIdBySource = /* @__PURE__ */ new Map();
91
+ function setRootSubSource(subId, sourceKey) {
92
+ rootSubIdBySource.set(sourceKey, subId);
93
+ }
94
+ function getRootSubIdBySource(sourceKey) {
95
+ return rootSubIdBySource.get(sourceKey);
96
+ }
97
+ function clearRootSubSources() {
98
+ rootSubIdBySource.clear();
99
+ }
86
100
  function clearSubs() {
87
101
  clearReactions();
88
102
  clearHandlers("sub");
@@ -152,7 +166,14 @@ function updateAppDbWithPatches(newDb, patches) {
152
166
  const pathSegments = patch.path;
153
167
  if (pathSegments.length > 0) {
154
168
  const rootKey = pathSegments[0];
155
- const subVectorKey = JSON.stringify([rootKey]);
169
+ if (typeof rootKey !== "string") {
170
+ continue;
171
+ }
172
+ const subId = getRootSubIdBySource(rootKey);
173
+ if (!subId) {
174
+ continue;
175
+ }
176
+ const subVectorKey = JSON.stringify([subId]);
156
177
  const reaction = getReaction(subVectorKey);
157
178
  if (reaction) {
158
179
  if (!reaction.isRoot) {
@@ -1011,16 +1032,23 @@ var Reaction = class _Reaction {
1011
1032
  // src/subs.ts
1012
1033
  var KIND4 = "sub";
1013
1034
  var KIND_DEPS = "subDeps";
1035
+ function registerRootSub(id, sourceKey) {
1036
+ const conflictingSubId = getRootSubIdBySource(sourceKey);
1037
+ if (conflictingSubId && conflictingSubId !== id) {
1038
+ consoleLog("error", `[reflex] Subscription with id '${id}' will be overridden. Root key '${sourceKey}' is already used by subscription '${conflictingSubId}'.`);
1039
+ }
1040
+ setRootSubSource(id, sourceKey);
1041
+ registerHandler(KIND4, id, () => getAppDb()[sourceKey]);
1042
+ registerHandler(KIND_DEPS, id, () => []);
1043
+ }
1014
1044
  function regSub(id, computeFn, depsFn, config) {
1015
1045
  if (hasHandler(KIND4, id)) {
1016
1046
  consoleLog("warn", `[reflex] Overriding. Subscription '${id}' already registered.`);
1017
1047
  }
1018
1048
  if (!computeFn) {
1019
- registerHandler(KIND4, id, () => getAppDb()[id]);
1020
- registerHandler(KIND_DEPS, id, () => []);
1049
+ registerRootSub(id, id);
1021
1050
  } else if (typeof computeFn === "string") {
1022
- registerHandler(KIND4, id, () => getAppDb()[computeFn]);
1023
- registerHandler(KIND_DEPS, id, () => []);
1051
+ registerRootSub(id, computeFn);
1024
1052
  } else {
1025
1053
  if (!depsFn) {
1026
1054
  consoleLog("error", `[reflex] Subscription '${id}' has computeFn but missing depsFn. Computed subscriptions must specify their dependencies.`);
package/llms.txt ADDED
@@ -0,0 +1,186 @@
1
+ ## 0) Reflex Quick Info
2
+
3
+ - Reflex is a React state-management library inspired by ClojureScript re-frame (event-driven updates, subscriptions for derived data, effects/coeffects for side effects).
4
+ - Install:
5
+ - Runtime: `npm i @flexsurfer/reflex`
6
+ - Devtools (dev only): `npm i -D @flexsurfer/reflex-devtools`
7
+ - Docs:
8
+ - Main docs: [reflex.js.org/docs](https://reflex.js.org/docs)
9
+ - Best practices: [reflex.js.org/docs/best-practices.html](https://reflex.js.org/docs/best-practices.html)
10
+ - Packages: [@flexsurfer/reflex](https://www.npmjs.com/package/@flexsurfer/reflex), [@flexsurfer/reflex-devtools](https://www.npmjs.com/package/@flexsurfer/reflex-devtools)
11
+
12
+ ## 1) State Architecture
13
+
14
+ Use this baseline structure:
15
+
16
+ ```text
17
+ src/state/
18
+ db.ts
19
+ event-ids.ts
20
+ events.ts
21
+ effect-ids.ts
22
+ effects.ts
23
+ sub-ids.ts
24
+ subs.ts
25
+ ```
26
+
27
+ Rules:
28
+ - Keep IDs centralized in `event-ids.ts`, `effect-ids.ts`, `sub-ids.ts`.
29
+ - Keep init in `db.ts` (`initAppDb(...)`).
30
+ - Register events/effects/subscriptions via side-effect imports in app bootstrap (`main.tsx` style).
31
+ - If `events.ts` or `subs.ts` grows too large, split by feature, keep shared/global state separate.
32
+
33
+ ## 2) State Shape
34
+
35
+ - Grow horizontally (new top-level feature keys), avoid deep nesting.
36
+ - Normalize entity-like data (`byId` maps + id arrays) for fast lookup and simpler updates.
37
+ - Keep UI state explicit and separate from domain entities.
38
+ - If using `Map`/`Set` in DB, call `enableMapSet()` before `initAppDb`.
39
+
40
+ ## 3) Events `regEvent`
41
+
42
+ - Events must be synchronous and focused on state transitions.
43
+ - Events should read all required data from `draftDb` (or via coeffects) — never rely on callers passing subscription-derived state; dispatch calls from views should only carry user intent (IDs, input values, flags).
44
+ - Validate inputs and guard clauses first; return early on invalid state.
45
+ - Mutate only required fields on `draftDb`.
46
+ - Avoid unnecessary object/array recreation (`{...obj}`, `[...arr]`) when no actual change is needed.
47
+ - Never perform async/API/localStorage work directly in events.
48
+ - Return effect tuples for side effects.
49
+ - When sending mutated draft data to effects, always use `current(...)`.
50
+ - Prefer deterministic coeffects (time/id/random/env) instead of direct globals for testability.
51
+
52
+ Event naming:
53
+ - Keep exported constant keys in `UPPER_SNAKE_CASE`.
54
+ - Use namespaced string values: `feature/action` (example: `bases/create`).
55
+ - Keep key and value aligned:
56
+ - `BASES_CREATE` -> `bases/create`
57
+ - `PRODUCTION_PLAN_ADD_BUILDINGS_TO_BASE` -> `production_plan/add_buildings_to_base`
58
+
59
+ ## 4) Effects and Coeffects
60
+
61
+ - Put all I/O here: localStorage, HTTP, timers, analytics, navigation.
62
+ - Effects should be small, defensive, and fail-soft (log, do not crash app state flow).
63
+
64
+ ## 5) Subscriptions `regSub`
65
+
66
+ - Define root subscriptions first `regSub(id, "pathKey")`.
67
+ - Build derived subscriptions from other subscriptions only.
68
+ - Use parameterized subscriptions for by-id and section-specific queries.
69
+ - Keep subscriptions deterministic and lightweight.
70
+ - Subscriptions must return data shaped and ready for direct view consumption — all filtering, sorting, formatting, and joining should happen in the subscription layer, not in React components.
71
+ - Move heavy computations to events (precompute once, read many); avoid repeated expensive derivations in hot subscriptions.
72
+
73
+ ## 6) React Component Contract
74
+
75
+ - Components should only:
76
+ - subscribe to minimal required data
77
+ - dispatch events on user intent (pass only user-provided values — e.g. input text, selected id — never forward subscription data back through dispatch; the event handler should read everything it needs from the DB itself)
78
+ - render UI
79
+ - Never transform, filter, sort, or reshape subscription data inside a component — if the view needs a different shape, create a dedicated subscription that returns it ready to render.
80
+ - Use direct React hooks only for local/ephemeral component concerns:
81
+ - temporary form/input draft state before dispatch
82
+ - UI-only toggles scoped to one component (hover/open/focus)
83
+ - refs, DOM measurement, animation lifecycle
84
+ - Do not mirror Reflex global state in `useState`/`useReducer`.
85
+ - If state is shared, persisted, or business-relevant, keep it in Reflex DB via events/subscriptions.
86
+ - Keep `useEffect` thin in components; business side effects belong in Reflex effects/coeffects.
87
+ - Do not place business rules/validation pipelines in component handlers.
88
+ - Avoid over-subscription (row/item components should not subscribe to full collections).
89
+
90
+ ## 7) Test Minimum
91
+
92
+ For every new feature:
93
+ - Event tests: mutation correctness + emitted effect tuples.
94
+ - Subscription tests: derived outputs from fixed state fixtures.
95
+
96
+ ## 8) AI Generation Checklist
97
+
98
+ Before finalizing generated code:
99
+ - IDs added/updated in all relevant `*-ids.ts` files.
100
+ - Event namespaced and descriptive.
101
+ - Side effects isolated to effects/coeffects.
102
+ - `current(...)` used when passing draft-derived data to effects.
103
+ - No unnecessary object/array recreation in events.
104
+ - Expensive work not placed in frequently re-run subscriptions.
105
+ - Subscriptions return view-ready data; components do not reshape subscription output.
106
+ - Dispatch calls from components pass only user intent, not subscription data; events read from DB.
107
+ - Tests added for new event/subscription behavior.
108
+
109
+ ## 9) Starter Skeleton (copy pattern)
110
+
111
+ ```ts
112
+ import {
113
+ initAppDb,
114
+ regSub,
115
+ regEvent,
116
+ regEffect,
117
+ regCoeffect,
118
+ current,
119
+ dispatch,
120
+ useSubscription,
121
+ } from '@flexsurfer/reflex';
122
+
123
+ // event-ids.ts
124
+ export const EVENT_IDS = {
125
+ APP_INIT: 'app/init',
126
+ TODOS_ADD: 'todos/add',
127
+ } as const;
128
+
129
+ // effect-ids.ts
130
+ export const EFFECT_IDS = {
131
+ GET_TODOS: 'storage/get_todos',
132
+ SET_TODOS: 'storage/set_todos',
133
+ } as const;
134
+
135
+ // sub-ids.ts
136
+ export const SUB_IDS = {
137
+ TODOS_LIST: 'todos/list', // root sub
138
+ TODOS_OPEN_COUNT: 'todos/open_count', // computed sub
139
+ } as const;
140
+
141
+ // db.ts
142
+ type Todo = { id: string; text: string; done: boolean };
143
+ initAppDb({ todos: [] as Todo[] });
144
+
145
+ // effects.ts
146
+ regEffect(EFFECT_IDS.SET_TODOS, (todos: Todo[]) => {
147
+ localStorage.setItem('todos', JSON.stringify(todos));
148
+ });
149
+
150
+ regCoeffect(EFFECT_IDS.GET_TODOS, (coeffects) => {
151
+ const raw = localStorage.getItem('todos');
152
+ coeffects.localStoreTodos = raw ? (JSON.parse(raw) as Todo[]) : [];
153
+ return coeffects;
154
+ });
155
+
156
+ // events.ts
157
+ regEvent(
158
+ EVENT_IDS.APP_INIT,
159
+ ({ draftDb, localStoreTodos }) => {
160
+ draftDb.todos = Array.isArray(localStoreTodos) ? localStoreTodos : [];
161
+ },
162
+ [[EFFECT_IDS.GET_TODOS]]
163
+ );
164
+
165
+ regEvent(EVENT_IDS.TODOS_ADD, ({ draftDb }, text: string) => {
166
+ const clean = text.trim();
167
+ if (!clean) return;
168
+ draftDb.todos.push({ id: `todo_${Date.now()}`, text: clean, done: false });
169
+ return [[EFFECT_IDS.SET_TODOS, current(draftDb.todos)]];
170
+ });
171
+
172
+ // subs.ts
173
+ regSub(SUB_IDS.TODOS_LIST, 'todos'); // root sub
174
+
175
+ regSub(
176
+ SUB_IDS.TODOS_OPEN_COUNT, // computed sub from root sub
177
+ (todos: Todo[]) => todos.filter((t) => !t.done).length,
178
+ () => [[SUB_IDS.TODOS_LIST]]
179
+ );
180
+
181
+ // Usage example (React):
182
+ // const todos = useSubscription([SUB_IDS.TODOS_LIST]);
183
+ // const openCount = useSubscription([SUB_IDS.TODOS_OPEN_COUNT]);
184
+ // dispatch([EVENT_IDS.APP_INIT]);
185
+ // dispatch([EVENT_IDS.TODOS_ADD, 'Buy milk']);
186
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flexsurfer/reflex",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -23,7 +23,8 @@
23
23
  "files": [
24
24
  "dist",
25
25
  "README.md",
26
- "LICENSE"
26
+ "LICENSE",
27
+ "llms.txt"
27
28
  ],
28
29
  "scripts": {
29
30
  "build": "tsup",