synstate 3.2.1 → 3.4.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.
Files changed (91) hide show
  1. package/README.md +2 -2
  2. package/dist/core/base/create-child-observable.d.mts +9 -2
  3. package/dist/core/base/create-child-observable.d.mts.map +1 -1
  4. package/dist/core/base/create-child-observable.mjs +49 -11
  5. package/dist/core/base/create-child-observable.mjs.map +1 -1
  6. package/dist/core/base/create-observable-base.d.mts +5 -1
  7. package/dist/core/base/create-observable-base.d.mts.map +1 -1
  8. package/dist/core/base/create-observable-base.mjs +24 -4
  9. package/dist/core/base/create-observable-base.mjs.map +1 -1
  10. package/dist/core/base/create-root-observable.d.mts +9 -0
  11. package/dist/core/base/create-root-observable.d.mts.map +1 -1
  12. package/dist/core/base/create-root-observable.mjs +32 -3
  13. package/dist/core/base/create-root-observable.mjs.map +1 -1
  14. package/dist/core/combine/merge.d.mts +0 -3
  15. package/dist/core/combine/merge.d.mts.map +1 -1
  16. package/dist/core/combine/merge.mjs +0 -3
  17. package/dist/core/combine/merge.mjs.map +1 -1
  18. package/dist/core/combine/zip.d.mts +4 -2
  19. package/dist/core/combine/zip.d.mts.map +1 -1
  20. package/dist/core/combine/zip.mjs +21 -3
  21. package/dist/core/combine/zip.mjs.map +1 -1
  22. package/dist/core/create/counter.d.mts +9 -3
  23. package/dist/core/create/counter.d.mts.map +1 -1
  24. package/dist/core/create/counter.mjs +16 -3
  25. package/dist/core/create/counter.mjs.map +1 -1
  26. package/dist/core/create/from-subscribable.d.mts.map +1 -1
  27. package/dist/core/create/from-subscribable.mjs +34 -11
  28. package/dist/core/create/from-subscribable.mjs.map +1 -1
  29. package/dist/core/create/source.d.mts +7 -0
  30. package/dist/core/create/source.d.mts.map +1 -1
  31. package/dist/core/create/source.mjs +6 -2
  32. package/dist/core/create/source.mjs.map +1 -1
  33. package/dist/core/operators/merge-map.d.mts +0 -3
  34. package/dist/core/operators/merge-map.d.mts.map +1 -1
  35. package/dist/core/operators/merge-map.mjs +0 -3
  36. package/dist/core/operators/merge-map.mjs.map +1 -1
  37. package/dist/core/operators/skip-until.d.mts.map +1 -1
  38. package/dist/core/operators/skip-until.mjs +15 -6
  39. package/dist/core/operators/skip-until.mjs.map +1 -1
  40. package/dist/core/operators/switch-map.d.mts +0 -3
  41. package/dist/core/operators/switch-map.d.mts.map +1 -1
  42. package/dist/core/operators/switch-map.mjs +0 -3
  43. package/dist/core/operators/switch-map.mjs.map +1 -1
  44. package/dist/core/operators/take-until.d.mts.map +1 -1
  45. package/dist/core/operators/take-until.mjs +13 -1
  46. package/dist/core/operators/take-until.mjs.map +1 -1
  47. package/dist/core/operators/with-buffered-from.d.mts.map +1 -1
  48. package/dist/core/operators/with-buffered-from.mjs +12 -3
  49. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  50. package/dist/core/types/observable-family.d.mts +5 -1
  51. package/dist/core/types/observable-family.d.mts.map +1 -1
  52. package/dist/core/types/observable-family.mjs.map +1 -1
  53. package/dist/core/types/observable.d.mts +17 -0
  54. package/dist/core/types/observable.d.mts.map +1 -1
  55. package/dist/core/types/observable.mjs.map +1 -1
  56. package/dist/utils/create-event-emitter.d.mts +2 -2
  57. package/dist/utils/create-reducer.d.mts +10 -1
  58. package/dist/utils/create-reducer.d.mts.map +1 -1
  59. package/dist/utils/create-reducer.mjs +13 -5
  60. package/dist/utils/create-reducer.mjs.map +1 -1
  61. package/dist/utils/create-state.d.mts +7 -0
  62. package/dist/utils/create-state.d.mts.map +1 -1
  63. package/dist/utils/create-state.mjs +7 -0
  64. package/dist/utils/create-state.mjs.map +1 -1
  65. package/package.json +4 -4
  66. package/src/core/base/create-child-observable.mts +68 -10
  67. package/src/core/base/create-observable-base.mts +31 -3
  68. package/src/core/base/create-root-observable.mts +43 -2
  69. package/src/core/base/write-during-propagation.test.mts +234 -0
  70. package/src/core/combine/merge.mts +0 -3
  71. package/src/core/combine/zip.mts +36 -5
  72. package/src/core/create/counter.mts +27 -6
  73. package/src/core/create/counter.test.mts +82 -0
  74. package/src/core/create/from-subscribable.mts +33 -5
  75. package/src/core/create/from-subscribable.test.mts +129 -0
  76. package/src/core/create/source.mts +18 -3
  77. package/src/core/create/source.test.mts +41 -0
  78. package/src/core/operators/merge-map.mts +0 -3
  79. package/src/core/operators/skip-until.mts +21 -9
  80. package/src/core/operators/switch-map.mts +0 -3
  81. package/src/core/operators/take-until-skip-until-teardown.test.mts +127 -0
  82. package/src/core/operators/take-until.mts +15 -1
  83. package/src/core/operators/with-buffered-from.mts +15 -3
  84. package/src/core/operators/with-buffered-from.test.mts +56 -0
  85. package/src/core/types/observable-family.mts +5 -1
  86. package/src/core/types/observable.mts +19 -0
  87. package/src/utils/create-event-emitter.mts +2 -2
  88. package/src/utils/create-reducer.mts +14 -8
  89. package/src/utils/create-reducer.test.mts +12 -0
  90. package/src/utils/create-state.mts +7 -0
  91. package/src/utils/create-state.test.mts +20 -0
@@ -1 +1 @@
1
- {"version":3,"file":"create-reducer.d.mts","sourceRoot":"","sources":["../../src/utils/create-reducer.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAU,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,CAAC,WACvB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,gBACR,CAAC,YACL,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KACjC,SAAS,CACV,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAC1B,KAAK,EAAE,QAAQ,CAAC;IACd,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC,CAuBH,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,QAAQ,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;CAC3C,CAAC,CAAC"}
1
+ {"version":3,"file":"create-reducer.d.mts","sourceRoot":"","sources":["../../src/utils/create-reducer.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAU,KAAK,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAAE,CAAC,WACvB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,gBACR,CAAC,YACL,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KACjC,SAAS,CACV,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,EAC1B,KAAK,EAAE,QAAQ,CAAC;IACd,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC,CAoBH,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,YAAY,CAAC,CAAC,IAAI,QAAQ,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;CAC3C,CAAC,CAAC"}
@@ -56,6 +56,15 @@ import { source } from '../core/index.mjs';
56
56
 
57
57
 
58
58
 
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
59
68
 
60
69
 
61
70
 
@@ -72,11 +81,10 @@ export const createReducer = (reducer, initialState, options) => {
72
81
  const dispatch = (action) => {
73
82
  const currentState = state.getSnapshot().value;
74
83
  const nextState = reducer(currentState, action);
75
- if (equals?.(currentState, nextState) === true) {
76
- return currentState;
77
- }
78
- state.next(nextState);
79
- return nextState;
84
+
85
+ return equals?.(currentState, nextState) === true
86
+ ? currentState
87
+ : state.next(nextState);
80
88
  };
81
89
  const getSnapshot = () => state.getSnapshot().value;
82
90
  return [state, dispatch, { getSnapshot, initialState }];
@@ -1 +1 @@
1
- {"version":3,"file":"create-reducer.mjs","sourceRoot":"","sources":["../../src/utils/create-reducer.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAA8B,MAAM,mBAAmB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,OAAsB,EACtB,YAAe,EACf,OAAkC,EAQlC,EAAE;IACF,MAAM,KAAK,GAAG,MAAM,CAAI,YAAY,CAAC,CAAC;IAEtC,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC;IAE/B,MAAM,QAAQ,GAAG,CAAC,MAAS,EAAK,EAAE;QAChC,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC;QAE/C,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QAEhD,IAAI,MAAM,EAAE,CAAC,YAAY,EAAE,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;YAC/C,OAAO,YAAY,CAAC;QACtB,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAEtB,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,GAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC;IAEvD,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,CAAU,CAAC;AACnE,CAAC,CAAC"}
1
+ {"version":3,"file":"create-reducer.mjs","sourceRoot":"","sources":["../../src/utils/create-reducer.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAA8B,MAAM,mBAAmB,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,OAAsB,EACtB,YAAe,EACf,OAAkC,EAQlC,EAAE;IACF,MAAM,KAAK,GAAG,MAAM,CAAI,YAAY,CAAC,CAAC;IAEtC,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,CAAC;IAE/B,MAAM,QAAQ,GAAG,CAAC,MAAS,EAAK,EAAE;QAChC,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC;QAE/C,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QAEhD,8DAA8D;QAC9D,OAAO,MAAM,EAAE,CAAC,YAAY,EAAE,SAAS,CAAC,KAAK,IAAI;YAC/C,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,GAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC;IAEvD,OAAO,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,YAAY,EAAE,CAAU,CAAC;AACnE,CAAC,CAAC"}
@@ -66,6 +66,13 @@ import { type StateOptions } from './create-reducer.mjs';
66
66
  * 'label:bug page:2',
67
67
  * ]);
68
68
  * ```
69
+ *
70
+ * @note Do not call `setState`, `updateState` or `resetState` from a subscriber or a projection while another
71
+ * update is propagating: the write propagates immediately, so subscribers can
72
+ * see an intermediate state, and it logs a `console.warn` (once per
73
+ * observable). Derive the value from its source instead —
74
+ * `const b$ = a$.pipe(map((a) => a * 2))` rather than
75
+ * `a$.subscribe((a) => { setB(a * 2); })`.
69
76
  */
70
77
  export declare const createState: <S>(initialState: S, options?: StateOptions<NoInfer<S>>) => readonly [state: InitializedObservable<S>, setState: (v: S) => S, utils: Readonly<{
71
78
  updateState: (updateFn: (prev: S) => S) => S;
@@ -1 +1 @@
1
- {"version":3,"file":"create-state.d.mts","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAuBxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,gBACb,CAAC,YACL,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KACjC,SAAS,CACV,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EACrB,KAAK,EAAE,QAAQ,CAAC;IACd,WAAW,EAAE,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC,CA0BH,CAAC"}
1
+ {"version":3,"file":"create-state.d.mts","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAC/D,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAuBxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,gBACb,CAAC,YACL,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KACjC,SAAS,CACV,KAAK,EAAE,qBAAqB,CAAC,CAAC,CAAC,EAC/B,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EACrB,KAAK,EAAE,QAAQ,CAAC;IACd,WAAW,EAAE,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC;CACjB,CAAC,CA0BH,CAAC"}
@@ -66,6 +66,13 @@ const reducer = (state, action) => {
66
66
 
67
67
 
68
68
 
69
+
70
+
71
+
72
+
73
+
74
+
75
+
69
76
 
70
77
 
71
78
 
@@ -1 +1 @@
1
- {"version":3,"file":"create-state.mjs","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAaxE,MAAM,OAAO,GAAG,CAAK,KAAQ,EAAE,MAAiB,EAAK,EAAE;IACrD,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,KAAK;YACR,OAAO,MAAM,CAAC,SAAS,CAAC;QAE1B,KAAK,QAAQ;YACX,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,YAAe,EACf,OAAkC,EAUlC,EAAE;IACF,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,CAAC,GAAG,aAAa,CACtD,OAAO,EACP,YAAY,EACZ,OAAO,CACR,CAAC;IAEF,MAAM,WAAW,GAAG,CAAC,QAAwB,EAAK,EAAE,CAClD,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IAEzC,MAAM,QAAQ,GAAG,CAAC,SAAY,EAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAE3E,MAAM,UAAU,GAAG,GAAM,EAAE,CACzB,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC;IAErD,OAAO;QACL,KAAK;QACL,QAAQ;QACR;YACE,WAAW;YACX,UAAU;YACV,WAAW;YACX,YAAY;SACb;KACO,CAAC;AACb,CAAC,CAAC"}
1
+ {"version":3,"file":"create-state.mjs","sourceRoot":"","sources":["../../src/utils/create-state.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAaxE,MAAM,OAAO,GAAG,CAAK,KAAQ,EAAE,MAAiB,EAAK,EAAE;IACrD,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,KAAK;YACR,OAAO,MAAM,CAAC,SAAS,CAAC;QAE1B,KAAK,QAAQ;YACX,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClC,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,YAAe,EACf,OAAkC,EAUlC,EAAE;IACF,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,WAAW,EAAE,CAAC,GAAG,aAAa,CACtD,OAAO,EACP,YAAY,EACZ,OAAO,CACR,CAAC;IAEF,MAAM,WAAW,GAAG,CAAC,QAAwB,EAAK,EAAE,CAClD,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IAEzC,MAAM,QAAQ,GAAG,CAAC,SAAY,EAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAE3E,MAAM,UAAU,GAAG,GAAM,EAAE,CACzB,QAAQ,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC;IAErD,OAAO;QACL,KAAK;QACL,QAAQ;QACR;YACE,WAAW;YACX,UAAU;YACV,WAAW;YACX,YAAY;SACb;KACO,CAAC;AACb,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synstate",
3
- "version": "3.2.1",
3
+ "version": "3.4.0",
4
4
  "description": "Type-safe State Management Library for TypeScript/JavaScript",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,7 +39,7 @@
39
39
  "@types/react": "19.3.0",
40
40
  "@vitest/browser-playwright": "5.0.3",
41
41
  "argparse": "3.0.2",
42
- "eslint-config-typed": "5.17.4",
42
+ "eslint-config-typed": "5.18.0",
43
43
  "eslint-plugin-ts-data-forge": "0.8.6",
44
44
  "eslint-plugin-ts-fortress": "1.3.3",
45
45
  "eslint-plugin-ts-type-forge": "0.8.8",
@@ -51,8 +51,8 @@
51
51
  "prettier-plugin-packagejson": "3.0.2",
52
52
  "react": "19.3.0",
53
53
  "rxjs": "7.8.2",
54
- "synstate-react-hooks": "3.2.1",
55
- "synstate-react-hooks-compat": "3.2.1",
54
+ "synstate-react-hooks": "3.4.0",
55
+ "synstate-react-hooks-compat": "3.4.0",
56
56
  "ts-repo-utils": "10.7.1",
57
57
  "valtio": "2.3.2",
58
58
  "zustand": "5.0.15"
@@ -57,6 +57,14 @@ type ChildObservableConfigBase<P extends NonEmptyUnknownList> = Readonly<{
57
57
  * `override complete() { teardown(); super.complete(); }`.
58
58
  */
59
59
  onComplete?: () => void;
60
+
61
+ /**
62
+ * Whether this observable can emit nothing more although some parent is
63
+ * still live, asked whenever a parent completes. `true` completes it then,
64
+ * as all parents completing does (`zip` once a completed source's queue is
65
+ * empty).
66
+ */
67
+ canEmitNoMore?: () => boolean;
60
68
  }>;
61
69
 
62
70
  export type ChildObservableConfig<
@@ -91,6 +99,7 @@ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
91
99
  depth = 1 + maxDepth(parents),
92
100
  initialValue,
93
101
  onComplete,
102
+ canEmitNoMore,
94
103
  }: ChildObservableConfig<A, P>,
95
104
  createTryUpdate: (
96
105
  tools: SyncChildObservableTools<A>,
@@ -101,7 +110,10 @@ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
101
110
  /** Set once assembled; see {@link unregisterChild}. */
102
111
  let mut_self: ChildObservable<A> | undefined = undefined;
103
112
 
104
- const complete = (): void => {
113
+ const autoComplete =
114
+ onComplete !== undefined || parents.some((p) => p.autoComplete);
115
+
116
+ const dispose = (): void => {
105
117
  // A completed node is reached again through every parent and child that
106
118
  // completes after it; running again would repeat the teardown and the
107
119
  // ancestor walk in `unregisterChild` for each of them.
@@ -116,6 +128,14 @@ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
116
128
  if (mut_self !== undefined) {
117
129
  unregisterChild(mut_self, parents);
118
130
  }
131
+ };
132
+
133
+ const complete = (): void => {
134
+ if (handle.isCompleted()) {
135
+ return;
136
+ }
137
+
138
+ dispose();
119
139
 
120
140
  // propagate to parents
121
141
  for (const par of parents) {
@@ -129,8 +149,10 @@ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
129
149
  }
130
150
 
131
151
  tryCompleteChild({
152
+ canEmitNoMore,
132
153
  hasSubscriber: handle.hasSubscriber(),
133
154
  hasActiveChild: handle.hasActiveChild(),
155
+ autoComplete,
134
156
  parents,
135
157
  complete,
136
158
  });
@@ -149,12 +171,21 @@ export const createSyncChildObservable = <A, P extends NonEmptyUnknownList>(
149
171
  tryUpdate,
150
172
  tryComplete,
151
173
  complete,
174
+ dispose,
175
+ autoComplete,
152
176
  extra: { parents },
153
177
  });
154
178
 
155
- registerChild(observable, parents);
179
+ // Completed parents never update again, so a child created under them is
180
+ // complete from the start; registering it would only leave a reference in
181
+ // each parent (#2248).
182
+ if (parents.every((p) => p.isCompleted)) {
183
+ dispose();
184
+ } else {
185
+ registerChild(observable, parents);
156
186
 
157
- mut_self = observable;
187
+ mut_self = observable;
188
+ }
158
189
 
159
190
  return observable;
160
191
  };
@@ -196,6 +227,7 @@ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
196
227
  depth = 1 + maxDepth(parents),
197
228
  initialValue,
198
229
  onComplete,
230
+ canEmitNoMore,
199
231
  }: ChildObservableConfig<A, P>,
200
232
  createTryUpdate: (
201
233
  tools: AsyncChildObservableTools<A>,
@@ -209,7 +241,10 @@ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
209
241
  /** Set once assembled; see {@link unregisterChild}. */
210
242
  let mut_self: ChildObservable<A> | undefined = undefined;
211
243
 
212
- const complete = (): void => {
244
+ const autoComplete =
245
+ onComplete !== undefined || parents.some((p) => p.autoComplete);
246
+
247
+ const dispose = (): void => {
213
248
  // A completed node is reached again through every parent and child that
214
249
  // completes after it; running again would repeat the teardown and the
215
250
  // ancestor walk in `unregisterChild` for each of them.
@@ -224,6 +259,14 @@ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
224
259
  if (mut_self !== undefined) {
225
260
  unregisterChild(mut_self, parents);
226
261
  }
262
+ };
263
+
264
+ const complete = (): void => {
265
+ if (handle.isCompleted()) {
266
+ return;
267
+ }
268
+
269
+ dispose();
227
270
 
228
271
  // propagate to parents
229
272
  for (const par of parents) {
@@ -237,8 +280,10 @@ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
237
280
  }
238
281
 
239
282
  tryCompleteChild({
283
+ canEmitNoMore,
240
284
  hasSubscriber: handle.hasSubscriber(),
241
285
  hasActiveChild: handle.hasActiveChild(),
286
+ autoComplete,
242
287
  parents,
243
288
  complete,
244
289
  });
@@ -258,12 +303,21 @@ export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
258
303
  tryUpdate,
259
304
  tryComplete,
260
305
  complete,
306
+ dispose,
307
+ autoComplete,
261
308
  extra: { parents, addDescendant, deleteDescendant },
262
309
  });
263
310
 
264
- registerChild(observable, parents);
311
+ // Completed parents never update again, so a child created under them is
312
+ // complete from the start; registering it would only leave a reference in
313
+ // each parent (#2248).
314
+ if (parents.every((p) => p.isCompleted)) {
315
+ dispose();
316
+ } else {
317
+ registerChild(observable, parents);
265
318
 
266
- mut_self = observable;
319
+ mut_self = observable;
320
+ }
267
321
 
268
322
  return observable;
269
323
  };
@@ -398,25 +452,29 @@ const unregisterChild = <A,>(
398
452
  };
399
453
 
400
454
  const tryCompleteChild = <A,>({
455
+ canEmitNoMore,
401
456
  hasSubscriber,
402
457
  hasActiveChild,
458
+ autoComplete,
403
459
  parents,
404
460
  complete,
405
461
  }: Readonly<{
462
+ canEmitNoMore: (() => boolean) | undefined;
406
463
  hasSubscriber: boolean;
407
464
  hasActiveChild: boolean;
465
+ autoComplete: boolean;
408
466
  parents: ChildObservable<A>['parents'];
409
467
  complete: () => void;
410
468
  }>): void => {
411
- // If there is no working parent node
412
- if (parents.every((r) => r.isCompleted)) {
469
+ // If there is no working parent node, or no value can come from the live ones
470
+ if (parents.every((r) => r.isCompleted) || canEmitNoMore?.() === true) {
413
471
  complete();
414
472
 
415
473
  return;
416
474
  }
417
475
 
418
- // If there are no active child node
419
- if (!hasSubscriber && !hasActiveChild) {
476
+ // If there are no active child node, and completing releases something
477
+ if (autoComplete && !hasSubscriber && !hasActiveChild) {
420
478
  complete();
421
479
  }
422
480
 
@@ -285,6 +285,16 @@ export const createObservableBaseHandle = <A,>(
285
285
  };
286
286
  };
287
287
 
288
+ /**
289
+ * How many `startUpdate` calls are running, across every observable. A root
290
+ * written to while this is non-zero is written to from inside a subscriber or
291
+ * a projection, which {@link isPropagating} lets it detect (#2336).
292
+ */
293
+ const mut_propagation = { depth: 0 };
294
+
295
+ /** Whether some observable's update is propagating right now. */
296
+ export const isPropagating = (): boolean => mut_propagation.depth > 0;
297
+
288
298
  /**
289
299
  * The extra members every manager observable — a root or an async child — owns:
290
300
  * the depth-ordered descendant list and the update propagation that walks it.
@@ -389,10 +399,20 @@ export const createManagerObservableParts = <A,>(
389
399
  const startUpdate = (nextValue: A): void => {
390
400
  const updateToken = issueUpdateToken();
391
401
 
392
- handle.setNext(nextValue, updateToken);
402
+ mut_propagation.depth += 1;
393
403
 
394
- for (const p of getPropagationOrder()) {
395
- p.tryUpdate(updateToken);
404
+ // A subscriber or projection may throw, and the depth has to come back
405
+ // down regardless, or every later write would warn. The exception itself
406
+ // is not caught: it reaches the caller of `next` as it always has.
407
+ // @sumi-expect-error exceptions/no-try
408
+ try {
409
+ handle.setNext(nextValue, updateToken);
410
+
411
+ for (const p of getPropagationOrder()) {
412
+ p.tryUpdate(updateToken);
413
+ }
414
+ } finally {
415
+ mut_propagation.depth -= 1;
396
416
  }
397
417
  };
398
418
 
@@ -425,6 +445,8 @@ type AssembleObservableArgs<
425
445
  tryUpdate: (updateToken: UpdateToken) => void;
426
446
  tryComplete: () => void;
427
447
  complete: () => void;
448
+ dispose: () => void;
449
+ autoComplete: boolean;
428
450
  /**
429
451
  * Kind-specific public members (`parents`, `addDescendant`) and leaf
430
452
  * extensions (`next`, `start`). They are merged here, in the one place the
@@ -454,6 +476,8 @@ export const assembleObservable = <
454
476
  tryUpdate,
455
477
  tryComplete,
456
478
  complete,
479
+ dispose,
480
+ autoComplete,
457
481
  extra,
458
482
  }: AssembleObservableArgs<A, Kind, Extra>): Extra &
459
483
  ObservableBase<A> &
@@ -507,12 +531,16 @@ export const assembleObservable = <
507
531
 
508
532
  hasActiveChild: handle.hasActiveChild,
509
533
 
534
+ autoComplete,
535
+
510
536
  tryUpdate,
511
537
 
512
538
  tryComplete,
513
539
 
514
540
  complete,
515
541
 
542
+ dispose,
543
+
516
544
  subscribe: handle.subscribe,
517
545
 
518
546
  pipe,
@@ -6,6 +6,7 @@ import {
6
6
  createManagerObservableParts,
7
7
  createObservableBaseHandle,
8
8
  createTryUpdateNotImplemented,
9
+ isPropagating,
9
10
  } from './create-observable-base.mjs';
10
11
 
11
12
  /**
@@ -17,6 +18,8 @@ export type RootObservableTools<A> = Readonly<{
17
18
  /** Emits `nextValue` and propagates the update to all descendants. */
18
19
  startUpdate: (nextValue: A) => void;
19
20
 
21
+ getSnapshot: () => Optional<A>;
22
+
20
23
  isCompleted: () => boolean;
21
24
 
22
25
  /** Completes this observable (leaf teardown included). */
@@ -68,18 +71,43 @@ export const createRootObservable = <
68
71
  handle.completeBase();
69
72
  };
70
73
 
74
+ // A root without a teardown (`source`, `createState`, `just`) releases
75
+ // nothing by completing, so a child completing never completes it (#2133).
76
+ const autoComplete = onComplete !== undefined;
77
+
71
78
  const tryComplete = (): void => {
72
- if (!handle.hasSubscriber() && !handle.hasActiveChild()) {
79
+ if (autoComplete && !handle.hasSubscriber() && !handle.hasActiveChild()) {
73
80
  complete();
74
81
  }
75
82
  };
76
83
 
84
+ // A root created while an update propagates (inside a `switchMap`
85
+ // projection, say) may emit synchronously while `init` runs, as
86
+ // `fromSubscribable` does for a synchronous source. Nothing is derived from
87
+ // it yet, so that is its initial value rather than a write (#2336).
88
+ let mut_isInitializing = true;
89
+
90
+ let mut_hasWarned = false;
91
+
92
+ const startUpdateWithWarning = (nextValue: A): void => {
93
+ if (!mut_isInitializing && !mut_hasWarned && isPropagating()) {
94
+ mut_hasWarned = true;
95
+
96
+ console.warn(writeDuringPropagationWarning);
97
+ }
98
+
99
+ startUpdate(nextValue);
100
+ };
101
+
77
102
  const extension: Extension = init({
78
- startUpdate,
103
+ startUpdate: startUpdateWithWarning,
104
+ getSnapshot: handle.getSnapshot,
79
105
  isCompleted: handle.isCompleted,
80
106
  complete,
81
107
  });
82
108
 
109
+ mut_isInitializing = false;
110
+
83
111
  return assembleObservable({
84
112
  kind: 'root',
85
113
  depth: 0,
@@ -87,6 +115,9 @@ export const createRootObservable = <
87
115
  tryUpdate: createTryUpdateNotImplemented,
88
116
  tryComplete,
89
117
  complete,
118
+ // A root has no parents to keep out of it.
119
+ dispose: complete,
120
+ autoComplete,
90
121
  extra: {
91
122
  ...extension,
92
123
  addDescendant,
@@ -95,6 +126,16 @@ export const createRootObservable = <
95
126
  });
96
127
  };
97
128
 
129
+ /**
130
+ * Shown once per root observable, the first time it is written to (`next`,
131
+ * `setState`, `dispatch`, …) while another update is still propagating —
132
+ * from a subscriber or a projection. The write still propagates immediately,
133
+ * as it always has; the warning is there because the pattern is wrong, not
134
+ * because anything was refused.
135
+ */
136
+ export const writeDuringPropagationWarning =
137
+ 'synstate: an observable was written to (next / setState / dispatch) while an update was propagating, so subscribers can see intermediate states. Derive the value with map / combine instead of writing from a subscriber or a projection.';
138
+
98
139
  if (import.meta.vitest !== undefined) {
99
140
  test('isRootObservable', () => {
100
141
  assert.isTrue(