@jscutlery/operators 1.0.2 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
4
 
5
+ ## [1.1.1](https://github.com/jscutlery/devkit/compare/operators-1.1.0...operators-1.1.1) (2023-04-18)
6
+
7
+
8
+
9
+ # [1.1.0](https://github.com/jscutlery/devkit/compare/operators-1.0.2...operators-1.1.0) (2023-04-18)
10
+
11
+
12
+ ### Features
13
+
14
+ * **operators:** add suspensify's strict mode ([f9dd789](https://github.com/jscutlery/devkit/commit/f9dd789d82cdff2fffb2241e8b0aab9bd76cb363)), closes [#237](https://github.com/jscutlery/devkit/issues/237)
15
+
16
+
17
+
5
18
  ## [1.0.2](https://github.com/jscutlery/devkit/compare/operators-1.0.1...operators-1.0.2) (2023-04-18)
6
19
 
7
20
 
package/README.md CHANGED
@@ -7,7 +7,8 @@ This package regroups a couple of RxJS operators meant to simplify some common p
7
7
  - [`suspensify`](#suspensify)
8
8
  - [Goals](#goals)
9
9
  - [Usage](#usage)
10
- - [Basic](#basic)
10
+ - [Strict mode (will become default in 3.0.0)](#strict-mode-will-become-default-in-300)
11
+ - [Lax mode (default)](#lax-mode-default)
11
12
  - [With Angular](#with-angular)
12
13
  - [With `@rx-angular/state`](#with-rx-angularstate)
13
14
  - [Alternatives](#alternatives)
@@ -32,7 +33,7 @@ When dealing with an asynchronous source of data in a web application, it is a c
32
33
  - **pending**: the data is being fetched
33
34
  - **error**: an error occurred while fetching the data
34
35
  - **success**: some data has been fetched
35
- - **finalized**: there is no more data to fetch or an error happened
36
+ - **finalized**: there is no more data to fetch or an error happened _(it is called `finalized` and not `completed` to avoid confusion with RxJS `complete` event which only happens if there is no error)_.
36
37
 
37
38
  The most common ways of implementing this are error-prone as they are either based on a complex combination of native RxJS operators or side effects that break the reactivity chain.
38
39
 
@@ -47,7 +48,52 @@ The `suspensify` operator is meant to provide a simple and efficient way of deal
47
48
 
48
49
  ## Usage
49
50
 
50
- ### Basic
51
+ ### Strict mode (will become default in 3.0.0)
52
+
53
+ Thanks to strict mode the emitted suspense can be narrowed down.
54
+
55
+ ```ts
56
+ interval(1000)
57
+ .pipe(take(2), suspensify({strict: true}))
58
+ .subscribe((suspense) => {
59
+ suspense.value; // 💥
60
+ suspense.error; // 💥
61
+ if (suspense.hasValue) {
62
+ suspense.value; // ✅
63
+ suspense.error; // 💥
64
+ }
65
+ if (suspense.hasError) {
66
+ suspense.value; // 💥
67
+ suspense.error; // ✅
68
+ }
69
+ });
70
+ ```
71
+
72
+ ```ts
73
+ {
74
+ finalized: false,
75
+ hasError: false,
76
+ hasValue: false,
77
+ pending: true,
78
+ }
79
+ {
80
+ finalized: false,
81
+ hasError: false,
82
+ hasValue: false,
83
+ pending: false,
84
+ value: 0,
85
+ }
86
+ ...
87
+ {
88
+ finalized: true,
89
+ hasError: false,
90
+ hasValue: true,
91
+ pending: false,
92
+ value: 1,
93
+ }
94
+ ```
95
+
96
+ ### Lax mode (default)
51
97
 
52
98
  ```ts
53
99
  interval(1000)
@@ -57,23 +103,29 @@ interval(1000)
57
103
 
58
104
  ```ts
59
105
  {
106
+ finalized: false,
107
+ hasError: false,
108
+ hasValue: false,
109
+ pending: true,
60
110
  value: undefined,
61
111
  error: undefined,
62
- pending: true,
63
- finalized: false,
64
112
  }
65
113
  {
114
+ finalized: false,
115
+ hasError: false,
116
+ hasValue: false,
117
+ pending: false,
66
118
  value: 0,
67
119
  error: undefined,
68
- pending: false,
69
- finalized: false,
70
120
  }
71
121
  ...
72
122
  {
123
+ finalized: true,
124
+ hasError: false,
125
+ hasValue: true,
126
+ pending: false,
73
127
  value: 1,
74
128
  error: undefined,
75
- pending: false,
76
- finalized: true,
77
129
  }
78
130
  ```
79
131
 
@@ -86,9 +138,15 @@ interval(1000)
86
138
 
87
139
  <my-spinner *ngIf="suspense.pending"></my-spinner>
88
140
 
89
- <div *ngIf="suspense.error">{{ suspense.error }}</div>
141
+ <div *ngIf="suspense.hasError">
142
+ {{ suspense.error }} // ✅
143
+ {{ suspense.value }} // 💥 will not compile in strict mode
144
+ </div>
90
145
 
91
- <div *ngIf="suspense.value">{{ suspense.value }}</div>
146
+ <div *ngIf="suspense.hasValue">
147
+ {{ suspense.error }} // 💥 will not compile in strict mode
148
+ {{ suspense.value }} // ✅
149
+ </div>
92
150
 
93
151
  </ng-container>
94
152
  `,
package/index.cjs CHANGED
@@ -1076,13 +1076,20 @@ $({ target: 'Object', stat: true, arity: 2, forced: Object.assign !== assign },
1076
1076
  assign: assign
1077
1077
  });
1078
1078
 
1079
- function suspensify(
1080
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1081
- options) {
1079
+ function suspensify({
1080
+ strict = false
1081
+ } = {}) {
1082
1082
  return source$ => {
1083
- return source$.pipe(_suspensify(), _coalesceFirstEmittedValue());
1083
+ const strictSuspense$ = source$.pipe(_suspensify(), _coalesceFirstEmittedValue());
1084
+ return strict ? strictSuspense$ : strictSuspense$.pipe(operators.map(strictSuspense => Object.assign({
1085
+ error: undefined,
1086
+ value: undefined
1087
+ }, strictSuspense))
1088
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1089
+ );
1084
1090
  };
1085
1091
  }
1092
+
1086
1093
  function _coalesceFirstEmittedValue() {
1087
1094
  return source$ => {
1088
1095
  return new rxjs.Observable(observer => {
@@ -1096,31 +1103,44 @@ function _coalesceFirstEmittedValue() {
1096
1103
  });
1097
1104
  };
1098
1105
  }
1106
+ /* Use values as types for better type checking. */
1107
+ const TRUE = true;
1108
+ const FALSE = false;
1099
1109
  function _suspensify() {
1100
1110
  return source$ => {
1101
1111
  const initialState = {
1102
- value: undefined,
1103
- error: undefined,
1104
1112
  finalized: false,
1113
+ hasError: false,
1114
+ hasValue: false,
1105
1115
  pending: true
1106
1116
  };
1107
1117
  return source$.pipe(operators.materialize(), operators.scan((state = initialState, notification) => {
1108
- /* On complete, merge `finalized: true & pending: false`
1109
- * with the current state. */
1110
- if (notification.kind === 'C') {
1111
- return Object.assign({}, state, {
1112
- finalized: true,
1113
- pending: false
1114
- });
1118
+ switch (notification.kind) {
1119
+ /* Value. */
1120
+ case 'N':
1121
+ return {
1122
+ finalized: FALSE,
1123
+ hasError: FALSE,
1124
+ hasValue: TRUE,
1125
+ value: notification.value,
1126
+ pending: FALSE
1127
+ };
1128
+ /* Error. */
1129
+ case 'E':
1130
+ return {
1131
+ finalized: TRUE,
1132
+ hasError: TRUE,
1133
+ hasValue: FALSE,
1134
+ pending: FALSE,
1135
+ error: notification.error
1136
+ };
1137
+ /* Complete. */
1138
+ case 'C':
1139
+ return Object.assign({}, state, {
1140
+ finalized: TRUE,
1141
+ pending: FALSE
1142
+ });
1115
1143
  }
1116
- /* Mark as finalized on error as complete is only triggered on success. */
1117
- const finalized = notification.kind === 'E';
1118
- return {
1119
- value: notification.value,
1120
- error: notification.error,
1121
- finalized,
1122
- pending: false
1123
- };
1124
1144
  }, initialState), operators.startWith(initialState));
1125
1145
  };
1126
1146
  }
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Observable, ReplaySubject } from 'rxjs';
2
- import { debounce, materialize, scan, startWith } from 'rxjs/operators';
2
+ import { map, debounce, materialize, scan, startWith } from 'rxjs/operators';
3
3
 
4
4
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
5
5
 
@@ -1072,13 +1072,20 @@ $({ target: 'Object', stat: true, arity: 2, forced: Object.assign !== assign },
1072
1072
  assign: assign
1073
1073
  });
1074
1074
 
1075
- function suspensify(
1076
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1077
- options) {
1075
+ function suspensify({
1076
+ strict = false
1077
+ } = {}) {
1078
1078
  return source$ => {
1079
- return source$.pipe(_suspensify(), _coalesceFirstEmittedValue());
1079
+ const strictSuspense$ = source$.pipe(_suspensify(), _coalesceFirstEmittedValue());
1080
+ return strict ? strictSuspense$ : strictSuspense$.pipe(map(strictSuspense => Object.assign({
1081
+ error: undefined,
1082
+ value: undefined
1083
+ }, strictSuspense))
1084
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1085
+ );
1080
1086
  };
1081
1087
  }
1088
+
1082
1089
  function _coalesceFirstEmittedValue() {
1083
1090
  return source$ => {
1084
1091
  return new Observable(observer => {
@@ -1092,31 +1099,44 @@ function _coalesceFirstEmittedValue() {
1092
1099
  });
1093
1100
  };
1094
1101
  }
1102
+ /* Use values as types for better type checking. */
1103
+ const TRUE = true;
1104
+ const FALSE = false;
1095
1105
  function _suspensify() {
1096
1106
  return source$ => {
1097
1107
  const initialState = {
1098
- value: undefined,
1099
- error: undefined,
1100
1108
  finalized: false,
1109
+ hasError: false,
1110
+ hasValue: false,
1101
1111
  pending: true
1102
1112
  };
1103
1113
  return source$.pipe(materialize(), scan((state = initialState, notification) => {
1104
- /* On complete, merge `finalized: true & pending: false`
1105
- * with the current state. */
1106
- if (notification.kind === 'C') {
1107
- return Object.assign({}, state, {
1108
- finalized: true,
1109
- pending: false
1110
- });
1114
+ switch (notification.kind) {
1115
+ /* Value. */
1116
+ case 'N':
1117
+ return {
1118
+ finalized: FALSE,
1119
+ hasError: FALSE,
1120
+ hasValue: TRUE,
1121
+ value: notification.value,
1122
+ pending: FALSE
1123
+ };
1124
+ /* Error. */
1125
+ case 'E':
1126
+ return {
1127
+ finalized: TRUE,
1128
+ hasError: TRUE,
1129
+ hasValue: FALSE,
1130
+ pending: FALSE,
1131
+ error: notification.error
1132
+ };
1133
+ /* Complete. */
1134
+ case 'C':
1135
+ return Object.assign({}, state, {
1136
+ finalized: TRUE,
1137
+ pending: FALSE
1138
+ });
1111
1139
  }
1112
- /* Mark as finalized on error as complete is only triggered on success. */
1113
- const finalized = notification.kind === 'E';
1114
- return {
1115
- value: notification.value,
1116
- error: notification.error,
1117
- finalized,
1118
- pending: false
1119
- };
1120
1140
  }, initialState), startWith(initialState));
1121
1141
  };
1122
1142
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jscutlery/operators",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "repository": {
5
5
  "url": "https://github.com/jscutlery/devkit"
6
6
  },
@@ -1,9 +1,11 @@
1
1
  import { OperatorFunction } from 'rxjs';
2
2
  export interface SuspenseLax<T> {
3
- value: undefined | T;
4
- error: undefined | any;
5
3
  finalized: boolean;
4
+ hasError: boolean;
5
+ hasValue: boolean;
6
6
  pending: boolean;
7
+ error: undefined | any;
8
+ value: undefined | T;
7
9
  }
8
10
  export interface SuspensePending {
9
11
  finalized: false;
@@ -25,15 +27,18 @@ export interface SuspenseWithError {
25
27
  pending: false;
26
28
  error: unknown;
27
29
  }
28
- export declare type SuspenseStrict<T> = SuspensePending | SuspenseWithValue<T> | SuspenseWithError;
30
+ export interface SuspenseEmpty {
31
+ finalized: true;
32
+ hasError: false;
33
+ hasValue: false;
34
+ pending: false;
35
+ }
36
+ export declare type SuspenseStrict<T> = SuspensePending | SuspenseWithValue<T> | SuspenseWithError | SuspenseEmpty;
29
37
  /**
30
38
  * @todo 3.0.0: Make `Suspense` an alias for `SuspenseStrict`.
31
39
  */
32
40
  export declare type Suspense<T> = SuspenseLax<T>;
33
41
  export interface SuspensifyOptions {
34
- /**
35
- * @deprecated 🚧 Work in progress.
36
- */
37
42
  strict?: boolean;
38
43
  }
39
44
  /**