synstate 6.1.0 → 6.2.1

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 (151) hide show
  1. package/README.md +4 -4
  2. package/dist/core/authoring.d.mts +3 -3
  3. package/dist/core/class/child-observable-class.d.mts +24 -25
  4. package/dist/core/class/child-observable-class.d.mts.map +1 -1
  5. package/dist/core/class/child-observable-class.mjs +41 -42
  6. package/dist/core/class/child-observable-class.mjs.map +1 -1
  7. package/dist/core/class/create-child-observable.d.mts +27 -29
  8. package/dist/core/class/create-child-observable.d.mts.map +1 -1
  9. package/dist/core/class/create-child-observable.mjs.map +1 -1
  10. package/dist/core/class/create-root-observable.d.mts +11 -11
  11. package/dist/core/class/manager-observable-class.d.mts +6 -6
  12. package/dist/core/class/manager-observable-class.d.mts.map +1 -1
  13. package/dist/core/class/manager-observable-class.mjs +6 -0
  14. package/dist/core/class/manager-observable-class.mjs.map +1 -1
  15. package/dist/core/class/observable-base-class.d.mts +32 -26
  16. package/dist/core/class/observable-base-class.d.mts.map +1 -1
  17. package/dist/core/class/observable-base-class.mjs +16 -3
  18. package/dist/core/class/observable-base-class.mjs.map +1 -1
  19. package/dist/core/class/observable-node.d.mts +3 -4
  20. package/dist/core/class/observable-node.d.mts.map +1 -1
  21. package/dist/core/class/observable-node.mjs +0 -1
  22. package/dist/core/class/observable-node.mjs.map +1 -1
  23. package/dist/core/class/propagation-state.d.mts +10 -9
  24. package/dist/core/class/propagation-state.d.mts.map +1 -1
  25. package/dist/core/class/propagation-state.mjs +4 -2
  26. package/dist/core/class/propagation-state.mjs.map +1 -1
  27. package/dist/core/class/root-observable-class.d.mts +4 -4
  28. package/dist/core/combine/derive.d.mts +7 -8
  29. package/dist/core/combine/derive.d.mts.map +1 -1
  30. package/dist/core/combine/derive.mjs +0 -1
  31. package/dist/core/combine/derive.mjs.map +1 -1
  32. package/dist/core/combine/merge.mjs.map +1 -1
  33. package/dist/core/combine/zip.d.mts +3 -3
  34. package/dist/core/combine/zip.mjs.map +1 -1
  35. package/dist/core/create/counter.d.mts +3 -3
  36. package/dist/core/create/counter.mjs.map +1 -1
  37. package/dist/core/create/from-abortable-promise.mjs.map +1 -1
  38. package/dist/core/create/from-promise.mjs.map +1 -1
  39. package/dist/core/create/from-subscribable.mjs.map +1 -1
  40. package/dist/core/create/source.mjs.map +1 -1
  41. package/dist/core/create/timer.mjs.map +1 -1
  42. package/dist/core/operators/audit.mjs.map +1 -1
  43. package/dist/core/operators/debounce.mjs.map +1 -1
  44. package/dist/core/operators/filter.mjs.map +1 -1
  45. package/dist/core/operators/keep-equal-parts.d.mts +14 -15
  46. package/dist/core/operators/keep-equal-parts.d.mts.map +1 -1
  47. package/dist/core/operators/keep-equal-parts.mjs +0 -1
  48. package/dist/core/operators/keep-equal-parts.mjs.map +1 -1
  49. package/dist/core/operators/map.mjs.map +1 -1
  50. package/dist/core/operators/merge-map.d.mts +3 -3
  51. package/dist/core/operators/merge-map.mjs.map +1 -1
  52. package/dist/core/operators/pairwise.mjs.map +1 -1
  53. package/dist/core/operators/scan.mjs.map +1 -1
  54. package/dist/core/operators/skip-if-no-change.mjs.map +1 -1
  55. package/dist/core/operators/skip-until.mjs.map +1 -1
  56. package/dist/core/operators/skip-while.mjs.map +1 -1
  57. package/dist/core/operators/switch-map.mjs.map +1 -1
  58. package/dist/core/operators/take-until.mjs.map +1 -1
  59. package/dist/core/operators/take-while.mjs.map +1 -1
  60. package/dist/core/operators/take.mjs.map +1 -1
  61. package/dist/core/operators/throttle.mjs.map +1 -1
  62. package/dist/core/operators/with-buffered-from.mjs.map +1 -1
  63. package/dist/core/operators/with-current-value-from.mjs.map +1 -1
  64. package/dist/core/operators/with-initial-value.mjs.map +1 -1
  65. package/dist/core/predefined/operators/attach-index.mjs.map +1 -1
  66. package/dist/core/predefined/operators/map-to.d.mts +3 -4
  67. package/dist/core/predefined/operators/map-to.d.mts.map +1 -1
  68. package/dist/core/predefined/operators/map-to.mjs +0 -1
  69. package/dist/core/predefined/operators/map-to.mjs.map +1 -1
  70. package/dist/core/scope/scope.d.mts +4 -4
  71. package/dist/core/types/id.d.mts +2 -1
  72. package/dist/core/types/id.d.mts.map +1 -1
  73. package/dist/core/types/observable-family.d.mts.map +1 -1
  74. package/dist/core/types/observable-family.mjs.map +1 -1
  75. package/dist/core/types/observable.d.mts +7 -6
  76. package/dist/core/types/observable.d.mts.map +1 -1
  77. package/dist/core/types/observable.mjs.map +1 -1
  78. package/dist/core/utils/current-scope.d.mts +6 -6
  79. package/dist/core/utils/id-maker.d.mts +7 -0
  80. package/dist/core/utils/id-maker.d.mts.map +1 -1
  81. package/dist/core/utils/id-maker.mjs +9 -0
  82. package/dist/core/utils/id-maker.mjs.map +1 -1
  83. package/dist/core/utils/observable-brand.d.mts +5 -5
  84. package/dist/utils/create-boolean-state.d.mts +2 -2
  85. package/dist/utils/create-reducer.d.mts +20 -20
  86. package/dist/utils/create-reducer.d.mts.map +1 -1
  87. package/dist/utils/create-reducer.mjs +4 -16
  88. package/dist/utils/create-reducer.mjs.map +1 -1
  89. package/dist/utils/create-state.d.mts +3 -3
  90. package/dist/utils/create-state.d.mts.map +1 -1
  91. package/dist/utils/create-state.mjs +10 -13
  92. package/dist/utils/create-state.mjs.map +1 -1
  93. package/dist/utils/state-source.d.mts +19 -0
  94. package/dist/utils/state-source.d.mts.map +1 -0
  95. package/dist/utils/state-source.mjs +31 -0
  96. package/dist/utils/state-source.mjs.map +1 -0
  97. package/package.json +10 -10
  98. package/src/core/authoring.mts +3 -3
  99. package/src/core/class/child-observable-class.mts +86 -87
  100. package/src/core/class/circular-dependency-comparison.test.mts +2 -2
  101. package/src/core/class/create-child-observable.mts +27 -29
  102. package/src/core/class/create-root-observable.mts +11 -11
  103. package/src/core/class/manager-observable-class.mts +16 -9
  104. package/src/core/class/observable-base-class.mts +48 -30
  105. package/src/core/class/observable-node.mts +3 -4
  106. package/src/core/class/propagation-state.mts +31 -29
  107. package/src/core/class/remove-completed.test.mts +2 -2
  108. package/src/core/class/root-observable-class.mts +9 -9
  109. package/src/core/class/unobserved-write.test.mts +194 -0
  110. package/src/core/combine/derive.mts +10 -11
  111. package/src/core/combine/merge.mts +2 -2
  112. package/src/core/combine/zip.mts +7 -7
  113. package/src/core/create/counter.mts +5 -5
  114. package/src/core/create/from-abortable-promise.mts +2 -2
  115. package/src/core/create/from-promise.mts +2 -2
  116. package/src/core/create/from-subscribable.mts +2 -2
  117. package/src/core/create/source.mts +5 -5
  118. package/src/core/create/timer.mts +2 -2
  119. package/src/core/operators/audit.mts +2 -2
  120. package/src/core/operators/debounce.mts +2 -2
  121. package/src/core/operators/filter.mts +4 -4
  122. package/src/core/operators/keep-equal-parts.mts +16 -17
  123. package/src/core/operators/map.mts +2 -2
  124. package/src/core/operators/merge-map.mts +5 -5
  125. package/src/core/operators/pairwise.mts +4 -4
  126. package/src/core/operators/scan.mts +2 -2
  127. package/src/core/operators/skip-if-no-change.mts +4 -4
  128. package/src/core/operators/skip-until.mts +4 -4
  129. package/src/core/operators/skip-while.mts +2 -2
  130. package/src/core/operators/switch-map.mts +5 -5
  131. package/src/core/operators/take-until.mts +2 -2
  132. package/src/core/operators/take-while.mts +2 -2
  133. package/src/core/operators/take.mts +2 -2
  134. package/src/core/operators/throttle.mts +2 -2
  135. package/src/core/operators/with-buffered-from.mts +2 -2
  136. package/src/core/operators/with-current-value-from.mts +2 -2
  137. package/src/core/operators/with-initial-value.mts +2 -2
  138. package/src/core/predefined/operators/attach-index.mts +2 -2
  139. package/src/core/predefined/operators/map-to.mts +3 -4
  140. package/src/core/scope/scope.mts +6 -6
  141. package/src/core/types/id.mts +2 -1
  142. package/src/core/types/observable-family.mts +4 -5
  143. package/src/core/types/observable.mts +9 -8
  144. package/src/core/utils/current-scope.mts +8 -8
  145. package/src/core/utils/id-maker.mts +10 -0
  146. package/src/core/utils/observable-brand.mts +5 -5
  147. package/src/utils/create-boolean-state.mts +2 -2
  148. package/src/utils/create-reducer.mts +27 -42
  149. package/src/utils/create-state.mts +14 -31
  150. package/src/utils/create-state.test.mts +20 -2
  151. package/src/utils/state-source.mts +48 -0
@@ -1,12 +1,4 @@
1
- import { createReducer } from './create-reducer.mjs';
2
- const reducer = (state, action) => {
3
- switch (action.type) {
4
- case 'set':
5
- return action.nextState;
6
- case 'update':
7
- return action.updateFn(state);
8
- }
9
- };
1
+ import { createStateSource } from './state-source.mjs';
10
2
 
11
3
 
12
4
 
@@ -134,10 +126,15 @@ const reducer = (state, action) => {
134
126
 
135
127
 
136
128
  export const createState = (initialState, options) => {
137
- const [state, dispatch, { getSnapshot }] = createReducer(reducer, initialState, options);
138
- const updateState = (updateFn) => dispatch({ type: 'update', updateFn });
139
- const setState = (nextState) => dispatch({ type: 'set', nextState });
140
- const resetState = () => dispatch({ type: 'set', nextState: initialState });
129
+ const { state, getSnapshot, commit } = createStateSource(initialState, options);
130
+
131
+
132
+ const updateState = (updateFn) => {
133
+ const currentState = getSnapshot();
134
+ return commit(currentState, updateFn(currentState));
135
+ };
136
+ const setState = (nextState) => commit(getSnapshot(), nextState);
137
+ const resetState = () => commit(getSnapshot(), initialState);
141
138
  return [
142
139
  state,
143
140
  setState,
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6HG;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":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6HG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,YAAe,EACf,OAAkC,EAUlC,EAAE;IACF,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,iBAAiB,CACtD,YAAY,EACZ,OAAO,CACR,CAAC;IAEF,2EAA2E;IAC3E,iCAAiC;IACjC,MAAM,WAAW,GAAG,CAAC,QAAwB,EAAK,EAAE;QAClD,MAAM,YAAY,GAAG,WAAW,EAAE,CAAC;QAEnC,OAAO,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,CAAC,SAAY,EAAK,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,SAAS,CAAC,CAAC;IAEvE,MAAM,UAAU,GAAG,GAAM,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,YAAY,CAAC,CAAC;IAEhE,OAAO;QACL,KAAK;QACL,QAAQ;QACR;YACE,WAAW;YACX,UAAU;YACV,WAAW;YACX,YAAY;SACb;KACO,CAAC;AACb,CAAC,CAAC"}
@@ -0,0 +1,19 @@
1
+ import { type InitializedSourceObservable } from '../core/index.mjs';
2
+ import type { StateOptions } from './create-reducer.mjs';
3
+ /**
4
+ * What `createState` and `createReducer` are both built on: the source holding
5
+ * the state, its current value, and `commit`, the one way either writes it.
6
+ * `commit` writes `reduced` unless `equals` finds it the same as
7
+ * `currentState`, after keeping the parts of `currentState` it did not change
8
+ * when `keepEqualParts` asks for that, and returns the state written, or
9
+ * `currentState` when nothing was.
10
+ *
11
+ * Not part of the package's API: `gen:index:src` leaves this file out of the
12
+ * barrel.
13
+ */
14
+ export declare const createStateSource: <S>(initialState: S, options: StateOptions<S> | undefined) => Readonly<{
15
+ state: InitializedSourceObservable<S>;
16
+ getSnapshot: () => S;
17
+ commit: (currentState: S, reduced: S) => S;
18
+ }>;
19
+ //# sourceMappingURL=state-source.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-source.d.mts","sourceRoot":"","sources":["../../src/utils/state-source.mts"],"names":[],"mappings":"AACA,OAAO,EAAU,KAAK,2BAA2B,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,gBACnB,CAAC,WACN,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,KACnC,QAAQ,CAAC;IACV,KAAK,EAAE,2BAA2B,CAAC,CAAC,CAAC,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC,CAAC;IACrB,MAAM,EAAE,CAAC,YAAY,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;CAC5C,CAyBA,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { replaceEqualDeep } from 'ts-data-forge';
2
+ import { source } from '../core/index.mjs';
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+ export const createStateSource = (initialState, options) => {
15
+ const state = source(initialState);
16
+ const getSnapshot = () => state.getSnapshot().value;
17
+ const equals = options?.equals ?? Object.is;
18
+ const keepEqualParts = options?.keepEqualParts ?? false;
19
+ const keepOptions = typeof keepEqualParts === 'boolean' ? undefined : keepEqualParts;
20
+ const commit = (currentState, reduced) => {
21
+ const nextState = keepEqualParts === false
22
+ ? reduced
23
+ : replaceEqualDeep(currentState, reduced, keepOptions);
24
+
25
+ return equals(currentState, nextState)
26
+ ? currentState
27
+ : state.next(nextState);
28
+ };
29
+ return { state, getSnapshot, commit };
30
+ };
31
+ //# sourceMappingURL=state-source.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-source.mjs","sourceRoot":"","sources":["../../src/utils/state-source.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,MAAM,EAAoC,MAAM,mBAAmB,CAAC;AAG7E;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAC/B,YAAe,EACf,OAAoC,EAKnC,EAAE;IACH,MAAM,KAAK,GAAG,MAAM,CAAI,YAAY,CAAC,CAAC;IAEtC,MAAM,WAAW,GAAG,GAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC;IAEvD,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,MAAM,CAAC,EAAE,CAAC;IAE5C,MAAM,cAAc,GAAG,OAAO,EAAE,cAAc,IAAI,KAAK,CAAC;IAExD,MAAM,WAAW,GACf,OAAO,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC;IAEnE,MAAM,MAAM,GAAG,CAAC,YAAe,EAAE,OAAU,EAAK,EAAE;QAChD,MAAM,SAAS,GACb,cAAc,KAAK,KAAK;YACtB,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,gBAAgB,CAAC,YAAY,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;QAE3D,8DAA8D;QAC9D,OAAO,MAAM,CAAC,YAAY,EAAE,SAAS,CAAC;YACpC,CAAC,CAAC,YAAY;YACd,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,EAAE,CAAC;AACxC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synstate",
3
- "version": "6.1.0",
3
+ "version": "6.2.1",
4
4
  "description": "Type-safe State Management Library for TypeScript/JavaScript",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,21 +29,21 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "dependencies": {
32
- "ts-data-forge": "^14.10.0",
32
+ "ts-data-forge": "^14.10.2",
33
33
  "ts-std-forge": "^0.5.2",
34
34
  "ts-type-forge": "^9.2.2"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@preact/signals-core": "1.14.4",
38
38
  "@reduxjs/toolkit": "2.13.0",
39
- "@sumi-lang/cli": "0.1.2",
39
+ "@sumi-lang/cli": "0.1.5",
40
40
  "@types/react": "19.3.0",
41
41
  "@vitest/browser-playwright": "5.0.5",
42
42
  "argparse": "3.0.3",
43
- "eslint-config-typed": "5.20.1",
44
- "eslint-plugin-ts-data-forge": "0.10.4",
45
- "eslint-plugin-ts-fortress": "1.3.9",
46
- "eslint-plugin-ts-type-forge": "0.8.14",
43
+ "eslint-config-typed": "7.3.1",
44
+ "eslint-plugin-ts-data-forge": "0.10.8",
45
+ "eslint-plugin-ts-fortress": "1.3.12",
46
+ "eslint-plugin-ts-type-forge": "0.8.17",
47
47
  "jotai": "3.0.1",
48
48
  "mobx": "7.1.0",
49
49
  "playwright": "1.64.1",
@@ -53,8 +53,8 @@
53
53
  "react": "19.3.1",
54
54
  "rxjs": "7.8.2",
55
55
  "solid-js": "1.9.17",
56
- "synstate-react-hooks": "6.1.0",
57
- "synstate-react-hooks-compat": "6.1.0",
56
+ "synstate-react-hooks": "6.2.1",
57
+ "synstate-react-hooks-compat": "6.2.1",
58
58
  "ts-repo-utils": "10.7.1",
59
59
  "valtio": "2.3.2",
60
60
  "zustand": "5.0.15"
@@ -85,7 +85,7 @@
85
85
  "fmt": "pnpm run fix:fmt",
86
86
  "gen": "run-s gen:index gen:readme gen:jsdoc gen:sample-diffs",
87
87
  "gen:index": "run-s gen:index:src fix:fmt",
88
- "gen:index:src": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/gen-index-ts.mts ./src --index-ext .mts --export-ext .mjs --target-ext .mts --target-ext .tsx --exclude entry-point.mts --exclude \"core/class/**\" --exclude \"core/utils/**\"",
88
+ "gen:index:src": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/gen-index-ts.mts ./src --index-ext .mts --export-ext .mjs --target-ext .mts --target-ext .tsx --exclude entry-point.mts --exclude \"core/class/**\" --exclude \"core/utils/**\" --exclude utils/state-source.mts",
89
89
  "gen:jsdoc": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/embed-examples-in-jsdoc.mts",
90
90
  "gen:readme": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/embed-examples.mts",
91
91
  "gen:sample-diffs": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/generate-sample-diffs.mts",
@@ -1,7 +1,7 @@
1
1
  /**
2
- * What custom creators and operators are written with: the factories every
3
- * built-in observable is made by (#2117). The rest of `core/base` stays
4
- * internal.
2
+ * What custom creators and operators are written with: factories that wrap the
3
+ * base classes the built-in observables extend (#2117). The rest of `core/base`
4
+ * stays internal.
5
5
  */
6
6
  export {
7
7
  createAsyncChildObservable,
@@ -30,11 +30,11 @@ type ChildObservableConfig<A, P extends NonEmptyUnknownList> = Readonly<{
30
30
  parents: Wrap<P>;
31
31
 
32
32
  /**
33
- * Defaults to one more than the deepest parent. An update reaches
34
- * observables in ascending depth, so an observable `tryUpdate` reads with
35
- * `getSnapshot` without being its child (`withCurrentValueFrom`'s second
36
- * source) has to be shallower than this one too, or this one reads its value
37
- * from before the update: pass `1 + Math.max(...)` over both then.
33
+ * Defaults to one more than the deepest parent. An update reaches observables
34
+ * in ascending depth, so an observable `tryUpdate` reads with `getSnapshot`
35
+ * without being its child (`withCurrentValueFrom`'s second source) has to be
36
+ * shallower than this one too, or this one reads its value from before the
37
+ * update: pass `1 + Math.max(...)` over both then.
38
38
  */
39
39
  depth?: number;
40
40
 
@@ -45,9 +45,9 @@ type ChildObservableConfig<A, P extends NonEmptyUnknownList> = Readonly<{
45
45
  * An observable derived from `parents` that emits, if at all, in the same
46
46
  * update as they do (`map`, `filter`, `combine`, …).
47
47
  *
48
- * A subclass overrides `tryUpdate`, and, when it holds something to
49
- * release, `teardown`; its constructor ends with `this.attach()`, once
50
- * everything `tryUpdate` and `teardown` read is in place.
48
+ * A subclass overrides `tryUpdate`, and, when it holds something to release,
49
+ * `teardown`; its constructor ends with `this.attach()`, once everything
50
+ * `tryUpdate` and `teardown` read is in place.
51
51
  */
52
52
  export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
53
53
  extends ObservableBaseClass<A, 'sync child', number>
@@ -107,10 +107,9 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
107
107
  }
108
108
 
109
109
  /**
110
- * Whether this observable can emit nothing more although some parent is
111
- * still live, asked whenever a parent completes. `true` completes it then,
112
- * as all parents completing does (`zip` once a completed source's queue is
113
- * empty).
110
+ * Whether this observable can emit nothing more although some parent is still
111
+ * live, asked whenever a parent completes. `true` completes it then, as all
112
+ * parents completing does (`zip` once a completed source's queue is empty).
114
113
  */
115
114
  // eslint-disable-next-line @typescript-eslint/class-methods-use-this
116
115
  canEmitNoMore(): boolean {
@@ -118,8 +117,8 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
118
117
  }
119
118
 
120
119
  /**
121
- * Completes this observable once the update now propagating has reached
122
- * every descendant, so that they see the value just passed on first.
120
+ * Completes this observable once the update now propagating has reached every
121
+ * descendant, so that they see the value just passed on first.
123
122
  */
124
123
  protected completeAfterPropagation(): void {
125
124
  runAfterPropagation(() => {
@@ -129,8 +128,8 @@ export class SyncChildObservableClass<A, const P extends NonEmptyUnknownList>
129
128
  }
130
129
 
131
130
  /**
132
- * An observable derived from `parents` that may emit later, in an update of
133
- * its own, through `startUpdate` (`debounce`, `switchMap`). It may also emit
131
+ * An observable derived from `parents` that may emit later, in an update of its
132
+ * own, through `startUpdate` (`debounce`, `switchMap`). It may also emit
134
133
  * synchronously with `setNext`, as a sync child does. A subclass may also
135
134
  * override `flush`, to pass on at completion what it still holds.
136
135
  */
@@ -204,13 +203,13 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
204
203
  * `mergeMap` returned.
205
204
  *
206
205
  * If the inner observable already holds a value (`just(x)`, a state), it
207
- * emits that value synchronously while being subscribed. That first value
208
- * is emitted with `updateToken`, the token of the update that ran the
209
- * projection, so it joins the propagation in progress like a sync
210
- * operator's value would. Starting an update of its own instead would
211
- * reach the nodes below before the current update reached this
212
- * observable's siblings: a node reading both would see the new inner value
213
- * beside stale sibling values (a glitch), and would emit twice.
206
+ * emits that value synchronously while being subscribed. That first value is
207
+ * emitted with `updateToken`, the token of the update that ran the
208
+ * projection, so it joins the propagation in progress like a sync operator's
209
+ * value would. Starting an update of its own instead would reach the nodes
210
+ * below before the current update reached this observable's siblings: a node
211
+ * reading both would see the new inner value beside stale sibling values (a
212
+ * glitch), and would emit twice.
214
213
  *
215
214
  * Values the inner observable emits later start updates of their own.
216
215
  */
@@ -236,8 +235,8 @@ export class AsyncChildObservableClass<A, const P extends NonEmptyUnknownList>
236
235
 
237
236
  /**
238
237
  * A sync child that always holds a value: a subclass passes a `Some` as its
239
- * `initialValue`. It differs from its base only in the types of
240
- * `getSnapshot` and `pipe`.
238
+ * `initialValue`. It differs from its base only in the types of `getSnapshot`
239
+ * and `pipe`.
241
240
  */
242
241
  export class InitializedSyncChildObservableClass<
243
242
  A,
@@ -266,8 +265,8 @@ export class InitializedSyncChildObservableClass<
266
265
  /* helpers */
267
266
 
268
267
  /**
269
- * Called by a parent that has completed: whether no live parent is left, or
270
- * no value can come from the live ones.
268
+ * Called by a parent that has completed: whether no live parent is left, or no
269
+ * value can come from the live ones.
271
270
  */
272
271
  const shouldComplete = (
273
272
  child: ChildNode & Readonly<{ canEmitNoMore: () => boolean }>,
@@ -275,6 +274,63 @@ const shouldComplete = (
275
274
  !child.isCompleted &&
276
275
  (child.parents.every((p) => p.isCompleted) || child.canEmitNoMore());
277
276
 
277
+ const hasCircularDependencyFrom = (
278
+ node: Observable<unknown>,
279
+ mut_visited: MutableSet<Observable<unknown>>,
280
+ mut_inPath: MutableSet<Observable<unknown>>,
281
+ ): boolean => {
282
+ if (mut_inPath.has(node)) {
283
+ return true;
284
+ }
285
+
286
+ if (mut_visited.has(node)) {
287
+ return false;
288
+ }
289
+
290
+ mut_visited.add(node);
291
+
292
+ mut_inPath.add(node);
293
+
294
+ if (node.kind !== 'root') {
295
+ for (const parent of node.parents) {
296
+ if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
297
+ return true;
298
+ }
299
+ }
300
+ }
301
+
302
+ mut_inPath.delete(node);
303
+
304
+ return false;
305
+ };
306
+
307
+ /**
308
+ * Detects circular dependencies by walking the full ancestor chain of the
309
+ * child's parents and checking whether the child already appears among them.
310
+ *
311
+ * @throws {Error} if a circular dependency is detected
312
+ */
313
+ const detectCircularDependency = (child: ChildNode): void => {
314
+ const mut_visited = new Set<Observable<unknown>>();
315
+
316
+ const mut_inPath = new Set<Observable<unknown>>([
317
+ // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
318
+ child as Observable<unknown>,
319
+ ]);
320
+
321
+ for (const parent of child.parents) {
322
+ if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
323
+ // A programming error (invariant violation), not a recoverable failure.
324
+ // `panic` marks the error it throws, so the `Result` / `AsyncResult`
325
+ // boundaries rethrow it rather than turning an invariant violation into
326
+ // an `Err` a caller might handle (Sumi D-48 / D-53).
327
+ panic(
328
+ 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
329
+ );
330
+ }
331
+ }
332
+ };
333
+
278
334
  /** Registers a child to its parents. */
279
335
  const registerChild = (child: ChildNode): void => {
280
336
  if (checksCircularDependency) {
@@ -287,9 +343,9 @@ const registerChild = (child: ChildNode): void => {
287
343
  };
288
344
 
289
345
  /**
290
- * Undoes {@link registerChild} for a completed child: its parents forget it,
291
- * so that no update puts it in its queue again. Without this a completed
292
- * node stayed reachable for as long as its root lived (#2134).
346
+ * Undoes {@link registerChild} for a completed child: its parents forget it, so
347
+ * that no update puts it in its queue again. Without this a completed node
348
+ * stayed reachable for as long as its root lived (#2134).
293
349
  */
294
350
  const unregisterChild = (child: ChildNode): void => {
295
351
  for (const p of child.parents) {
@@ -320,60 +376,3 @@ const isDevelopmentProcess = (): boolean => {
320
376
  * bundle).
321
377
  */
322
378
  export const checksCircularDependency: boolean = isDevelopmentProcess();
323
-
324
- /**
325
- * Detects circular dependencies by walking the full ancestor chain of the
326
- * child's parents and checking whether the child already appears among them.
327
- *
328
- * @throws {Error} if a circular dependency is detected
329
- */
330
- const detectCircularDependency = (child: ChildNode): void => {
331
- const mut_visited = new Set<Observable<unknown>>();
332
-
333
- const mut_inPath = new Set<Observable<unknown>>([
334
- // @sumi-expect-error banned-syntax/no-unsafe-type-assertion
335
- child as Observable<unknown>,
336
- ]);
337
-
338
- for (const parent of child.parents) {
339
- if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
340
- // A programming error (invariant violation), not a recoverable
341
- // failure. `panic` marks the error it throws, so the `Result` /
342
- // `AsyncResult` boundaries rethrow it rather than turning an invariant
343
- // violation into an `Err` a caller might handle (Sumi D-48 / D-53).
344
- panic(
345
- 'Circular dependency detected in observable graph: a child observable cannot be its own ancestor.',
346
- );
347
- }
348
- }
349
- };
350
-
351
- const hasCircularDependencyFrom = (
352
- node: Observable<unknown>,
353
- mut_visited: MutableSet<Observable<unknown>>,
354
- mut_inPath: MutableSet<Observable<unknown>>,
355
- ): boolean => {
356
- if (mut_inPath.has(node)) {
357
- return true;
358
- }
359
-
360
- if (mut_visited.has(node)) {
361
- return false;
362
- }
363
-
364
- mut_visited.add(node);
365
-
366
- mut_inPath.add(node);
367
-
368
- if (node.kind !== 'root') {
369
- for (const parent of node.parents) {
370
- if (hasCircularDependencyFrom(parent, mut_visited, mut_inPath)) {
371
- return true;
372
- }
373
- }
374
- }
375
-
376
- mut_inPath.delete(node);
377
-
378
- return false;
379
- };
@@ -49,8 +49,8 @@ describe('circular dependency comparison', () => {
49
49
  );
50
50
 
51
51
  // Simulate a cycle: mut_childA → childB → mut_childA
52
- // In normal usage this is impossible — the check runs in every
53
- // child constructor before the reference becomes available.
52
+ // In normal usage this is impossible — the check runs in every child
53
+ // constructor before the reference becomes available.
54
54
  Object.defineProperty(mut_childA, 'parents', {
55
55
  value: [childB],
56
56
  writable: false,
@@ -31,17 +31,16 @@ export type SyncChildObservableTools<A> = Readonly<{
31
31
  complete: () => void;
32
32
 
33
33
  /**
34
- * Completes this observable once the update now propagating has reached
35
- * every descendant, so that they see the value just passed on first.
34
+ * Completes this observable once the update now propagating has reached every
35
+ * descendant, so that they see the value just passed on first.
36
36
  */
37
37
  completeAfterPropagation: () => void;
38
38
 
39
39
  /**
40
40
  * Registers a teardown (clear timers, unsubscribe inner observables, …) to
41
- * run when this observable completes or is disposed, in the order
42
- * registered. Called from `init`, it lets the teardown close over what
43
- * `init` creates. Registered once the observable has completed, the
44
- * teardown runs at once.
41
+ * run when this observable completes or is disposed, in the order registered.
42
+ * Called from `init`, it lets the teardown close over what `init` creates.
43
+ * Registered once the observable has completed, the teardown runs at once.
45
44
  */
46
45
  onComplete: (teardown: () => void) => void;
47
46
  }>;
@@ -57,21 +56,20 @@ export type AsyncChildObservableTools<A> = Readonly<{
57
56
  SyncChildObservableTools<A>;
58
57
 
59
58
  /**
60
- * What a sync child's `init` may return instead of `tryUpdate` alone: the
61
- * hooks the factory calls, which read the state `init` creates.
59
+ * What a sync child's `init` may return instead of `tryUpdate` alone: the hooks
60
+ * the factory calls, which read the state `init` creates.
62
61
  */
63
62
  export type SyncChildObservableHooks = Readonly<{
64
63
  /**
65
- * Called with the token of every update in which a parent of this
66
- * observable emitted.
64
+ * Called with the token of every update in which a parent of this observable
65
+ * emitted.
67
66
  */
68
67
  tryUpdate: (updateToken: UpdateToken) => void;
69
68
 
70
69
  /**
71
- * Whether this observable can emit nothing more although some parent is
72
- * still live, asked whenever a parent completes. `true` completes it then,
73
- * as all parents completing does (`zip` once a completed source's queue is
74
- * empty).
70
+ * Whether this observable can emit nothing more although some parent is still
71
+ * live, asked whenever a parent completes. `true` completes it then, as all
72
+ * parents completing does (`zip` once a completed source's queue is empty).
75
73
  */
76
74
  canEmitNoMore?: () => boolean;
77
75
  }>;
@@ -84,8 +82,8 @@ export type SyncChildObservableHooks = Readonly<{
84
82
  export type AsyncChildObservableHooks = SyncChildObservableHooks &
85
83
  Readonly<{
86
84
  /**
87
- * Run at the beginning of `complete()`, before the teardowns, and never
88
- * by `dispose()`: passes on what this observable still holds through
85
+ * Run at the beginning of `complete()`, before the teardowns, and never by
86
+ * `dispose()`: passes on what this observable still holds through
89
87
  * `startUpdate`, so that its subscribers see it before they see the
90
88
  * completion. A disposed observable has no one left to tell.
91
89
  */
@@ -96,11 +94,11 @@ type ChildObservableConfigBase<P extends NonEmptyUnknownList> = Readonly<{
96
94
  parents: Wrap<P>;
97
95
 
98
96
  /**
99
- * Defaults to one more than the deepest parent. An update reaches
100
- * observables in ascending depth, so an observable `tryUpdate` reads with
101
- * `getSnapshot` without being its child (`withCurrentValueFrom`'s second
102
- * source) has to be shallower than this one too, or this one reads its value
103
- * from before the update: pass `1 + Math.max(...)` over both then.
97
+ * Defaults to one more than the deepest parent. An update reaches observables
98
+ * in ascending depth, so an observable `tryUpdate` reads with `getSnapshot`
99
+ * without being its child (`withCurrentValueFrom`'s second source) has to be
100
+ * shallower than this one too, or this one reads its value from before the
101
+ * update: pass `1 + Math.max(...)` over both then.
104
102
  */
105
103
  depth?: number;
106
104
  }>;
@@ -122,15 +120,15 @@ export type InitializedChildObservableConfig<
122
120
  }>;
123
121
 
124
122
  /**
125
- * Creates a sync-child observable: one derived from `parents` that emits, if
126
- * at all, in the same update as they do. `map`, `filter`, `combine` and the
127
- * other synchronous operators are built with it.
123
+ * Creates a sync-child observable: one derived from `parents` that emits, if at
124
+ * all, in the same update as they do. `map`, `filter`, `combine` and the other
125
+ * synchronous operators are built with it.
128
126
  *
129
127
  * `init` runs during construction and receives the tools. It returns
130
128
  * `tryUpdate`, which is called with the token of every update in which some
131
- * parent of this observable emitted — a parent that did not emit in that
132
- * update has a different `updateToken`; to emit, call `setNext` with the
133
- * token received — or an object holding `tryUpdate` and the other hooks
129
+ * parent of this observable emitted — a parent that did not emit in that update
130
+ * has a different `updateToken`; to emit, call `setNext` with the token
131
+ * received — or an object holding `tryUpdate` and the other hooks
134
132
  * ({@link SyncChildObservableHooks}). State the hooks share can live inside
135
133
  * `init`, next to the teardown it registers with the `onComplete` tool.
136
134
  * `init` may complete the observable; it is then never registered to its
@@ -211,8 +209,8 @@ export const createInitializedSyncChildObservable = <
211
209
  * Creates an async-child observable: one derived from `parents` that may emit
212
210
  * later, in an update of its own, through `startUpdate` (`debounce`,
213
211
  * `switchMap`). It may also emit synchronously with `setNext`, as a sync child
214
- * does. See {@link createSyncChildObservable} for the `init` contract; an
215
- * async child's `init` may also return a `flush` hook
212
+ * does. See {@link createSyncChildObservable} for the `init` contract; an async
213
+ * child's `init` may also return a `flush` hook
216
214
  * ({@link AsyncChildObservableHooks}).
217
215
  */
218
216
  export const createAsyncChildObservable = <A, P extends NonEmptyUnknownList>(
@@ -8,8 +8,8 @@ import type { RootObservable } from '../types/index.mjs';
8
8
  import { RootObservableClass } from './root-observable-class.mjs';
9
9
 
10
10
  /**
11
- * The tools a custom root's `init` receives: the `protected` methods a
12
- * subclass of `RootObservableClass` inherits.
11
+ * The tools a custom root's `init` receives: the `protected` methods a subclass
12
+ * of `RootObservableClass` inherits.
13
13
  */
14
14
  export type RootObservableTools<A> = Readonly<{
15
15
  /** Emits `nextValue` and propagates the update to all descendants. */
@@ -23,9 +23,9 @@ export type RootObservableTools<A> = Readonly<{
23
23
  complete: () => void;
24
24
 
25
25
  /**
26
- * Registers a teardown (clear timers, abort requests, remove listeners, …)
27
- * to run at the beginning of `complete()`, in the order registered. Called
28
- * from `init`, it lets the teardown close over what `init` creates.
26
+ * Registers a teardown (clear timers, abort requests, remove listeners, …) to
27
+ * run at the beginning of `complete()`, in the order registered. Called from
28
+ * `init`, it lets the teardown close over what `init` creates.
29
29
  * Registered once the observable has completed — `init` may complete it
30
30
  * synchronously first — the teardown runs at once.
31
31
  */
@@ -37,13 +37,13 @@ export type RootObservableConfig<A> = Readonly<{
37
37
  }>;
38
38
 
39
39
  /**
40
- * Creates a root observable, for a custom creator written without a class
41
- * of its own.
40
+ * Creates a root observable, for a custom creator written without a class of
41
+ * its own.
42
42
  *
43
- * `init` runs during construction, receives the tools, and returns extra
44
- * public members to merge into the observable (e.g. `next` for a source,
45
- * `start` for a timer). It may register asynchronous callbacks that use the
46
- * tools, and may call `complete` synchronously.
43
+ * `init` runs during construction, receives the tools, and returns extra public
44
+ * members to merge into the observable (e.g. `next` for a source, `start` for a
45
+ * timer). It may register asynchronous callbacks that use the tools, and may
46
+ * call `complete` synchronously.
47
47
  */
48
48
  export const createRootObservable = <
49
49
  A,
@@ -12,12 +12,12 @@ import {
12
12
  } from './propagation-state.mjs';
13
13
 
14
14
  /**
15
- * The base of the observables that start updates of their own: a root, and
16
- * an async child (`debounce`, `switchMap`), which emits later than its
17
- * parents do. Its update visits, shallowest first, the nodes its value
18
- * reaches: each node that emits puts its children in the update's queue
19
- * (`propagation-state.mts`), so that each sees its parents' values of that
20
- * update (no glitch) and a node no value reached is not visited.
15
+ * The base of the observables that start updates of their own: a root, and an
16
+ * async child (`debounce`, `switchMap`), which emits later than its parents do.
17
+ * Its update visits, shallowest first, the nodes its value reaches: each node
18
+ * that emits puts its children in the update's queue (`propagation-state.mts`),
19
+ * so that each sees its parents' values of that update (no glitch) and a node
20
+ * no value reached is not visited.
21
21
  */
22
22
  export class ManagerObservableClass<
23
23
  A,
@@ -26,13 +26,20 @@ export class ManagerObservableClass<
26
26
  > extends ObservableBaseClass<A, Kind, Depth> {
27
27
  /** Emits `nextValue` and propagates it to the descendants. */
28
28
  protected startUpdate(nextValue: A): void {
29
+ // Nothing to propagate to (a branch no `switchMap` follows, a state written
30
+ // before anything subscribes): the update is the value alone, and opening
31
+ // one would cost more than storing it.
32
+ if (this.setNextIfUnobserved(nextValue)) {
33
+ return;
34
+ }
35
+
29
36
  const updateToken = openUpdate(this.depth);
30
37
 
31
38
  beginPropagation(updateToken);
32
39
 
33
- // A subscriber or projection may throw, and the depth has to come back
34
- // down regardless, or every later write would warn. The exception itself
35
- // is not caught: it reaches the caller of `next` as it always has.
40
+ // A subscriber or projection may throw, and the depth has to come back down
41
+ // regardless, or every later write would warn. The exception itself is not
42
+ // caught: it reaches the caller of `next` as it always has.
36
43
  // @sumi-expect-error exceptions/no-try
37
44
  try {
38
45
  this.setNext(nextValue, updateToken);