Skip to content

Commit 441332e

Browse files
authored
Added COUNTER mode for saga injectors (#24)
1 parent 1785cbd commit 441332e

File tree

5 files changed

+129
-12
lines changed

5 files changed

+129
-12
lines changed

docs/api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,10 @@ An enum of all the possible saga injection behaviours
203203
- `DAEMON` **[String][26]** Causes the saga to be started on component instantiation and never canceled
204204
or started again.
205205
- `ONCE_TILL_UNMOUNT` **[String][26]** Behaves like 'RESTART_ON_REMOUNT' but never runs it again.
206+
- `COUNTER` **[String][26]** The saga will be mounted similar to 'RESTART_ON_REMOUNT', only difference is that
207+
saga will be mounted only once on first inject, and ejected when all injectors are unmounted.
208+
So this enables you to have multiple injectors with same saga and key, only one instance of saga will run
209+
and enables you to have system that are more similar to widgets
206210

207211
[1]: #setup
208212

index.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,18 @@ export function createInjectorsEnhancer(options: {
6565
* @property {String} DAEMON Causes the saga to be started on component instantiation and never canceled
6666
* or started again.
6767
* @property {String} ONCE_TILL_UNMOUNT Behaves like 'RESTART_ON_REMOUNT' but never runs it again.
68+
* @property {String} COUNTER Similar to 'RESTART_ON_REMOUNT' except the
69+
* saga will be mounted only once on first inject and ejected when all injectors are unmounted.
70+
* This enables you to have multiple injectors with the same saga and key and only one instance of the saga will run.
6871
*
6972
* @enum
7073
* @public
7174
*/
7275
export enum SagaInjectionModes {
7376
RESTART_ON_REMOUNT = "@@saga-injector/restart-on-remount",
7477
DAEMON = "@@saga-injector/daemon",
75-
ONCE_TILL_UNMOUNT = "@@saga-injector/once-till-unmount"
78+
ONCE_TILL_UNMOUNT = "@@saga-injector/once-till-unmount",
79+
COUNTER = "@@saga-injector/counter"
7680
}
7781

7882
/**

src/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount';
22
export const DAEMON = '@@saga-injector/daemon';
33
export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount';
4+
export const COUNTER = '@@saga-injector/counter';
45

56
/**
67
* An enum of all the possible saga injection behaviours
@@ -10,6 +11,9 @@ export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount';
1011
* @property {String} DAEMON Causes the saga to be started on component instantiation and never canceled
1112
* or started again.
1213
* @property {String} ONCE_TILL_UNMOUNT Behaves like 'RESTART_ON_REMOUNT' but never runs it again.
14+
* @property {String} COUNTER Similar to 'RESTART_ON_REMOUNT' except the
15+
* saga will be mounted only once on first inject and ejected when all injectors are unmounted.
16+
* This enables you to have multiple injectors with the same saga and key and only one instance of the saga will run.
1317
*
1418
* @enum
1519
* @public
@@ -18,4 +22,5 @@ export const SagaInjectionModes = {
1822
RESTART_ON_REMOUNT,
1923
DAEMON,
2024
ONCE_TILL_UNMOUNT,
25+
COUNTER,
2126
};

src/sagaInjectors.js

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ import invariant from 'invariant';
22
import isEmpty from 'lodash/isEmpty';
33
import isFunction from 'lodash/isFunction';
44
import isString from 'lodash/isString';
5+
import isNumber from 'lodash/isNumber';
56
import conformsTo from 'lodash/conformsTo';
67

78
import checkStore from './checkStore';
8-
import { DAEMON, ONCE_TILL_UNMOUNT, RESTART_ON_REMOUNT } from './constants';
9+
import {
10+
DAEMON,
11+
ONCE_TILL_UNMOUNT,
12+
RESTART_ON_REMOUNT,
13+
COUNTER,
14+
} from './constants';
915

10-
const allowedModes = [RESTART_ON_REMOUNT, DAEMON, ONCE_TILL_UNMOUNT];
16+
const allowedModes = [RESTART_ON_REMOUNT, DAEMON, ONCE_TILL_UNMOUNT, COUNTER];
1117

1218
const checkKey = key =>
1319
invariant(
@@ -19,6 +25,7 @@ const checkDescriptor = descriptor => {
1925
const shape = {
2026
saga: isFunction,
2127
mode: mode => isString(mode) && allowedModes.includes(mode),
28+
count: isNumber,
2229
};
2330
invariant(
2431
conformsTo(descriptor, shape),
@@ -33,6 +40,7 @@ export function injectSagaFactory(store, isValid) {
3340
const newDescriptor = {
3441
...descriptor,
3542
mode: descriptor.mode || DAEMON,
43+
count: 0,
3644
};
3745
const { saga, mode } = newDescriptor;
3846

@@ -50,16 +58,33 @@ export function injectSagaFactory(store, isValid) {
5058
}
5159
}
5260

61+
if (mode === COUNTER) {
62+
// COUNTER must be added if saga is done
63+
if (store.injectedSagas[key] === 'done') hasSaga = false;
64+
let oldCounterValue = 0;
65+
if (hasSaga) {
66+
oldCounterValue = Math.max(store.injectedSagas[key].count || 0, 0);
67+
}
68+
newDescriptor.count = oldCounterValue + 1;
69+
}
70+
5371
if (
5472
!hasSaga ||
55-
(hasSaga && mode !== DAEMON && mode !== ONCE_TILL_UNMOUNT)
73+
(hasSaga &&
74+
mode !== DAEMON &&
75+
mode !== ONCE_TILL_UNMOUNT &&
76+
mode !== COUNTER)
5677
) {
5778
/* eslint-disable no-param-reassign */
5879
store.injectedSagas[key] = {
5980
...newDescriptor,
6081
task: store.runSaga(saga),
6182
};
6283
/* eslint-enable no-param-reassign */
84+
} else if (hasSaga && mode === COUNTER) {
85+
// increment num of sagas that wants to be injected
86+
/* eslint-disable no-param-reassign */
87+
store.injectedSagas[key].count = newDescriptor.count;
6388
}
6489
};
6590
}
@@ -72,12 +97,22 @@ export function ejectSagaFactory(store, isValid) {
7297

7398
if (Reflect.has(store.injectedSagas, key)) {
7499
const descriptor = store.injectedSagas[key];
75-
if (descriptor.mode && descriptor.mode !== DAEMON) {
76-
descriptor.task.cancel();
77-
// Clean up in production; in development we need `descriptor.saga` for hot reloading
78-
if (process.env.NODE_ENV === 'production') {
79-
// Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga`
80-
store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign
100+
101+
if (descriptor.mode) {
102+
if (descriptor.mode === COUNTER) {
103+
descriptor.count -= 1;
104+
105+
// don't cancel task if still not 0
106+
if (descriptor.count > 0) return;
107+
}
108+
109+
if (descriptor.mode !== DAEMON) {
110+
descriptor.task.cancel();
111+
// Clean up in production; in development we need `descriptor.saga` for hot reloading
112+
if (process.env.NODE_ENV === 'production') {
113+
// Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga`
114+
store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign
115+
}
81116
}
82117
}
83118
}

src/tests/sagaInjectors.test.js

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import getInjectors, {
1010
injectSagaFactory,
1111
ejectSagaFactory,
1212
} from '../sagaInjectors';
13-
import { DAEMON, ONCE_TILL_UNMOUNT, RESTART_ON_REMOUNT } from '../constants';
13+
import {
14+
COUNTER,
15+
DAEMON,
16+
ONCE_TILL_UNMOUNT,
17+
RESTART_ON_REMOUNT,
18+
} from '../constants';
1419
import { createInjectorsEnhancer } from '../createInjectorsEnhancer';
1520

1621
function configureStore() {
@@ -174,6 +179,9 @@ describe('injectors', () => {
174179
expect(() =>
175180
injectSaga('test', { saga: testSaga, mode: ONCE_TILL_UNMOUNT }),
176181
).not.toThrow();
182+
expect(() =>
183+
injectSaga('test', { saga: testSaga, mode: COUNTER }),
184+
).not.toThrow();
177185
});
178186

179187
it('should not start daemon and once-till-unmount sagas if were started before', () => {
@@ -183,8 +191,11 @@ describe('injectors', () => {
183191
injectSaga('test1', { saga: testSaga, mode: DAEMON });
184192
injectSaga('test2', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
185193
injectSaga('test2', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
194+
injectSaga('test3', { saga: testSaga, mode: COUNTER });
195+
injectSaga('test3', { saga: testSaga, mode: COUNTER });
196+
injectSaga('test3', { saga: testSaga, mode: COUNTER });
186197

187-
expect(store.runSaga).toHaveBeenCalledTimes(2);
198+
expect(store.runSaga).toHaveBeenCalledTimes(3);
188199
});
189200

190201
it('should start any saga that was not started before', () => {
@@ -235,5 +246,63 @@ describe('injectors', () => {
235246
injectSaga('test', { saga: testSaga, foo: 'bar' });
236247
expect(store.injectedSagas.test.foo).toBe('bar');
237248
});
249+
250+
it('should have correctly counter value in injectedSagas for COUNTER mode', () => {
251+
function* testSaga1() {
252+
yield put({ type: 'TEST', payload: 'yup' });
253+
}
254+
255+
injectSaga('test', { saga: testSaga1, mode: COUNTER });
256+
expect(store.injectedSagas.test.count).toBe(1);
257+
258+
injectSaga('test', { saga: testSaga1, mode: COUNTER });
259+
expect(store.injectedSagas.test.count).toBe(2);
260+
261+
injectSaga('test', { saga: testSaga1, mode: COUNTER });
262+
ejectSaga('test');
263+
expect(store.injectedSagas.test.count).toBe(2);
264+
265+
ejectSaga('test');
266+
ejectSaga('test');
267+
expect(store.injectedSagas.test.count).toBe(0);
268+
});
269+
270+
it('should handle injection after ejecting all sagas', () => {
271+
function* testSaga1() {
272+
yield put({ type: 'TEST', payload: 'yup' });
273+
}
274+
275+
injectSaga('test', { saga: testSaga1, mode: COUNTER });
276+
injectSaga('test', { saga: testSaga1, mode: COUNTER });
277+
ejectSaga('test');
278+
ejectSaga('test');
279+
expect(store.injectedSagas.test.count).toBe(0);
280+
281+
injectSaga('test', { saga: testSaga1, mode: COUNTER });
282+
expect(store.injectedSagas.test.count).toBe(1);
283+
284+
ejectSaga('test');
285+
expect(store.injectedSagas.test.count).toBe(0);
286+
});
287+
288+
it('should not behave differently in production for COUNTER mode', () => {
289+
process.env.NODE_ENV = 'production';
290+
function* testSaga1() {
291+
yield put({ type: 'TEST', payload: 'yup' });
292+
}
293+
294+
injectSaga('test', { saga: testSaga1, mode: COUNTER });
295+
injectSaga('test', { saga: testSaga1, mode: COUNTER });
296+
ejectSaga('test');
297+
ejectSaga('test');
298+
expect(store.injectedSagas.test).toBe('done');
299+
300+
injectSaga('test', { saga: testSaga1, mode: COUNTER });
301+
expect(store.injectedSagas.test.count).toBe(1);
302+
303+
ejectSaga('test');
304+
expect(store.injectedSagas.test).toBe('done');
305+
process.env.NODE_ENV = originalNodeEnv;
306+
});
238307
});
239308
});

0 commit comments

Comments
 (0)