@tolgee/core 5.0.0-rc.9be0f0e.0 → 5.0.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 (96) hide show
  1. package/README.md +174 -0
  2. package/README.njk.md +61 -0
  3. package/dist/tolgee.cjs.js +723 -351
  4. package/dist/tolgee.cjs.js.map +1 -1
  5. package/dist/tolgee.cjs.min.js +1 -1
  6. package/dist/tolgee.cjs.min.js.map +1 -1
  7. package/dist/{tolgee.esm.mjs → tolgee.esm.js} +722 -346
  8. package/dist/tolgee.esm.js.map +1 -0
  9. package/dist/tolgee.esm.min.mjs +1 -1
  10. package/dist/tolgee.esm.min.mjs.map +1 -1
  11. package/dist/tolgee.umd.js +723 -351
  12. package/dist/tolgee.umd.js.map +1 -1
  13. package/dist/tolgee.umd.min.js +1 -1
  14. package/dist/tolgee.umd.min.js.map +1 -1
  15. package/lib/Controller/Cache/Cache.d.ts +10 -9
  16. package/lib/Controller/Controller.d.ts +104 -45
  17. package/lib/Controller/Events/EventEmitter.d.ts +6 -0
  18. package/lib/Controller/Events/EventEmitterSelective.d.ts +7 -0
  19. package/lib/Controller/Events/Events.d.ts +14 -0
  20. package/lib/Controller/Plugins/Plugins.d.ts +12 -25
  21. package/lib/Controller/State/State.d.ts +27 -9
  22. package/lib/Controller/State/initState.d.ts +46 -15
  23. package/lib/Controller/State/observerOptions.d.ts +41 -0
  24. package/lib/Controller/ValueObserver.d.ts +5 -5
  25. package/lib/FormatSimple/FormatError.d.ts +7 -0
  26. package/lib/FormatSimple/FormatSimple.d.ts +2 -0
  27. package/lib/FormatSimple/formatParser.d.ts +1 -0
  28. package/lib/FormatSimple/formatter.d.ts +2 -0
  29. package/lib/TolgeeCore.d.ts +204 -0
  30. package/lib/TranslateParams.d.ts +1 -1
  31. package/lib/helpers.d.ts +8 -0
  32. package/lib/index.d.ts +4 -4
  33. package/lib/types/cache.d.ts +25 -0
  34. package/lib/types/events.d.ts +66 -0
  35. package/lib/types/general.d.ts +34 -0
  36. package/lib/types/index.d.ts +7 -0
  37. package/lib/types/plugin.d.ts +127 -0
  38. package/package.json +5 -4
  39. package/src/Controller/Cache/Cache.ts +31 -31
  40. package/src/Controller/Cache/helpers.ts +6 -6
  41. package/src/Controller/Controller.ts +78 -50
  42. package/src/Controller/Events/EventEmitter.ts +34 -0
  43. package/src/Controller/Events/EventEmitterSelective.test.ts +110 -0
  44. package/src/Controller/Events/EventEmitterSelective.ts +132 -0
  45. package/src/Controller/Events/Events.ts +69 -0
  46. package/src/Controller/Plugins/Plugins.ts +182 -133
  47. package/src/Controller/State/State.ts +43 -26
  48. package/src/Controller/State/initState.ts +97 -25
  49. package/src/Controller/State/observerOptions.ts +66 -0
  50. package/src/Controller/ValueObserver.ts +5 -2
  51. package/src/FormatSimple/FormatError.ts +26 -0
  52. package/src/FormatSimple/FormatSimple.ts +13 -0
  53. package/src/FormatSimple/formatParser.ts +133 -0
  54. package/src/FormatSimple/formatter.test.ts +190 -0
  55. package/src/FormatSimple/formatter.ts +19 -0
  56. package/src/TolgeeCore.ts +267 -0
  57. package/src/TranslateParams.test.ts +9 -12
  58. package/src/TranslateParams.ts +6 -5
  59. package/src/__test/backend.test.ts +6 -6
  60. package/src/__test/cache.test.ts +190 -0
  61. package/src/__test/client.test.ts +2 -2
  62. package/src/__test/events.test.ts +32 -7
  63. package/src/__test/format.simple.test.ts +14 -0
  64. package/src/__test/formatError.test.ts +61 -0
  65. package/src/__test/initialization.test.ts +15 -3
  66. package/src/__test/languageDetection.test.ts +14 -8
  67. package/src/__test/languageStorage.test.ts +10 -11
  68. package/src/__test/languages.test.ts +30 -6
  69. package/src/__test/loading.test.ts +2 -2
  70. package/src/__test/{namespacesFallback.test.ts → namespaces.fallback.test.ts} +10 -8
  71. package/src/__test/namespaces.test.ts +30 -7
  72. package/src/__test/options.test.ts +64 -0
  73. package/src/__test/plugins.test.ts +29 -18
  74. package/src/helpers.ts +53 -0
  75. package/src/index.ts +4 -10
  76. package/src/types/cache.ts +37 -0
  77. package/src/types/events.ts +85 -0
  78. package/src/types/general.ts +50 -0
  79. package/src/types/index.ts +19 -0
  80. package/src/types/plugin.ts +181 -0
  81. package/dist/tolgee.esm.mjs.map +0 -1
  82. package/lib/Controller/State/helpers.d.ts +0 -6
  83. package/lib/Events/EventEmitter.d.ts +0 -6
  84. package/lib/Events/EventEmitterSelective.d.ts +0 -15
  85. package/lib/Events/Events.d.ts +0 -50
  86. package/lib/Tolgee.d.ts +0 -2
  87. package/lib/constants.d.ts +0 -5
  88. package/lib/types.d.ts +0 -274
  89. package/src/Controller/State/helpers.ts +0 -41
  90. package/src/Events/EventEmitter.ts +0 -27
  91. package/src/Events/EventEmitterSelective.test.ts +0 -108
  92. package/src/Events/EventEmitterSelective.ts +0 -160
  93. package/src/Events/Events.ts +0 -66
  94. package/src/Tolgee.ts +0 -77
  95. package/src/constants.ts +0 -7
  96. package/src/types.ts +0 -380
@@ -1,22 +1,26 @@
1
1
  import {
2
- CacheAsyncRequests,
3
2
  CacheDescriptor,
4
3
  CacheDescriptorInternal,
5
4
  CacheDescriptorWithKey,
6
- EventEmitterType,
7
- FallbackNSTranslation,
8
- Options,
5
+ NsFallback,
9
6
  TranslationsFlat,
10
7
  TranslationValue,
11
8
  TreeTranslationsData,
12
9
  BackendGetRecord,
13
10
  BackendGetDevRecord,
14
11
  } from '../../types';
15
- import { getFallbackArray } from '../State/helpers';
12
+ import { getFallbackArray, unique } from '../../helpers';
13
+ import { EventEmitterInstance } from '../Events/EventEmitter';
14
+ import { TolgeeStaticData } from '../State/initState';
16
15
  import { ValueObserverInstance } from '../ValueObserver';
17
16
 
18
17
  import { decodeCacheKey, encodeCacheKey, flattenTranslations } from './helpers';
19
18
 
19
+ type CacheAsyncRequests = Map<
20
+ string,
21
+ Promise<TreeTranslationsData | undefined> | undefined
22
+ >;
23
+
20
24
  type CacheRecord = {
21
25
  version: number;
22
26
  data: TranslationsFlat;
@@ -25,7 +29,7 @@ type CacheRecord = {
25
29
  type StateCache = Map<string, CacheRecord>;
26
30
 
27
31
  export const Cache = (
28
- onCacheChange: EventEmitterType<CacheDescriptorWithKey>,
32
+ onCacheChange: EventEmitterInstance<CacheDescriptorWithKey>,
29
33
  backendGetRecord: BackendGetRecord,
30
34
  backendGetDevRecord: BackendGetDevRecord,
31
35
  withDefaultNs: (descriptor: CacheDescriptor) => CacheDescriptorInternal,
@@ -35,10 +39,10 @@ export const Cache = (
35
39
  ) => {
36
40
  const asyncRequests: CacheAsyncRequests = new Map();
37
41
  const cache: StateCache = new Map();
38
- let staticData: NonNullable<Options['staticData']> = {};
42
+ let staticData: NonNullable<TolgeeStaticData> = {};
39
43
  let version = 0;
40
44
 
41
- function addStaticData(data: Options['staticData']) {
45
+ function addStaticData(data: TolgeeStaticData | undefined) {
42
46
  if (data) {
43
47
  staticData = { ...staticData, ...data };
44
48
  Object.entries(data).forEach(([key, value]) => {
@@ -86,8 +90,8 @@ export const Cache = (
86
90
  return Boolean(record);
87
91
  }
88
92
 
89
- function getRecord(descriptor: CacheDescriptorInternal) {
90
- return cache.get(encodeCacheKey(descriptor))?.data;
93
+ function getRecord(descriptor: CacheDescriptor) {
94
+ return cache.get(encodeCacheKey(withDefaultNs(descriptor)))?.data;
91
95
  }
92
96
 
93
97
  function getTranslation(descriptor: CacheDescriptorInternal, key: string) {
@@ -105,11 +109,11 @@ export const Cache = (
105
109
  .get(encodeCacheKey({ language, namespace }))
106
110
  ?.data.get(key);
107
111
  if (value !== undefined && value !== null) {
108
- return namespace;
112
+ return [namespace];
109
113
  }
110
114
  }
111
115
  }
112
- return Array.from(new Set(namespaces));
116
+ return unique(namespaces);
113
117
  }
114
118
 
115
119
  function getTranslationFallback(
@@ -140,11 +144,7 @@ export const Cache = (
140
144
  onCacheChange.emit({ ...descriptor, key });
141
145
  }
142
146
 
143
- function clear() {
144
- cache.clear();
145
- }
146
-
147
- function isFetching(ns?: FallbackNSTranslation) {
147
+ function isFetching(ns?: NsFallback) {
148
148
  if (isInitialLoading()) {
149
149
  return true;
150
150
  }
@@ -160,7 +160,7 @@ export const Cache = (
160
160
  );
161
161
  }
162
162
 
163
- function isLoading(language: string | undefined, ns?: FallbackNSTranslation) {
163
+ function isLoading(language: string | undefined, ns?: NsFallback) {
164
164
  const namespaces = getFallbackArray(ns);
165
165
 
166
166
  return Boolean(
@@ -178,7 +178,10 @@ export const Cache = (
178
178
  );
179
179
  }
180
180
 
181
- function fetchNormal(keyObject: CacheDescriptorInternal) {
181
+ /**
182
+ * Fetches production data
183
+ */
184
+ function fetchProd(keyObject: CacheDescriptorInternal) {
182
185
  let dataPromise = undefined as
183
186
  | Promise<TreeTranslationsData | undefined>
184
187
  | undefined;
@@ -186,8 +189,6 @@ export const Cache = (
186
189
  const staticDataValue = staticData[encodeCacheKey(keyObject)];
187
190
  if (typeof staticDataValue === 'function') {
188
191
  dataPromise = staticDataValue();
189
- } else if (staticDataValue) {
190
- dataPromise = Promise.resolve(staticDataValue);
191
192
  }
192
193
  }
193
194
 
@@ -195,10 +196,6 @@ export const Cache = (
195
196
  dataPromise = backendGetRecord(keyObject);
196
197
  }
197
198
 
198
- if (!dataPromise) {
199
- // return empty data, so we know it has already been attempted to fetch
200
- dataPromise = Promise.resolve({});
201
- }
202
199
  return dataPromise;
203
200
  }
204
201
 
@@ -210,13 +207,13 @@ export const Cache = (
210
207
  dataPromise = backendGetDevRecord(keyObject)?.catch(() => {
211
208
  // eslint-disable-next-line no-console
212
209
  console.warn(`Tolgee: Failed to fetch data from dev backend`);
213
- // fallback to normal fetch if dev fails
214
- return fetchNormal(keyObject);
210
+ // fallback to prod fetch if dev fails
211
+ return fetchProd(keyObject);
215
212
  });
216
213
  }
217
214
 
218
215
  if (!dataPromise) {
219
- dataPromise = fetchNormal(keyObject);
216
+ dataPromise = fetchProd(keyObject);
220
217
  }
221
218
 
222
219
  return dataPromise;
@@ -236,7 +233,8 @@ export const Cache = (
236
233
  cacheKey,
237
234
  };
238
235
  }
239
- const dataPromise = fetchData(keyObject, isDev);
236
+ const dataPromise =
237
+ fetchData(keyObject, isDev) || Promise.resolve(undefined);
240
238
  asyncRequests.set(cacheKey, dataPromise);
241
239
  return {
242
240
  new: true,
@@ -260,6 +258,9 @@ export const Cache = (
260
258
  const data = results[i];
261
259
  if (data) {
262
260
  addRecord(value.keyObject, data);
261
+ } else if (!getRecord(value.keyObject)) {
262
+ // if no data exist, put empty object
263
+ addRecord(value.keyObject, {});
263
264
  }
264
265
  }
265
266
  });
@@ -292,9 +293,8 @@ export const Cache = (
292
293
  isFetching,
293
294
  isLoading,
294
295
  loadRecords,
295
- clear,
296
296
  getAllRecords,
297
297
  });
298
298
  };
299
299
 
300
- export type CacheType = ReturnType<typeof Cache>;
300
+ export type CacheInstance = ReturnType<typeof Cache>;
@@ -10,11 +10,9 @@ export const flattenTranslations = (
10
10
  return;
11
11
  }
12
12
  if (typeof value === 'object') {
13
- Object.entries(flattenTranslations(value)).forEach(
14
- ([flatKey, flatValue]) => {
15
- result.set(key + '.' + flatKey, flatValue);
16
- }
17
- );
13
+ flattenTranslations(value).forEach((flatValue, flatKey) => {
14
+ result.set(key + '.' + flatKey, flatValue);
15
+ });
18
16
  return;
19
17
  }
20
18
  result.set(key, value as string);
@@ -23,7 +21,9 @@ export const flattenTranslations = (
23
21
  };
24
22
 
25
23
  export const decodeCacheKey = (key: string): CacheDescriptorInternal => {
26
- const [firstPart, secondPart] = key.split(':');
24
+ const [firstPart, ...rest] = key.split(':');
25
+ // if namespaces contains ":" it won't get lost
26
+ const secondPart = rest.join(':');
27
27
  return { language: firstPart, namespace: secondPart || '' };
28
28
  };
29
29
 
@@ -1,25 +1,26 @@
1
- import type { EventServiceType } from '../Events/Events';
1
+ import { Events } from './Events/Events';
2
2
  import {
3
3
  CacheDescriptor,
4
- FallbackNSTranslation,
5
- Options,
4
+ NsFallback,
5
+ TolgeeOptions,
6
6
  TFnType,
7
- TranslatePropsInternal,
7
+ NsType,
8
+ KeyAndNamespacesInternal,
8
9
  } from '../types';
9
10
  import { Cache } from './Cache/Cache';
10
- import { getFallbackArray } from './State/helpers';
11
- import { PluginService } from './Plugins/Plugins';
11
+ import { getFallbackArray } from '../helpers';
12
+ import { Plugins } from './Plugins/Plugins';
12
13
  import { ValueObserver } from './ValueObserver';
13
14
  import { State } from './State/State';
14
15
  import { isPromise, missingOptionError, valueOrPromise } from '../helpers';
15
- import { getTranslateParams } from '../TranslateParams';
16
+ import { getTranslateProps } from '../TranslateParams';
16
17
 
17
18
  type StateServiceProps = {
18
- events: EventServiceType;
19
- options?: Partial<Options>;
19
+ options?: Partial<TolgeeOptions>;
20
20
  };
21
21
 
22
- export const Controller = ({ events, options }: StateServiceProps) => {
22
+ export const Controller = ({ options }: StateServiceProps) => {
23
+ const events = Events(getFallbackNs, getDefaultNs);
23
24
  const fetchingObserver = ValueObserver<boolean>(
24
25
  false,
25
26
  () => cache.isFetching(),
@@ -37,7 +38,7 @@ export const Controller = ({ events, options }: StateServiceProps) => {
37
38
  events.onRunningChange
38
39
  );
39
40
 
40
- const pluginService = PluginService(
41
+ const pluginService = Plugins(
41
42
  state.getLanguage,
42
43
  state.getInitialOptions,
43
44
  state.getAvailableLanguages,
@@ -56,24 +57,38 @@ export const Controller = ({ events, options }: StateServiceProps) => {
56
57
  loadingObserver
57
58
  );
58
59
 
59
- state.init(options);
60
- cache.addStaticData(state.getInitialOptions().staticData);
61
- if (isDev()) {
62
- cache.invalidate();
60
+ if (options) {
61
+ init(options);
63
62
  }
64
63
 
65
- events.onKeyUpdate.listen(() => {
64
+ events.onUpdate.listen(() => {
66
65
  if (state.isRunning()) {
67
66
  pluginService.retranslate();
68
67
  }
69
68
  });
70
69
 
71
- const t: TFnType = (...args) => {
72
- // @ts-ignore
73
- const params = getTranslateParams(...args);
74
- const translation = getTranslation(params);
75
- return pluginService.formatTranslation({ ...params, translation });
76
- };
70
+ function getFallbackNs() {
71
+ return state.getFallbackNs();
72
+ }
73
+
74
+ function getDefaultNs(ns?: NsType) {
75
+ return state.getDefaultNs(ns);
76
+ }
77
+
78
+ // gets all namespaces where translation could be located
79
+ // takes (ns|default, fallback ns)
80
+ function getDefaultAndFallbackNs(ns?: NsType) {
81
+ return [...getFallbackArray(getDefaultNs(ns)), ...getFallbackNs()];
82
+ }
83
+
84
+ // gets all namespaces which need to be loaded
85
+ // takes (ns|default, initial ns, fallback ns, active ns)
86
+ function getRequiredNamespaces(ns: NsFallback) {
87
+ return [
88
+ ...getFallbackArray(ns || getDefaultNs()),
89
+ ...state.getRequiredNamespaces(),
90
+ ];
91
+ }
77
92
 
78
93
  function changeTranslation(
79
94
  descriptor: CacheDescriptor,
@@ -90,22 +105,22 @@ export const Controller = ({ events, options }: StateServiceProps) => {
90
105
  };
91
106
  }
92
107
 
93
- function init(options: Partial<Options>) {
108
+ function init(options: Partial<TolgeeOptions>) {
94
109
  state.init(options);
95
110
  cache.addStaticData(state.getInitialOptions().staticData);
96
111
  }
97
112
 
98
- function isLoading(ns?: FallbackNSTranslation) {
113
+ function isLoading(ns?: NsFallback) {
99
114
  return cache.isLoading(state.getLanguage()!, ns);
100
115
  }
101
116
 
102
117
  function isDev() {
103
118
  return Boolean(
104
- state.getInitialOptions().apiKey && pluginService.getDevBackend()
119
+ state.getInitialOptions().apiKey && state.getInitialOptions().apiUrl
105
120
  );
106
121
  }
107
122
 
108
- async function addActiveNs(ns: FallbackNSTranslation, forget?: boolean) {
123
+ async function addActiveNs(ns: NsFallback, forget?: boolean) {
109
124
  if (!forget) {
110
125
  state.addActiveNs(ns);
111
126
  }
@@ -114,10 +129,9 @@ export const Controller = ({ events, options }: StateServiceProps) => {
114
129
  }
115
130
  }
116
131
 
117
- function getRequiredRecords(lang?: string, ns?: FallbackNSTranslation) {
132
+ function getRequiredRecords(lang?: string, ns?: NsFallback) {
118
133
  const languages = state.getFallbackLangs(lang);
119
- const namespaces =
120
- ns !== undefined ? getFallbackArray(ns) : state.getRequiredNamespaces();
134
+ const namespaces = getRequiredNamespaces(ns);
121
135
  const result: CacheDescriptor[] = [];
122
136
  languages.forEach((language) => {
123
137
  namespaces.forEach((namespace) => {
@@ -129,14 +143,13 @@ export const Controller = ({ events, options }: StateServiceProps) => {
129
143
  return result;
130
144
  }
131
145
 
132
- function isLoaded(ns?: FallbackNSTranslation) {
146
+ function isLoaded(ns?: NsFallback) {
133
147
  const language = state.getLanguage();
134
148
  if (!language) {
135
149
  return false;
136
150
  }
137
151
  const languages = state.getFallbackLangs(language);
138
- const namespaces =
139
- ns !== undefined ? getFallbackArray(ns) : state.getRequiredNamespaces();
152
+ const namespaces = getRequiredNamespaces(ns);
140
153
  const result: CacheDescriptor[] = [];
141
154
  languages.forEach((language) => {
142
155
  namespaces.forEach((namespace) => {
@@ -148,7 +161,7 @@ export const Controller = ({ events, options }: StateServiceProps) => {
148
161
  return result.length === 0;
149
162
  }
150
163
 
151
- function loadRequiredRecords(lang?: string, ns?: FallbackNSTranslation) {
164
+ function loadRequiredRecords(lang?: string, ns?: NsFallback) {
152
165
  const descriptors = getRequiredRecords(lang, ns);
153
166
  if (descriptors.length) {
154
167
  return valueOrPromise(loadRecords(descriptors), () => {});
@@ -176,24 +189,14 @@ export const Controller = ({ events, options }: StateServiceProps) => {
176
189
  }
177
190
  }
178
191
 
179
- function getTranslationNs({
180
- key,
181
- ns,
182
- }: Pick<TranslatePropsInternal, 'key' | 'ns'>) {
183
- const namespaces = ns
184
- ? getFallbackArray(ns)
185
- : state.getFallbackNamespaces();
192
+ function getTranslationNs({ key, ns }: KeyAndNamespacesInternal) {
186
193
  const languages = state.getFallbackLangs();
194
+ const namespaces = getDefaultAndFallbackNs(ns);
187
195
  return cache.getTranslationNs(namespaces, languages, key);
188
196
  }
189
197
 
190
- function getTranslation({
191
- key,
192
- ns,
193
- }: Pick<TranslatePropsInternal, 'key' | 'ns'>) {
194
- const namespaces = ns
195
- ? getFallbackArray(ns)
196
- : state.getFallbackNamespaces();
198
+ function getTranslation({ key, ns }: KeyAndNamespacesInternal) {
199
+ const namespaces = getDefaultAndFallbackNs(ns);
197
200
  const languages = state.getFallbackLangs();
198
201
  return cache.getTranslationFallback(namespaces, languages, key);
199
202
  }
@@ -201,7 +204,6 @@ export const Controller = ({ events, options }: StateServiceProps) => {
201
204
  function loadInitial() {
202
205
  const data = valueOrPromise(initializeLanguage(), () => {
203
206
  // fail if there is no language
204
- state.getLanguageOrFail();
205
207
  return loadRequiredRecords();
206
208
  });
207
209
 
@@ -245,8 +247,27 @@ export const Controller = ({ events, options }: StateServiceProps) => {
245
247
  return cache.loadRecords(descriptors, isDev());
246
248
  }
247
249
 
250
+ const checkCorrectConfiguration = () => {
251
+ const languageComputable =
252
+ pluginService.getLanguageDetector() || pluginService.getLanguageStorage();
253
+ if (languageComputable) {
254
+ const availableLanguages = state.getAvailableLanguages();
255
+ if (!availableLanguages) {
256
+ throw new Error(missingOptionError('availableLanguages'));
257
+ }
258
+ }
259
+ if (!state.getLanguage() && !state.getInitialOptions().defaultLanguage) {
260
+ if (languageComputable) {
261
+ throw new Error(missingOptionError('defaultLanguage'));
262
+ } else {
263
+ throw new Error(missingOptionError('language'));
264
+ }
265
+ }
266
+ };
267
+
248
268
  function run() {
249
269
  let result: Promise<void> | undefined = undefined;
270
+ checkCorrectConfiguration();
250
271
  if (!state.isRunning()) {
251
272
  if (isDev()) {
252
273
  cache.invalidate();
@@ -265,7 +286,15 @@ export const Controller = ({ events, options }: StateServiceProps) => {
265
286
  }
266
287
  }
267
288
 
289
+ const t: TFnType = (...args) => {
290
+ // @ts-ignore
291
+ const params = getTranslateProps(...args);
292
+ const translation = getTranslation(params);
293
+ return pluginService.formatTranslation({ ...params, translation });
294
+ };
295
+
268
296
  return Object.freeze({
297
+ ...events,
269
298
  ...state,
270
299
  ...pluginService,
271
300
  ...cache,
@@ -274,7 +303,6 @@ export const Controller = ({ events, options }: StateServiceProps) => {
274
303
  getTranslation,
275
304
  changeTranslation,
276
305
  addActiveNs,
277
- loadRequiredRecords,
278
306
  loadRecords,
279
307
  loadRecord,
280
308
  isLoading,
@@ -286,4 +314,4 @@ export const Controller = ({ events, options }: StateServiceProps) => {
286
314
  });
287
315
  };
288
316
 
289
- export type StateServiceType = ReturnType<typeof Controller>;
317
+ export type ControllerInstance = ReturnType<typeof Controller>;
@@ -0,0 +1,34 @@
1
+ import { Subscription, Listener } from '../../types';
2
+
3
+ export const EventEmitter = <T>(
4
+ isActive: () => boolean
5
+ ): EventEmitterInstance<T> => {
6
+ let handlers: Listener<T>[] = [];
7
+
8
+ const listen = (handler: Listener<T>): Subscription => {
9
+ const handlerWrapper: Listener<T> = (e) => {
10
+ handler(e);
11
+ };
12
+
13
+ handlers.push(handlerWrapper);
14
+
15
+ return {
16
+ unsubscribe: () => {
17
+ handlers = handlers.filter((i) => handlerWrapper !== i);
18
+ },
19
+ };
20
+ };
21
+
22
+ const emit = (data: T) => {
23
+ if (isActive()) {
24
+ handlers.forEach((handler) => handler({ value: data }));
25
+ }
26
+ };
27
+
28
+ return Object.freeze({ listen, emit });
29
+ };
30
+
31
+ export type EventEmitterInstance<T> = {
32
+ readonly listen: (handler: Listener<T>) => Subscription;
33
+ readonly emit: (data: T) => void;
34
+ };
@@ -0,0 +1,110 @@
1
+ import { EventEmitterSelective } from './EventEmitterSelective';
2
+
3
+ describe('event emitter selective', () => {
4
+ it('handles correctly default namespace', () => {
5
+ const emitter = EventEmitterSelective(
6
+ () => true,
7
+ () => [],
8
+ () => 'default'
9
+ );
10
+ const handler = jest.fn();
11
+ const listener = emitter.listenSome(handler);
12
+
13
+ // subscribe to default ns
14
+ listener.subscribeNs();
15
+
16
+ // emmit
17
+ emitter.emit(['default']);
18
+ // should be ignored
19
+ emitter.emit(['c']);
20
+
21
+ expect(handler).toBeCalledTimes(1);
22
+ });
23
+
24
+ it('unsubscribes', () => {
25
+ const emitter = EventEmitterSelective(
26
+ () => true,
27
+ () => [],
28
+ () => ''
29
+ );
30
+ const handler = jest.fn();
31
+ const listener = emitter.listen(handler);
32
+
33
+ emitter.emit();
34
+
35
+ listener.unsubscribe();
36
+ emitter.emit();
37
+ expect(handler).toBeCalledTimes(1);
38
+ });
39
+
40
+ it('groups events correctly', async () => {
41
+ const emitter = EventEmitterSelective(
42
+ () => true,
43
+ () => ['test', 'opqrst'],
44
+ () => ''
45
+ );
46
+ const handler = jest.fn();
47
+ const hanlderAll = jest.fn();
48
+ const listener = emitter.listenSome(handler);
49
+ const listenerAll = emitter.listen(hanlderAll);
50
+
51
+ listener.subscribeNs('test');
52
+
53
+ // is fallback should always call handler
54
+ emitter.emit(['opqrst'], true);
55
+
56
+ await new Promise((resolve) => setTimeout(resolve));
57
+
58
+ expect(hanlderAll).toBeCalledTimes(1);
59
+ expect(handler).toBeCalledTimes(1);
60
+
61
+ // these should be merged together
62
+ emitter.emit(['abcd'], true);
63
+ emitter.emit(['abcd']);
64
+
65
+ expect(hanlderAll).toBeCalledTimes(2);
66
+ expect(handler).toBeCalledTimes(1);
67
+
68
+ listener.unsubscribe();
69
+ listenerAll.unsubscribe();
70
+ emitter.emit();
71
+ });
72
+
73
+ it('always subscribes to fallback ns', async () => {
74
+ const emitter = EventEmitterSelective(
75
+ () => true,
76
+ () => ['fallback1', 'fallback2'],
77
+ () => ''
78
+ );
79
+ const handler = jest.fn();
80
+ emitter.listenSome(handler);
81
+
82
+ emitter.emit(['fallback1']);
83
+ expect(handler).toBeCalledTimes(1);
84
+
85
+ emitter.emit(['fallback2']);
86
+ expect(handler).toBeCalledTimes(2);
87
+
88
+ emitter.emit(['test']);
89
+ expect(handler).toBeCalledTimes(2);
90
+ });
91
+
92
+ it('switches off emitting', () => {
93
+ const emitter = EventEmitterSelective(
94
+ () => false,
95
+ () => ['fallback1', 'fallback2'],
96
+ () => ''
97
+ );
98
+ const handler = jest.fn();
99
+ emitter.listenSome(handler);
100
+
101
+ emitter.emit(['fallback1']);
102
+ expect(handler).toBeCalledTimes(0);
103
+
104
+ emitter.emit(['fallback2']);
105
+ expect(handler).toBeCalledTimes(0);
106
+
107
+ emitter.emit(['']);
108
+ expect(handler).toBeCalledTimes(0);
109
+ });
110
+ });