@flexsurfer/reflex 0.1.12 โ†’ 0.1.14

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,10 +1,10 @@
1
1
  <div align="center">
2
- <img src="reflex-logo-300kb.png" alt="Reflex Logo" width="200" />
2
+ <img src="reflex_logo.jpg" alt="Reflex Logo" width="200" />
3
3
  </div>
4
4
 
5
5
  **re-frame for the JavaScript world**
6
6
 
7
- A reactive, functional state management library that brings the elegance and power of ClojureScript's re-frame to JavaScript and React/ReactNative applications.
7
+ A reactive, functional state management library that brings the elegance and power of ClojureScript's re-frame to JavaScript/TypeScript and React/ReactNative applications.
8
8
 
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10
10
  [![NPM Version](https://img.shields.io/npm/v/%40flexsurfer%2Freflex)](https://www.npmjs.com/package/@flexsurfer/reflex)
@@ -20,265 +20,23 @@ After many years of building applications with re-frame in the ClojureScript wor
20
20
  ๐Ÿงฉ **Composable Architecture** - Build complex apps from simple, reusable pieces
21
21
  ๐Ÿ”„ **Reactive Subscriptions** - UI automatically updates when state changes
22
22
  ๐ŸŒ **Multi-Platform Support** - With effects separation, it's super easy to support multiple platforms with the same codebase, including web, mobile, and desktop
23
- ๐Ÿค– **AI Friendly** - Reviewing AI-generated changes is easier because all logic is expressed through pure, isolated functions, making each change understandable, verifiable, and deterministic.
24
- ๐Ÿ› ๏ธ **Integrated DevTools** - Reflex-devtools provides deep visibility into your appโ€™s state, events, and subscriptions in real time, forming a powerful combo with Reflex for effective development and debugging.
23
+ ๐Ÿค– **AI Friendly** - Reviewing AI-generated changes is easier because all logic is expressed through pure, isolated functions, making each change understandable, verifiable, and deterministic.
24
+ ๐Ÿ› ๏ธ **Integrated DevTools** - [`@flexsurfer/reflex-devtools`](https://github.com/flexsurfer/reflex-devtools) provides deep visibility into your appโ€™s state, events, and subscriptions in real time, forming a powerful combo with Reflex for effective development and debugging.
25
25
  โšก **Interceptor Pattern** - Powerful middleware system for cross-cutting concerns
26
26
  ๐Ÿ›ก๏ธ **Type Safety** - Full TypeScript support with excellent IDE experience
27
27
  ๐Ÿงช **Testability** - Pure functions make testing straightforward and reliable
28
28
 
29
- ## ๐Ÿš€ Quick Start
30
-
31
- ```bash
32
- npm install @flexsurfer/reflex
33
- npm install --save-dev @flexsurfer/reflex-devtools
34
-
35
- npx reflex-devtools
36
- ```
37
-
38
- ### Basic Example
39
-
40
- ```typescript
41
- import {
42
- initAppDb,
43
- regEvent,
44
- regSub,
45
- dispatch,
46
- useSubscription,
47
- enableTracing
48
- } from '@flexsurfer/reflex';
49
- import { enableDevtools } from '@flexsurfer/reflex-devtools'
50
-
51
- enableTracing()
52
- enableDevtools();
53
-
54
- // Initialize your app database
55
- initAppDb({ counter: 0 });
56
-
57
- // Register events (state transitions)
58
- regEvent('increment', ({ draftDb }) => {
59
- draftDb.counter += 1;
60
- });
61
-
62
- regEvent('decrement', ({ draftDb }) => {
63
- draftDb.counter -= 1;
64
- });
65
-
66
- // Register subscriptions (reactive queries)
67
- regSub('counter');
68
-
69
- // React component
70
- const Counter = () => {
71
- const counter = useSubscription<number>(['counter']);
72
-
73
- return (
74
- <div>
75
- <h1>Count: {counter}</h1>
76
- <button onClick={() => dispatch(['increment'])}>+</button>
77
- <button onClick={() => dispatch(['decrement'])}>-</button>
78
- </div>
79
- );
80
- }
81
- ```
82
-
83
- ## ๐Ÿ—๏ธ Core Concepts
84
-
85
- ### Events & Effects
86
-
87
- Events define state transitions and may declare side effects:
88
-
89
- ```typescript
90
- // Simple state update
91
- regEvent('set-name', ({ draftDb }, name) => {
92
- draftDb.user.name = name;
93
- });
94
-
95
- // Dispatch with parameters
96
- dispatch(['set-name', 'John Doe']);
97
-
98
- // Event with side effects
99
- regEvent('save-user', ({ draftDb }, user) => {
100
- draftDb.saving = true;
101
- return [
102
- ['http', {
103
- method: 'POST',
104
- url: '/api/users',
105
- body: user,
106
- onSuccess: ['save-user-success'],
107
- onFailure: ['save-user-error']
108
- }]
109
- ]
110
- });
111
-
112
- // Dispatch with parameters
113
- dispatch(['save-user', { id: 1, name: 'John', email: 'john@example.com' }]);
114
- ```
115
-
116
- ### Subscriptions
117
-
118
- Create reactive queries that automatically update your UI:
119
-
120
- ```typescript
121
- regSub('user');
122
- regSub('display-prefix');
123
- regSub('user-name', (user) => user.name, () => [['user']]);
124
-
125
- // Computed subscription with dependencies
126
- regSub('user-display-name',
127
- (name, prefix) => `${prefix}: ${name}`,
128
- () => [['user-name'], ['display-prefix']]
129
- );
130
-
131
- // Parameterized subscription
132
- regSub(
133
- 'todo-by-id',
134
- (todos, id) => todos.find(todo => todo.id === id),
135
- () => [['todos']]
136
- );
137
-
138
- regSub(
139
- 'todo-text-by-id',
140
- (todo, _id) => todo.text,
141
- (id) => [['todo-by-id' id]]
142
- );
143
-
144
- // Use in React components
145
- function UserProfile() {
146
- const name = useSubscription<string>(['user-display-name']);
147
- const todo = useSubscription(['todo-by-id', 123])
148
- const todoText = useSubscription(['todo-text-by-id', 123]);
149
-
150
- return <div>{name}</div>;
151
- }
152
- ```
153
-
154
- ### Effects & Co-effects
155
-
156
- Handle side effects in a controlled, testable way:
157
-
158
- ```typescript
159
- import {
160
- regEffect,
161
- regCoeffect
162
- } from '@flexsurfer/reflex';
163
-
164
- // Register custom effects
165
- regEffect('local-storage', (payload) => {
166
- localStorage.setItem(payload.key, JSON.stringify(payload.value));
167
- });
168
-
169
- // Use in events
170
- regEvent('save-to-storage', (_coeffects, data) => {
171
- return [['local-storage', { key: 'app-data', value: data }]]
172
- });
173
-
174
- // Dispatch with data parameter
175
- dispatch(['save-to-storage', { user: 'John', preferences: { theme: 'dark' } }]);
176
-
177
- // Register co-effects
178
- regCoeffect('timestamp', (coeffects) => {
179
- coeffects.timestamp = Date.now();
180
- return coeffects;
181
- });
182
-
183
- regCoeffect('random', (coeffects) => {
184
- coeffects.random = Math.random();
185
- return coeffects;
186
- });
187
-
188
- // Use co-effect in events
189
- regEvent('log-action',
190
- ({ draftDb, timestamp, random }, action) => {
191
- draftDb.actionLog.push({
192
- action,
193
- timestamp: timestamp,
194
- id: random.toString(36)
195
- });
196
- },
197
- [['timestamp'], ['random']]
198
- );
199
-
200
- // Dispatch with action parameter
201
- dispatch(['log-action', 'some-action']);
202
- ```
203
-
204
- ### Interceptors
205
-
206
- Compose functionality with interceptors:
207
-
208
- ```typescript
209
- const loggingInterceptor = {
210
- id: 'logging',
211
- before: (context) => {
212
- console.log('Event:', context.coeffects.event);
213
- return context;
214
- },
215
- after: (context) => {
216
- console.log('Updated DB:', context.coeffects.newDb);
217
- return context;
218
- }
219
- };
220
-
221
- regEvent('my-event', handler, [loggingInterceptor]);
222
- ```
223
-
224
- ## ๐ŸŽฏ Why Re-frame Pattern?
225
-
226
- The re-frame pattern has proven itself in production applications over many years:
227
-
228
- - **Separation of Concerns**: Clear boundaries between events, effects, and subscriptions
229
- - **Time Travel Debugging**: Every state change is an event that can be replayed
230
- - **Testability**: Pure functions make unit testing straightforward
231
- - **Composability**: Build complex features from simple, reusable parts
232
- - **Maintainability**: Code becomes self-documenting and easy to reason about
233
-
234
- ## ๐Ÿ”„ Migration from Other Libraries
235
-
236
- ### From Redux
237
-
238
- ```typescript
239
- // Redux style
240
- const counterSlice = createSlice({
241
- name: 'count',
242
- initialState: { value: 0 },
243
- reducers: {
244
- increment: (state) => { state.value += 1; }
245
- }
246
- });
247
-
248
- // Reflex style
249
- initAppDb({ count: 0 });
250
- regEvent('increment', ({ draftDb }) => {
251
- draftDb.count += 1;
252
- });
253
- regSub('count');
254
- ```
255
-
256
- ### From Zustand
257
-
258
- ```typescript
259
- // Zustand style
260
- const useStore = create((set) => ({
261
- count: 0,
262
- increment: () => set((state) => ({ count: state.count + 1 }))
263
- }));
264
-
265
- // Reflex style
266
- initAppDb({ count: 0 });
267
- regEvent('increment', ({ draftDb }) => {
268
- draftDb.count += 1;
269
- });
270
- regSub('count');
271
- ```
272
-
273
29
  ## ๐Ÿ“š Learn More
274
30
 
31
+ - [Documentation](https://reflex.js.org/docs/)
32
+ - [Step-by-Step Tutorial](https://reflex.js.org/docs/quick-start.html)
33
+ - [Best Practices](https://reflex.js.org/docs/api-reference.html)
34
+ - [API Reference](https://reflex.js.org/docs/best-practices.html)
275
35
  - [re-frame Documentation](https://day8.github.io/re-frame/re-frame/) - The original and comprehensive guide to understanding the philosophy and patterns
276
- - Step-by-Step Tutorial - TBD
277
- - API Reference - TBD
36
+
278
37
  - Examples
279
38
  - [TodoMVC](https://github.com/flexsurfer/reflex/tree/main/examples/todomvc) - Classic todo app implementation showcasing core reflex patterns
280
39
  - [Einbรผrgerungstest](https://github.com/flexsurfer/einburgerungstest/) - German citizenship test app built with reflex ([Live Demo](https://www.ebtest.org/))
281
- - Best Practices - TBD
282
40
 
283
41
  ## ๐Ÿค Contributing
284
42
 
package/dist/index.cjs CHANGED
@@ -30,12 +30,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
+ DISPATCH: () => DISPATCH,
34
+ DISPATCH_LATER: () => DISPATCH_LATER,
33
35
  HotReloadWrapper: () => HotReloadWrapper,
36
+ NOW: () => NOW,
37
+ RANDOM: () => RANDOM,
34
38
  clearGlobalInterceptors: () => clearGlobalInterceptors,
35
39
  clearHandlers: () => clearHandlers,
36
40
  clearHotReloadCallbacks: () => clearHotReloadCallbacks,
37
41
  clearReactions: () => clearReactions,
38
42
  clearSubs: () => clearSubs,
43
+ current: () => current,
39
44
  debounceAndDispatch: () => debounceAndDispatch,
40
45
  defaultErrorHandler: () => defaultErrorHandler,
41
46
  disableTracing: () => disableTracing,
@@ -47,7 +52,7 @@ __export(src_exports, {
47
52
  getHandler: () => getHandler,
48
53
  getSubscriptionValue: () => getSubscriptionValue,
49
54
  initAppDb: () => initAppDb,
50
- isDebugEnabled: () => isDebugEnabled,
55
+ original: () => original,
51
56
  regCoeffect: () => regCoeffect,
52
57
  regEffect: () => regEffect,
53
58
  regEvent: () => regEvent,
@@ -56,7 +61,6 @@ __export(src_exports, {
56
61
  regSub: () => regSub,
57
62
  registerHotReloadCallback: () => registerHotReloadCallback,
58
63
  registerTraceCb: () => registerTraceCb,
59
- setDebugEnabled: () => setDebugEnabled,
60
64
  setupSubsHotReload: () => setupSubsHotReload,
61
65
  throttleAndDispatch: () => throttleAndDispatch,
62
66
  triggerHotReload: () => triggerHotReload,
@@ -226,6 +230,15 @@ function updateAppDbWithPatches(newDb, patches) {
226
230
  }
227
231
  }
228
232
 
233
+ // src/immer-utils.ts
234
+ var import_immer = require("immer");
235
+ function original(value) {
236
+ return (0, import_immer.isDraft)(value) ? (0, import_immer.original)(value) : value;
237
+ }
238
+ function current(value) {
239
+ return (0, import_immer.isDraft)(value) ? (0, import_immer.current)(value) : value;
240
+ }
241
+
229
242
  // src/interceptor.ts
230
243
  function isInterceptor(m) {
231
244
  if (typeof m !== "object" || m === null)
@@ -314,8 +327,8 @@ function execute(eventV, interceptors) {
314
327
  try {
315
328
  return executeInterceptors(ctx);
316
329
  } catch (e) {
317
- const reFrameError = mergeExData(e, { eventV });
318
- errorHandler(e.cause || e, reFrameError);
330
+ const reflexError = mergeExData(e, { eventV });
331
+ errorHandler(e.cause || e, reflexError);
319
332
  return ctx;
320
333
  }
321
334
  }
@@ -343,11 +356,13 @@ function getInjectCofxInterceptor(id, value) {
343
356
  }
344
357
  };
345
358
  }
346
- regCoeffect("now", (coeffects) => ({
359
+ var NOW = "now";
360
+ var RANDOM = "random";
361
+ regCoeffect(NOW, (coeffects) => ({
347
362
  ...coeffects,
348
363
  now: Date.now()
349
364
  }));
350
- regCoeffect("random", (coeffects) => ({
365
+ regCoeffect(RANDOM, (coeffects) => ({
351
366
  ...coeffects,
352
367
  random: Math.random()
353
368
  }));
@@ -532,6 +547,8 @@ var doFxInterceptor = {
532
547
  return context;
533
548
  }
534
549
  };
550
+ var DISPATCH_LATER = "dispatch-later";
551
+ var DISPATCH = "dispatch";
535
552
  function dispatchLater(effect) {
536
553
  const { ms, dispatch: eventToDispatch } = effect;
537
554
  if (!Array.isArray(eventToDispatch) || typeof ms !== "number") {
@@ -543,10 +560,10 @@ function dispatchLater(effect) {
543
560
  }
544
561
  setTimeout(() => dispatch(eventToDispatch), Math.max(0, ms));
545
562
  }
546
- regEffect("dispatch-later", (value) => {
563
+ regEffect(DISPATCH_LATER, (value) => {
547
564
  dispatchLater(value);
548
565
  });
549
- regEffect("dispatch", (value) => {
566
+ regEffect(DISPATCH, (value) => {
550
567
  if (!Array.isArray(value)) {
551
568
  consoleLog("error", "[reflex] ignoring bad dispatch value. Expected a vector, but got:", value);
552
569
  return;
@@ -555,20 +572,15 @@ regEffect("dispatch", (value) => {
555
572
  });
556
573
 
557
574
  // src/events.ts
558
- var import_immer = require("immer");
575
+ var import_immer2 = require("immer");
559
576
 
560
577
  // src/settings.ts
561
578
  var store = {
562
- globalInterceptors: [],
563
- debugEnabled: true
564
- // Default to true, can be configured via setDebugEnabled
579
+ globalInterceptors: []
565
580
  };
566
581
  function replaceGlobalInterceptor(globalInterceptors, interceptor) {
567
582
  return globalInterceptors.reduce((ret, existingInterceptor) => {
568
583
  if (interceptor.id === existingInterceptor.id) {
569
- if (store.debugEnabled) {
570
- consoleLog("warn", "[reflex] replacing duplicate global interceptor id:", interceptor.id);
571
- }
572
584
  return [...ret, interceptor];
573
585
  } else {
574
586
  return [...ret, existingInterceptor];
@@ -594,12 +606,6 @@ function clearGlobalInterceptors(id) {
594
606
  store.globalInterceptors = store.globalInterceptors.filter((interceptor) => interceptor.id !== id);
595
607
  }
596
608
  }
597
- function setDebugEnabled(enabled) {
598
- store.debugEnabled = enabled;
599
- }
600
- function isDebugEnabled() {
601
- return store.debugEnabled;
602
- }
603
609
 
604
610
  // src/trace.ts
605
611
  var nextId = 1;
@@ -700,6 +706,13 @@ function enableTracePrint() {
700
706
  });
701
707
  }
702
708
 
709
+ // src/env.ts
710
+ var IS_DEV = (
711
+ // Node.js check
712
+ typeof process !== "undefined" && process.env?.NODE_ENV === "development" || // React Native / bundler check
713
+ typeof __DEV__ !== "undefined" && __DEV__
714
+ );
715
+
703
716
  // src/events.ts
704
717
  var KIND3 = "event";
705
718
  function regEvent(id, handler, cofxOrInterceptors, interceptors) {
@@ -748,22 +761,28 @@ function registerInterceptors(id, cofxOrInterceptors, interceptors) {
748
761
  setInterceptors(id, allInterceptors);
749
762
  }
750
763
  }
751
- (0, import_immer.enablePatches)();
764
+ (0, import_immer2.enablePatches)();
752
765
  function eventHandlerInterceptor(handler) {
753
766
  return {
754
767
  id: "fx-handler",
755
768
  before(context) {
756
- const coeffects = context.coeffects;
757
- const event = coeffects.event;
769
+ const event = context.coeffects.event;
758
770
  const params = event.slice(1);
759
771
  let effects = [];
760
- const [newDb, patches] = (0, import_immer.produceWithPatches)(
772
+ const [newDb, patches] = (0, import_immer2.produceWithPatches)(
761
773
  getAppDb(),
762
774
  (draftDb) => {
763
- coeffects.draftDb = draftDb;
764
- effects = handler({ ...coeffects }, ...params) || [];
775
+ const coeffectsWithDb = { ...context.coeffects, draftDb };
776
+ effects = handler(coeffectsWithDb, ...params) || [];
765
777
  }
766
778
  );
779
+ if (IS_DEV) {
780
+ try {
781
+ JSON.stringify(effects);
782
+ } catch (e) {
783
+ consoleLog("warn", `[reflex] Effects ${effects} contain Proxy (probably an Immer draft). Use current() for draftDb values.`);
784
+ }
785
+ }
767
786
  context.newDb = newDb;
768
787
  context.patches = patches;
769
788
  mergeTrace({ tags: { "patches": patches, "effects": effects } });
@@ -808,11 +827,11 @@ function handle(eventV) {
808
827
  function regEventErrorHandler(handler) {
809
828
  registerHandler("error", "event-handler", handler);
810
829
  }
811
- function defaultErrorHandler(originalError, reFrameError) {
830
+ function defaultErrorHandler(originalError, reflexError) {
812
831
  consoleLog("error", "[reflex] Interceptor Exception:", {
813
832
  originalError,
814
- reFrameError,
815
- data: reFrameError.data
833
+ reflexError,
834
+ data: reflexError.data
816
835
  });
817
836
  throw originalError;
818
837
  }
@@ -1187,21 +1206,22 @@ function setupSubsHotReload() {
1187
1206
  return { dispose, accept };
1188
1207
  }
1189
1208
  function HotReloadWrapper({ children }) {
1190
- if (isDebugEnabled()) {
1191
- const key = useHotReloadKey();
1192
- return import_react2.default.createElement(import_react2.default.Fragment, { key }, children);
1193
- } else {
1194
- return children;
1195
- }
1209
+ const key = useHotReloadKey();
1210
+ return import_react2.default.createElement(import_react2.default.Fragment, { key }, children);
1196
1211
  }
1197
1212
  // Annotate the CommonJS export names for ESM import in node:
1198
1213
  0 && (module.exports = {
1214
+ DISPATCH,
1215
+ DISPATCH_LATER,
1199
1216
  HotReloadWrapper,
1217
+ NOW,
1218
+ RANDOM,
1200
1219
  clearGlobalInterceptors,
1201
1220
  clearHandlers,
1202
1221
  clearHotReloadCallbacks,
1203
1222
  clearReactions,
1204
1223
  clearSubs,
1224
+ current,
1205
1225
  debounceAndDispatch,
1206
1226
  defaultErrorHandler,
1207
1227
  disableTracing,
@@ -1213,7 +1233,7 @@ function HotReloadWrapper({ children }) {
1213
1233
  getHandler,
1214
1234
  getSubscriptionValue,
1215
1235
  initAppDb,
1216
- isDebugEnabled,
1236
+ original,
1217
1237
  regCoeffect,
1218
1238
  regEffect,
1219
1239
  regEvent,
@@ -1222,7 +1242,6 @@ function HotReloadWrapper({ children }) {
1222
1242
  regSub,
1223
1243
  registerHotReloadCallback,
1224
1244
  registerTraceCb,
1225
- setDebugEnabled,
1226
1245
  setupSubsHotReload,
1227
1246
  throttleAndDispatch,
1228
1247
  triggerHotReload,
package/dist/index.d.cts CHANGED
@@ -7,7 +7,7 @@ type EventVector = [Id, ...any[]];
7
7
  type EventHandler<T = Record<string, any>> = (coeffects: CoEffects<T>, ...params: any[]) => Effects | void;
8
8
  type EffectHandler = (value: any) => void;
9
9
  type CoEffectHandler<T = Record<string, any>> = (coeffects: CoEffects<T>, value?: any) => CoEffects<T>;
10
- type ErrorHandler = (originalError: Error, reFrameError: Error & {
10
+ type ErrorHandler = (originalError: Error, reflexError: Error & {
11
11
  data: any;
12
12
  }) => void;
13
13
  type SubVector = [Id, ...any[]];
@@ -42,6 +42,23 @@ interface Interceptor<T = Record<string, any>> {
42
42
  declare function initAppDb<T = Record<string, any>>(value: Db<T>): void;
43
43
  declare function getAppDb<T = Record<string, any>>(): Db<T>;
44
44
 
45
+ /**
46
+ * Safe versions of immer's original and current functions
47
+ * These check if the value is actually a draft before calling the immer functions
48
+ */
49
+ /**
50
+ * Safe version of immer's original function
51
+ * Returns the original (frozen) version of a draft if the value is a draft,
52
+ * otherwise returns the value as-is
53
+ */
54
+ declare function original<T>(value: T): T;
55
+ /**
56
+ * Safe version of immer's current function
57
+ * Returns the current draft state as a plain object if the value is a draft,
58
+ * otherwise returns the value as-is
59
+ */
60
+ declare function current<T>(value: T): T;
61
+
45
62
  /** Register an event handler with only a handler function (db event) */
46
63
  declare function regEvent<T = Record<string, any>>(id: Id, handler: EventHandler<T>): void;
47
64
  /** Register an event handler with interceptors and handler function (backward compatibility) */
@@ -57,16 +74,16 @@ declare function regEvent<T = Record<string, any>>(id: Id, handler: EventHandler
57
74
  * Only one handler can be registered. Registering a new handler clears the existing handler.
58
75
  *
59
76
  * This handler function has the signature:
60
- * `(originalError: Error, reFrameError: Error & { data: any }) => void`
77
+ * `(originalError: Error, reflexError: Error & { data: any }) => void`
61
78
  *
62
79
  * - `originalError`: A platform-native Error object.
63
80
  * Represents the original error thrown by user code.
64
81
  * This is the error you see when no handler is registered.
65
82
  *
66
- * - `reFrameError`: An Error object with additional data.
83
+ * - `reflexError`: An Error object with additional data.
67
84
  * Includes the stacktrace of reflex's internal functions,
68
85
  * and extra data about the interceptor process.
69
- * Access `reFrameError.data` to get this info.
86
+ * Access `reflexError.data` to get this info.
70
87
  *
71
88
  * The data includes:
72
89
  * - `interceptor`: the `id` of the throwing interceptor.
@@ -77,7 +94,7 @@ declare function regEventErrorHandler(handler: ErrorHandler): void;
77
94
  /**
78
95
  * Default error handler that logs errors to console
79
96
  */
80
- declare function defaultErrorHandler(originalError: Error, reFrameError: Error & {
97
+ declare function defaultErrorHandler(originalError: Error, reflexError: Error & {
81
98
  data: any;
82
99
  }): void;
83
100
 
@@ -85,8 +102,12 @@ declare function regSub<R>(id: Id, computeFn?: (...values: any[]) => R, depsFn?:
85
102
  declare function getSubscriptionValue<T>(subVector: SubVector): T;
86
103
 
87
104
  declare function regEffect(id: string, handler: EffectHandler): void;
105
+ declare const DISPATCH_LATER = "dispatch-later";
106
+ declare const DISPATCH = "dispatch";
88
107
 
89
108
  declare function regCoeffect(id: string, handler: CoEffectHandler): void;
109
+ declare const NOW = "now";
110
+ declare const RANDOM = "random";
90
111
 
91
112
  /**
92
113
  * Register a global interceptor
@@ -101,14 +122,6 @@ declare function getGlobalInterceptors(): Interceptor[];
101
122
  */
102
123
  declare function clearGlobalInterceptors(): void;
103
124
  declare function clearGlobalInterceptors(id: string): void;
104
- /**
105
- * Enable or disable debug mode
106
- */
107
- declare function setDebugEnabled(enabled: boolean): void;
108
- /**
109
- * Check if debug mode is enabled
110
- */
111
- declare function isDebugEnabled(): boolean;
112
125
 
113
126
  type Kind = 'event' | 'fx' | 'cofx' | 'sub' | 'subDeps' | 'error';
114
127
  type RegistryHandler = EventHandler | EffectHandler | CoEffectHandler | ErrorHandler | SubHandler | SubDepsHandler;
@@ -179,9 +192,9 @@ declare function setupSubsHotReload(): {
179
192
  */
180
193
  declare function HotReloadWrapper({ children }: {
181
194
  children: React.ReactNode;
182
- }): string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.FunctionComponentElement<{
195
+ }): React.FunctionComponentElement<{
183
196
  children?: React.ReactNode | undefined;
184
- }> | null | undefined;
197
+ }>;
185
198
 
186
199
  type TraceID = number;
187
200
  interface TraceOpts {
@@ -202,4 +215,4 @@ declare function disableTracing(): void;
202
215
  declare function registerTraceCb(key: string, cb: TraceCallback): void;
203
216
  declare function enableTracePrint(): void;
204
217
 
205
- export { CoEffectHandler, CoEffects, Context, Db, DispatchLaterEffect, EffectHandler, Effects, ErrorHandler, EventHandler, EventVector, HotReloadWrapper, Id, Interceptor, SubVector, clearGlobalInterceptors, clearHandlers, clearHotReloadCallbacks, clearReactions, clearSubs, debounceAndDispatch, defaultErrorHandler, disableTracing, dispatch, enableTracePrint, enableTracing, getAppDb, getGlobalInterceptors, getHandler, getSubscriptionValue, initAppDb, isDebugEnabled, regCoeffect, regEffect, regEvent, regEventErrorHandler, regGlobalInterceptor, regSub, registerHotReloadCallback, registerTraceCb, setDebugEnabled, setupSubsHotReload, throttleAndDispatch, triggerHotReload, useHotReload, useHotReloadKey, useSubscription };
218
+ export { CoEffectHandler, CoEffects, Context, DISPATCH, DISPATCH_LATER, Db, DispatchLaterEffect, EffectHandler, Effects, ErrorHandler, EventHandler, EventVector, HotReloadWrapper, Id, Interceptor, NOW, RANDOM, SubVector, clearGlobalInterceptors, clearHandlers, clearHotReloadCallbacks, clearReactions, clearSubs, current, debounceAndDispatch, defaultErrorHandler, disableTracing, dispatch, enableTracePrint, enableTracing, getAppDb, getGlobalInterceptors, getHandler, getSubscriptionValue, initAppDb, original, regCoeffect, regEffect, regEvent, regEventErrorHandler, regGlobalInterceptor, regSub, registerHotReloadCallback, registerTraceCb, setupSubsHotReload, throttleAndDispatch, triggerHotReload, useHotReload, useHotReloadKey, useSubscription };
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ type EventVector = [Id, ...any[]];
7
7
  type EventHandler<T = Record<string, any>> = (coeffects: CoEffects<T>, ...params: any[]) => Effects | void;
8
8
  type EffectHandler = (value: any) => void;
9
9
  type CoEffectHandler<T = Record<string, any>> = (coeffects: CoEffects<T>, value?: any) => CoEffects<T>;
10
- type ErrorHandler = (originalError: Error, reFrameError: Error & {
10
+ type ErrorHandler = (originalError: Error, reflexError: Error & {
11
11
  data: any;
12
12
  }) => void;
13
13
  type SubVector = [Id, ...any[]];
@@ -42,6 +42,23 @@ interface Interceptor<T = Record<string, any>> {
42
42
  declare function initAppDb<T = Record<string, any>>(value: Db<T>): void;
43
43
  declare function getAppDb<T = Record<string, any>>(): Db<T>;
44
44
 
45
+ /**
46
+ * Safe versions of immer's original and current functions
47
+ * These check if the value is actually a draft before calling the immer functions
48
+ */
49
+ /**
50
+ * Safe version of immer's original function
51
+ * Returns the original (frozen) version of a draft if the value is a draft,
52
+ * otherwise returns the value as-is
53
+ */
54
+ declare function original<T>(value: T): T;
55
+ /**
56
+ * Safe version of immer's current function
57
+ * Returns the current draft state as a plain object if the value is a draft,
58
+ * otherwise returns the value as-is
59
+ */
60
+ declare function current<T>(value: T): T;
61
+
45
62
  /** Register an event handler with only a handler function (db event) */
46
63
  declare function regEvent<T = Record<string, any>>(id: Id, handler: EventHandler<T>): void;
47
64
  /** Register an event handler with interceptors and handler function (backward compatibility) */
@@ -57,16 +74,16 @@ declare function regEvent<T = Record<string, any>>(id: Id, handler: EventHandler
57
74
  * Only one handler can be registered. Registering a new handler clears the existing handler.
58
75
  *
59
76
  * This handler function has the signature:
60
- * `(originalError: Error, reFrameError: Error & { data: any }) => void`
77
+ * `(originalError: Error, reflexError: Error & { data: any }) => void`
61
78
  *
62
79
  * - `originalError`: A platform-native Error object.
63
80
  * Represents the original error thrown by user code.
64
81
  * This is the error you see when no handler is registered.
65
82
  *
66
- * - `reFrameError`: An Error object with additional data.
83
+ * - `reflexError`: An Error object with additional data.
67
84
  * Includes the stacktrace of reflex's internal functions,
68
85
  * and extra data about the interceptor process.
69
- * Access `reFrameError.data` to get this info.
86
+ * Access `reflexError.data` to get this info.
70
87
  *
71
88
  * The data includes:
72
89
  * - `interceptor`: the `id` of the throwing interceptor.
@@ -77,7 +94,7 @@ declare function regEventErrorHandler(handler: ErrorHandler): void;
77
94
  /**
78
95
  * Default error handler that logs errors to console
79
96
  */
80
- declare function defaultErrorHandler(originalError: Error, reFrameError: Error & {
97
+ declare function defaultErrorHandler(originalError: Error, reflexError: Error & {
81
98
  data: any;
82
99
  }): void;
83
100
 
@@ -85,8 +102,12 @@ declare function regSub<R>(id: Id, computeFn?: (...values: any[]) => R, depsFn?:
85
102
  declare function getSubscriptionValue<T>(subVector: SubVector): T;
86
103
 
87
104
  declare function regEffect(id: string, handler: EffectHandler): void;
105
+ declare const DISPATCH_LATER = "dispatch-later";
106
+ declare const DISPATCH = "dispatch";
88
107
 
89
108
  declare function regCoeffect(id: string, handler: CoEffectHandler): void;
109
+ declare const NOW = "now";
110
+ declare const RANDOM = "random";
90
111
 
91
112
  /**
92
113
  * Register a global interceptor
@@ -101,14 +122,6 @@ declare function getGlobalInterceptors(): Interceptor[];
101
122
  */
102
123
  declare function clearGlobalInterceptors(): void;
103
124
  declare function clearGlobalInterceptors(id: string): void;
104
- /**
105
- * Enable or disable debug mode
106
- */
107
- declare function setDebugEnabled(enabled: boolean): void;
108
- /**
109
- * Check if debug mode is enabled
110
- */
111
- declare function isDebugEnabled(): boolean;
112
125
 
113
126
  type Kind = 'event' | 'fx' | 'cofx' | 'sub' | 'subDeps' | 'error';
114
127
  type RegistryHandler = EventHandler | EffectHandler | CoEffectHandler | ErrorHandler | SubHandler | SubDepsHandler;
@@ -179,9 +192,9 @@ declare function setupSubsHotReload(): {
179
192
  */
180
193
  declare function HotReloadWrapper({ children }: {
181
194
  children: React.ReactNode;
182
- }): string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.FunctionComponentElement<{
195
+ }): React.FunctionComponentElement<{
183
196
  children?: React.ReactNode | undefined;
184
- }> | null | undefined;
197
+ }>;
185
198
 
186
199
  type TraceID = number;
187
200
  interface TraceOpts {
@@ -202,4 +215,4 @@ declare function disableTracing(): void;
202
215
  declare function registerTraceCb(key: string, cb: TraceCallback): void;
203
216
  declare function enableTracePrint(): void;
204
217
 
205
- export { CoEffectHandler, CoEffects, Context, Db, DispatchLaterEffect, EffectHandler, Effects, ErrorHandler, EventHandler, EventVector, HotReloadWrapper, Id, Interceptor, SubVector, clearGlobalInterceptors, clearHandlers, clearHotReloadCallbacks, clearReactions, clearSubs, debounceAndDispatch, defaultErrorHandler, disableTracing, dispatch, enableTracePrint, enableTracing, getAppDb, getGlobalInterceptors, getHandler, getSubscriptionValue, initAppDb, isDebugEnabled, regCoeffect, regEffect, regEvent, regEventErrorHandler, regGlobalInterceptor, regSub, registerHotReloadCallback, registerTraceCb, setDebugEnabled, setupSubsHotReload, throttleAndDispatch, triggerHotReload, useHotReload, useHotReloadKey, useSubscription };
218
+ export { CoEffectHandler, CoEffects, Context, DISPATCH, DISPATCH_LATER, Db, DispatchLaterEffect, EffectHandler, Effects, ErrorHandler, EventHandler, EventVector, HotReloadWrapper, Id, Interceptor, NOW, RANDOM, SubVector, clearGlobalInterceptors, clearHandlers, clearHotReloadCallbacks, clearReactions, clearSubs, current, debounceAndDispatch, defaultErrorHandler, disableTracing, dispatch, enableTracePrint, enableTracing, getAppDb, getGlobalInterceptors, getHandler, getSubscriptionValue, initAppDb, original, regCoeffect, regEffect, regEvent, regEventErrorHandler, regGlobalInterceptor, regSub, registerHotReloadCallback, registerTraceCb, setupSubsHotReload, throttleAndDispatch, triggerHotReload, useHotReload, useHotReloadKey, useSubscription };
package/dist/index.mjs CHANGED
@@ -158,6 +158,15 @@ function updateAppDbWithPatches(newDb, patches) {
158
158
  }
159
159
  }
160
160
 
161
+ // src/immer-utils.ts
162
+ import { isDraft, original as immerOriginal, current as immerCurrent } from "immer";
163
+ function original(value) {
164
+ return isDraft(value) ? immerOriginal(value) : value;
165
+ }
166
+ function current(value) {
167
+ return isDraft(value) ? immerCurrent(value) : value;
168
+ }
169
+
161
170
  // src/interceptor.ts
162
171
  function isInterceptor(m) {
163
172
  if (typeof m !== "object" || m === null)
@@ -246,8 +255,8 @@ function execute(eventV, interceptors) {
246
255
  try {
247
256
  return executeInterceptors(ctx);
248
257
  } catch (e) {
249
- const reFrameError = mergeExData(e, { eventV });
250
- errorHandler(e.cause || e, reFrameError);
258
+ const reflexError = mergeExData(e, { eventV });
259
+ errorHandler(e.cause || e, reflexError);
251
260
  return ctx;
252
261
  }
253
262
  }
@@ -275,11 +284,13 @@ function getInjectCofxInterceptor(id, value) {
275
284
  }
276
285
  };
277
286
  }
278
- regCoeffect("now", (coeffects) => ({
287
+ var NOW = "now";
288
+ var RANDOM = "random";
289
+ regCoeffect(NOW, (coeffects) => ({
279
290
  ...coeffects,
280
291
  now: Date.now()
281
292
  }));
282
- regCoeffect("random", (coeffects) => ({
293
+ regCoeffect(RANDOM, (coeffects) => ({
283
294
  ...coeffects,
284
295
  random: Math.random()
285
296
  }));
@@ -464,6 +475,8 @@ var doFxInterceptor = {
464
475
  return context;
465
476
  }
466
477
  };
478
+ var DISPATCH_LATER = "dispatch-later";
479
+ var DISPATCH = "dispatch";
467
480
  function dispatchLater(effect) {
468
481
  const { ms, dispatch: eventToDispatch } = effect;
469
482
  if (!Array.isArray(eventToDispatch) || typeof ms !== "number") {
@@ -475,10 +488,10 @@ function dispatchLater(effect) {
475
488
  }
476
489
  setTimeout(() => dispatch(eventToDispatch), Math.max(0, ms));
477
490
  }
478
- regEffect("dispatch-later", (value) => {
491
+ regEffect(DISPATCH_LATER, (value) => {
479
492
  dispatchLater(value);
480
493
  });
481
- regEffect("dispatch", (value) => {
494
+ regEffect(DISPATCH, (value) => {
482
495
  if (!Array.isArray(value)) {
483
496
  consoleLog("error", "[reflex] ignoring bad dispatch value. Expected a vector, but got:", value);
484
497
  return;
@@ -491,16 +504,11 @@ import { enablePatches, produceWithPatches } from "immer";
491
504
 
492
505
  // src/settings.ts
493
506
  var store = {
494
- globalInterceptors: [],
495
- debugEnabled: true
496
- // Default to true, can be configured via setDebugEnabled
507
+ globalInterceptors: []
497
508
  };
498
509
  function replaceGlobalInterceptor(globalInterceptors, interceptor) {
499
510
  return globalInterceptors.reduce((ret, existingInterceptor) => {
500
511
  if (interceptor.id === existingInterceptor.id) {
501
- if (store.debugEnabled) {
502
- consoleLog("warn", "[reflex] replacing duplicate global interceptor id:", interceptor.id);
503
- }
504
512
  return [...ret, interceptor];
505
513
  } else {
506
514
  return [...ret, existingInterceptor];
@@ -526,12 +534,6 @@ function clearGlobalInterceptors(id) {
526
534
  store.globalInterceptors = store.globalInterceptors.filter((interceptor) => interceptor.id !== id);
527
535
  }
528
536
  }
529
- function setDebugEnabled(enabled) {
530
- store.debugEnabled = enabled;
531
- }
532
- function isDebugEnabled() {
533
- return store.debugEnabled;
534
- }
535
537
 
536
538
  // src/trace.ts
537
539
  var nextId = 1;
@@ -632,6 +634,13 @@ function enableTracePrint() {
632
634
  });
633
635
  }
634
636
 
637
+ // src/env.ts
638
+ var IS_DEV = (
639
+ // Node.js check
640
+ typeof process !== "undefined" && process.env?.NODE_ENV === "development" || // React Native / bundler check
641
+ typeof __DEV__ !== "undefined" && __DEV__
642
+ );
643
+
635
644
  // src/events.ts
636
645
  var KIND3 = "event";
637
646
  function regEvent(id, handler, cofxOrInterceptors, interceptors) {
@@ -685,17 +694,23 @@ function eventHandlerInterceptor(handler) {
685
694
  return {
686
695
  id: "fx-handler",
687
696
  before(context) {
688
- const coeffects = context.coeffects;
689
- const event = coeffects.event;
697
+ const event = context.coeffects.event;
690
698
  const params = event.slice(1);
691
699
  let effects = [];
692
700
  const [newDb, patches] = produceWithPatches(
693
701
  getAppDb(),
694
702
  (draftDb) => {
695
- coeffects.draftDb = draftDb;
696
- effects = handler({ ...coeffects }, ...params) || [];
703
+ const coeffectsWithDb = { ...context.coeffects, draftDb };
704
+ effects = handler(coeffectsWithDb, ...params) || [];
697
705
  }
698
706
  );
707
+ if (IS_DEV) {
708
+ try {
709
+ JSON.stringify(effects);
710
+ } catch (e) {
711
+ consoleLog("warn", `[reflex] Effects ${effects} contain Proxy (probably an Immer draft). Use current() for draftDb values.`);
712
+ }
713
+ }
699
714
  context.newDb = newDb;
700
715
  context.patches = patches;
701
716
  mergeTrace({ tags: { "patches": patches, "effects": effects } });
@@ -740,11 +755,11 @@ function handle(eventV) {
740
755
  function regEventErrorHandler(handler) {
741
756
  registerHandler("error", "event-handler", handler);
742
757
  }
743
- function defaultErrorHandler(originalError, reFrameError) {
758
+ function defaultErrorHandler(originalError, reflexError) {
744
759
  consoleLog("error", "[reflex] Interceptor Exception:", {
745
760
  originalError,
746
- reFrameError,
747
- data: reFrameError.data
761
+ reflexError,
762
+ data: reflexError.data
748
763
  });
749
764
  throw originalError;
750
765
  }
@@ -1119,20 +1134,21 @@ function setupSubsHotReload() {
1119
1134
  return { dispose, accept };
1120
1135
  }
1121
1136
  function HotReloadWrapper({ children }) {
1122
- if (isDebugEnabled()) {
1123
- const key = useHotReloadKey();
1124
- return React.createElement(React.Fragment, { key }, children);
1125
- } else {
1126
- return children;
1127
- }
1137
+ const key = useHotReloadKey();
1138
+ return React.createElement(React.Fragment, { key }, children);
1128
1139
  }
1129
1140
  export {
1141
+ DISPATCH,
1142
+ DISPATCH_LATER,
1130
1143
  HotReloadWrapper,
1144
+ NOW,
1145
+ RANDOM,
1131
1146
  clearGlobalInterceptors,
1132
1147
  clearHandlers,
1133
1148
  clearHotReloadCallbacks,
1134
1149
  clearReactions,
1135
1150
  clearSubs,
1151
+ current,
1136
1152
  debounceAndDispatch,
1137
1153
  defaultErrorHandler,
1138
1154
  disableTracing,
@@ -1144,7 +1160,7 @@ export {
1144
1160
  getHandler,
1145
1161
  getSubscriptionValue,
1146
1162
  initAppDb,
1147
- isDebugEnabled,
1163
+ original,
1148
1164
  regCoeffect,
1149
1165
  regEffect,
1150
1166
  regEvent,
@@ -1153,7 +1169,6 @@ export {
1153
1169
  regSub,
1154
1170
  registerHotReloadCallback,
1155
1171
  registerTraceCb,
1156
- setDebugEnabled,
1157
1172
  setupSubsHotReload,
1158
1173
  throttleAndDispatch,
1159
1174
  triggerHotReload,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flexsurfer/reflex",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,7 +30,9 @@
30
30
  "dev": "tsup --watch",
31
31
  "test": "jest --passWithNoTests",
32
32
  "test:clean": "jest --passWithNoTests --silent",
33
- "test:watch": "jest --passWithNoTests --watch"
33
+ "test:watch": "jest --passWithNoTests --watch",
34
+ "test:ci": "jest --passWithNoTests --coverage --silent",
35
+ "prepublishOnly": "npm run test:ci && npm run build"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@testing-library/dom": "^10.4.0",