@croct/sdk 0.10.0 → 0.11.0-alpha.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 (231) hide show
  1. package/.src/activeRecord.ts +150 -0
  2. package/.src/base64Url.ts +18 -0
  3. package/.src/cache/cache.ts +15 -0
  4. package/.src/cache/fallbackCache.ts +29 -0
  5. package/.src/cache/inMemoryCache.ts +21 -0
  6. package/.src/cache/index.ts +4 -0
  7. package/.src/cache/localStorageCache.ts +85 -0
  8. package/.src/channel/beaconSocketChannel.ts +153 -0
  9. package/.src/channel/channel.ts +20 -0
  10. package/.src/channel/encodedChannel.ts +21 -0
  11. package/.src/channel/guaranteedChannel.ts +131 -0
  12. package/.src/channel/index.ts +8 -0
  13. package/.src/channel/queuedChannel.ts +112 -0
  14. package/.src/channel/retryChannel.ts +90 -0
  15. package/.src/channel/sandboxChannel.ts +43 -0
  16. package/.src/channel/socketChannel.ts +217 -0
  17. package/.src/cid/assigner.ts +3 -0
  18. package/.src/cid/cachedAssigner.ts +35 -0
  19. package/.src/cid/fixedAssigner.ts +13 -0
  20. package/.src/cid/index.ts +4 -0
  21. package/.src/cid/remoteAssigner.ts +47 -0
  22. package/.src/constants.ts +6 -0
  23. package/.src/container.ts +388 -0
  24. package/.src/contentFetcher.ts +226 -0
  25. package/.src/context.ts +137 -0
  26. package/.src/error.ts +31 -0
  27. package/.src/evaluator.ts +251 -0
  28. package/.src/eventManager.ts +53 -0
  29. package/.src/facade/contentFetcherFacade.ts +69 -0
  30. package/.src/facade/evaluatorFacade.ts +152 -0
  31. package/.src/facade/index.ts +7 -0
  32. package/.src/facade/sdkFacade.ts +291 -0
  33. package/.src/facade/sessionFacade.ts +14 -0
  34. package/.src/facade/sessionPatch.ts +32 -0
  35. package/.src/facade/trackerFacade.ts +98 -0
  36. package/.src/facade/userFacade.ts +26 -0
  37. package/.src/facade/userPatch.ts +32 -0
  38. package/.src/index.ts +4 -0
  39. package/.src/logging/consoleLogger.ts +37 -0
  40. package/.src/logging/index.ts +4 -0
  41. package/.src/logging/logger.ts +13 -0
  42. package/.src/logging/namespacedLogger.ts +32 -0
  43. package/.src/logging/nullLogger.ts +19 -0
  44. package/.src/namespacedStorage.ts +69 -0
  45. package/.src/patch.ts +64 -0
  46. package/.src/queue/capacityRestrictedQueue.ts +44 -0
  47. package/.src/queue/inMemoryQueue.ts +43 -0
  48. package/.src/queue/index.ts +5 -0
  49. package/.src/queue/monitoredQueue.ts +168 -0
  50. package/.src/queue/persistentQueue.ts +84 -0
  51. package/.src/queue/queue.ts +15 -0
  52. package/.src/retry/arbitraryPolicy.ts +21 -0
  53. package/.src/retry/backoffPolicy.ts +84 -0
  54. package/.src/retry/index.ts +5 -0
  55. package/.src/retry/maxAttemptsPolicy.ts +28 -0
  56. package/.src/retry/neverPolicy.ts +11 -0
  57. package/.src/retry/policy.ts +5 -0
  58. package/.src/schema/attributeSchema.ts +6 -0
  59. package/.src/schema/contentFetcherSchemas.ts +23 -0
  60. package/.src/schema/contentSchemas.ts +44 -0
  61. package/.src/schema/contextSchemas.ts +5 -0
  62. package/.src/schema/ecommerceSchemas.ts +179 -0
  63. package/.src/schema/evaluatorSchemas.ts +11 -0
  64. package/.src/schema/eventSchemas.ts +150 -0
  65. package/.src/schema/index.ts +11 -0
  66. package/.src/schema/loggerSchema.ts +12 -0
  67. package/.src/schema/operationSchemas.ts +102 -0
  68. package/.src/schema/sdkFacadeSchemas.ts +44 -0
  69. package/.src/schema/sdkSchemas.ts +49 -0
  70. package/.src/schema/tokenSchema.ts +42 -0
  71. package/.src/schema/userSchema.ts +184 -0
  72. package/.src/sdk.ts +174 -0
  73. package/.src/sdkEvents.ts +15 -0
  74. package/.src/sourceLocation.ts +85 -0
  75. package/.src/tab.ts +148 -0
  76. package/.src/token/cachedTokenStore.ts +34 -0
  77. package/.src/token/inMemoryTokenStore.ts +13 -0
  78. package/.src/token/index.ts +4 -0
  79. package/.src/token/replicatedTokenStore.ts +21 -0
  80. package/.src/token/token.ts +164 -0
  81. package/.src/tracker.ts +460 -0
  82. package/.src/trackingEvents.ts +456 -0
  83. package/.src/transformer.ts +7 -0
  84. package/.src/utilityTypes.ts +3 -0
  85. package/.src/uuid.ts +43 -0
  86. package/.src/validation/arrayType.ts +71 -0
  87. package/.src/validation/booleanType.ts +22 -0
  88. package/.src/validation/functionType.ts +22 -0
  89. package/.src/validation/index.ts +12 -0
  90. package/.src/validation/jsonType.ts +157 -0
  91. package/.src/validation/mixedSchema.ts +7 -0
  92. package/.src/validation/nullType.ts +22 -0
  93. package/.src/validation/numberType.ts +59 -0
  94. package/.src/validation/objectType.ts +138 -0
  95. package/.src/validation/schema.ts +21 -0
  96. package/.src/validation/stringType.ts +118 -0
  97. package/.src/validation/unionType.ts +53 -0
  98. package/.src/validation/violation.ts +23 -0
  99. package/activeRecord.js +33 -36
  100. package/base64Url.js +1 -0
  101. package/cache/cache.js +1 -0
  102. package/cache/fallbackCache.js +16 -32
  103. package/cache/inMemoryCache.js +10 -10
  104. package/cache/index.js +2 -1
  105. package/cache/localStorageCache.js +25 -25
  106. package/channel/beaconSocketChannel.d.ts +1 -1
  107. package/channel/beaconSocketChannel.js +50 -79
  108. package/channel/channel.d.ts +1 -1
  109. package/channel/channel.js +1 -0
  110. package/channel/encodedChannel.js +9 -10
  111. package/channel/guaranteedChannel.d.ts +4 -4
  112. package/channel/guaranteedChannel.js +42 -43
  113. package/channel/index.js +2 -1
  114. package/channel/queuedChannel.js +36 -64
  115. package/channel/retryChannel.d.ts +1 -1
  116. package/channel/retryChannel.js +45 -77
  117. package/channel/sandboxChannel.js +18 -18
  118. package/channel/socketChannel.d.ts +4 -4
  119. package/channel/socketChannel.js +78 -79
  120. package/cid/assigner.js +1 -0
  121. package/cid/cachedAssigner.js +16 -27
  122. package/cid/fixedAssigner.js +6 -6
  123. package/cid/index.js +2 -1
  124. package/cid/remoteAssigner.js +24 -36
  125. package/constants.d.ts +6 -5
  126. package/constants.js +7 -5
  127. package/container.d.ts +13 -6
  128. package/container.js +153 -168
  129. package/contentFetcher.d.ts +59 -0
  130. package/contentFetcher.js +130 -0
  131. package/context.d.ts +3 -3
  132. package/context.js +37 -38
  133. package/error.js +3 -2
  134. package/evaluator.d.ts +33 -24
  135. package/evaluator.js +127 -117
  136. package/eventManager.d.ts +1 -1
  137. package/eventManager.js +15 -15
  138. package/facade/contentFetcherFacade.d.ts +27 -0
  139. package/facade/contentFetcherFacade.js +41 -0
  140. package/facade/evaluatorFacade.d.ts +13 -3
  141. package/facade/evaluatorFacade.js +58 -72
  142. package/facade/index.js +1 -0
  143. package/facade/sdkFacade.d.ts +10 -3
  144. package/facade/sdkFacade.js +130 -141
  145. package/facade/sessionFacade.js +7 -7
  146. package/facade/sessionPatch.js +10 -13
  147. package/facade/trackerFacade.js +33 -38
  148. package/facade/userFacade.js +11 -11
  149. package/facade/userPatch.js +10 -13
  150. package/index.js +3 -2
  151. package/logging/consoleLogger.js +19 -35
  152. package/logging/index.js +2 -1
  153. package/logging/logger.js +1 -0
  154. package/logging/namespacedLogger.js +15 -15
  155. package/logging/nullLogger.js +11 -13
  156. package/namespacedStorage.js +31 -47
  157. package/package.json +13 -16
  158. package/patch.d.ts +1 -1
  159. package/patch.js +1 -0
  160. package/queue/capacityRestrictedQueue.js +18 -18
  161. package/queue/inMemoryQueue.js +23 -28
  162. package/queue/index.js +2 -1
  163. package/queue/monitoredQueue.d.ts +2 -2
  164. package/queue/monitoredQueue.js +40 -40
  165. package/queue/persistentQueue.js +34 -38
  166. package/queue/queue.js +1 -0
  167. package/retry/arbitraryPolicy.js +9 -10
  168. package/retry/backoffPolicy.d.ts +1 -1
  169. package/retry/backoffPolicy.js +12 -13
  170. package/retry/index.js +2 -1
  171. package/retry/maxAttemptsPolicy.js +8 -8
  172. package/retry/neverPolicy.js +7 -9
  173. package/retry/policy.js +1 -0
  174. package/schema/attributeSchema.js +2 -1
  175. package/schema/contentFetcherSchemas.d.ts +2 -0
  176. package/schema/contentFetcherSchemas.js +23 -0
  177. package/schema/contentSchemas.js +2 -1
  178. package/schema/contextSchemas.js +2 -1
  179. package/schema/ecommerceSchemas.js +2 -1
  180. package/schema/evaluatorSchemas.d.ts +2 -0
  181. package/schema/{evaluationSchemas.js → evaluatorSchemas.js} +4 -3
  182. package/schema/eventSchemas.js +6 -7
  183. package/schema/index.d.ts +2 -1
  184. package/schema/index.js +4 -2
  185. package/schema/loggerSchema.js +2 -1
  186. package/schema/operationSchemas.js +9 -8
  187. package/schema/sdkFacadeSchemas.js +10 -6
  188. package/schema/sdkSchemas.js +9 -5
  189. package/schema/tokenSchema.js +6 -4
  190. package/schema/userSchema.js +3 -2
  191. package/sdk.d.ts +9 -3
  192. package/sdk.js +82 -127
  193. package/sdkEvents.d.ts +3 -3
  194. package/sdkEvents.js +1 -0
  195. package/sourceLocation.d.ts +3 -3
  196. package/sourceLocation.js +14 -14
  197. package/tab.d.ts +5 -5
  198. package/tab.js +51 -80
  199. package/token/cachedTokenStore.js +10 -10
  200. package/token/inMemoryTokenStore.js +8 -8
  201. package/token/index.js +2 -1
  202. package/token/replicatedTokenStore.js +8 -8
  203. package/token/token.d.ts +9 -5
  204. package/token/token.js +64 -57
  205. package/tracker.d.ts +4 -4
  206. package/tracker.js +146 -122
  207. package/trackingEvents.d.ts +36 -36
  208. package/trackingEvents.js +13 -6
  209. package/transformer.js +2 -1
  210. package/utilityTypes.d.ts +2 -2
  211. package/utilityTypes.js +1 -0
  212. package/uuid.js +10 -7
  213. package/validation/arrayType.d.ts +2 -2
  214. package/validation/arrayType.js +30 -27
  215. package/validation/booleanType.js +12 -15
  216. package/validation/functionType.js +12 -15
  217. package/validation/index.js +2 -1
  218. package/validation/jsonType.d.ts +2 -2
  219. package/validation/jsonType.js +62 -80
  220. package/validation/mixedSchema.js +5 -7
  221. package/validation/nullType.js +12 -15
  222. package/validation/numberType.d.ts +1 -1
  223. package/validation/numberType.js +24 -22
  224. package/validation/objectType.d.ts +1 -1
  225. package/validation/objectType.js +62 -72
  226. package/validation/schema.js +7 -10
  227. package/validation/stringType.d.ts +1 -1
  228. package/validation/stringType.js +37 -47
  229. package/validation/unionType.js +28 -77
  230. package/validation/violation.js +2 -2
  231. package/schema/evaluationSchemas.d.ts +0 -2
@@ -0,0 +1,388 @@
1
+ import {Logger, ConsoleLogger, NullLogger, NamespacedLogger} from './logging';
2
+ import {Context, TokenScope} from './context';
3
+ import {NamespacedStorage} from './namespacedStorage';
4
+ import {BackoffPolicy, ArbitraryPolicy} from './retry';
5
+ import {PersistentQueue, MonitoredQueue, CapacityRestrictedQueue} from './queue';
6
+ import {Beacon} from './trackingEvents';
7
+ import {CachedTokenStore, TokenStore} from './token';
8
+ import {Tracker} from './tracker';
9
+ import {Evaluator} from './evaluator';
10
+ import {encodeJson} from './transformer';
11
+ import {CidAssigner, CachedAssigner, RemoteAssigner, FixedAssigner} from './cid';
12
+ import {EventManager, SynchronousEventManager} from './eventManager';
13
+ import {SdkEventMap} from './sdkEvents';
14
+ import {LocalStorageCache} from './cache';
15
+ import {UrlSanitizer} from './tab';
16
+ import {TimeStamper} from './channel/guaranteedChannel';
17
+ import {
18
+ OutputChannel,
19
+ QueuedChannel,
20
+ RetryChannel,
21
+ GuaranteedChannel,
22
+ EncodedChannel,
23
+ BeaconSocketChannel,
24
+ SocketChannel,
25
+ SandboxChannel,
26
+ } from './channel';
27
+ import {ContentFetcher} from './contentFetcher';
28
+
29
+ export type Configuration = {
30
+ appId: string,
31
+ tokenScope: TokenScope,
32
+ clientId?: string,
33
+ debug: boolean,
34
+ test: boolean,
35
+ trackerEndpointUrl: string,
36
+ evaluationEndpointUrl: string,
37
+ contentEndpointUrl: string,
38
+ cidAssignerEndpointUrl: string,
39
+ beaconQueueSize: number,
40
+ logger?: Logger,
41
+ urlSanitizer?: UrlSanitizer,
42
+ eventMetadata?: {[key: string]: string},
43
+ };
44
+
45
+ export class Container {
46
+ private readonly configuration: Configuration;
47
+
48
+ private context?: Context;
49
+
50
+ private userTokenProvider?: TokenStore;
51
+
52
+ private previewTokenStore?: TokenStore;
53
+
54
+ private tracker?: Tracker;
55
+
56
+ private evaluator?: Evaluator;
57
+
58
+ private contentFetcher?: ContentFetcher;
59
+
60
+ private cidAssigner?: CidAssigner;
61
+
62
+ private beaconChannel?: OutputChannel<Beacon>;
63
+
64
+ private beaconQueue?: MonitoredQueue<string>;
65
+
66
+ private removeTokenSyncListener?: {(): void};
67
+
68
+ private readonly eventManager = new SynchronousEventManager<SdkEventMap>();
69
+
70
+ public constructor(configuration: Configuration) {
71
+ this.configuration = configuration;
72
+ }
73
+
74
+ public getConfiguration(): Configuration {
75
+ return this.configuration;
76
+ }
77
+
78
+ public getEvaluator(): Evaluator {
79
+ if (this.evaluator === undefined) {
80
+ this.evaluator = this.createEvaluator();
81
+ }
82
+
83
+ return this.evaluator;
84
+ }
85
+
86
+ private createEvaluator(): Evaluator {
87
+ return new Evaluator({
88
+ appId: this.configuration.appId,
89
+ endpointUrl: this.configuration.evaluationEndpointUrl,
90
+ });
91
+ }
92
+
93
+ public getContentFetcher(): ContentFetcher {
94
+ if (this.contentFetcher === undefined) {
95
+ this.contentFetcher = this.createContentFetcher();
96
+ }
97
+
98
+ return this.contentFetcher;
99
+ }
100
+
101
+ private createContentFetcher(): ContentFetcher {
102
+ return new ContentFetcher({
103
+ appId: this.configuration.appId,
104
+ endpointUrl: this.configuration.contentEndpointUrl,
105
+ });
106
+ }
107
+
108
+ public getPreviewTokenStore(): TokenStore {
109
+ if (this.previewTokenStore === undefined) {
110
+ this.previewTokenStore = new CachedTokenStore(
111
+ new LocalStorageCache(this.getGlobalBrowserStorage('preview'), 'token'),
112
+ );
113
+ }
114
+
115
+ return this.previewTokenStore;
116
+ }
117
+
118
+ public getTracker(): Tracker {
119
+ if (this.tracker === undefined) {
120
+ this.tracker = this.createTracker();
121
+ }
122
+
123
+ return this.tracker;
124
+ }
125
+
126
+ private createTracker(): Tracker {
127
+ const context = this.getContext();
128
+
129
+ const tracker = new Tracker({
130
+ tab: context.getTab(),
131
+ tokenProvider: this.getUserTokenStore(),
132
+ inactivityRetryPolicy: new ArbitraryPolicy([30_000, 30_000, 120_000, 120_000, 300_000, 300_000, 900_000]),
133
+ logger: this.getLogger('Tracker'),
134
+ channel: this.getBeaconChannel(),
135
+ eventMetadata: this.configuration.eventMetadata,
136
+ });
137
+
138
+ const queue = this.getBeaconQueue();
139
+
140
+ queue.addCallback('halfEmpty', tracker.unsuspend);
141
+ queue.addCallback('full', tracker.suspend);
142
+
143
+ return tracker;
144
+ }
145
+
146
+ public getUserTokenStore(): TokenStore {
147
+ if (this.userTokenProvider === undefined) {
148
+ const context = this.getContext();
149
+
150
+ this.userTokenProvider = {
151
+ getToken: context.getToken.bind(context),
152
+ setToken: context.setToken.bind(context),
153
+ };
154
+ }
155
+
156
+ return this.userTokenProvider;
157
+ }
158
+
159
+ public getContext(): Context {
160
+ if (this.context === undefined) {
161
+ this.context = this.createContext();
162
+ }
163
+
164
+ return this.context;
165
+ }
166
+
167
+ private createContext(): Context {
168
+ const tokenKey = this.resolveStorageNamespace('token');
169
+ const tabKey = this.resolveStorageNamespace('tab');
170
+ const browserStorage = this.getLocalStorage();
171
+ const browserCache = new LocalStorageCache(browserStorage, tokenKey);
172
+ const tabStorage = this.getSessionStorage();
173
+
174
+ this.removeTokenSyncListener = LocalStorageCache.autoSync(browserCache);
175
+
176
+ return Context.load({
177
+ tokenScope: this.configuration.tokenScope,
178
+ eventDispatcher: this.getEventManager(),
179
+ urlSanitizer: this.configuration.urlSanitizer,
180
+ cache: {
181
+ tabId: new LocalStorageCache(tabStorage, tabKey),
182
+ tabToken: new LocalStorageCache(tabStorage, tokenKey),
183
+ browserToken: browserCache,
184
+ },
185
+ });
186
+ }
187
+
188
+ private getBeaconChannel(): OutputChannel<Beacon> {
189
+ if (this.beaconChannel === undefined) {
190
+ this.beaconChannel = this.createBeaconChannel();
191
+ }
192
+
193
+ return this.beaconChannel;
194
+ }
195
+
196
+ private createBeaconChannel(): OutputChannel<Beacon> {
197
+ if (this.configuration.test) {
198
+ return new SandboxChannel();
199
+ }
200
+
201
+ const channelLogger = this.getLogger('BeaconChannel');
202
+ const {appId, trackerEndpointUrl} = this.configuration;
203
+
204
+ const queuedChannel = new QueuedChannel(
205
+ new RetryChannel({
206
+ channel: new GuaranteedChannel({
207
+ channel: new BeaconSocketChannel({
208
+ trackerEndpointUrl: `${trackerEndpointUrl}/${appId}`,
209
+ tokenParameter: 'token',
210
+ loggerFactory: this.getLogger.bind(this),
211
+ logger: channelLogger,
212
+ channelFactory: (url, logger): SocketChannel<any, any> => (
213
+ new SocketChannel({url: url, logger: logger})
214
+ ),
215
+ cidAssigner: this.getCidAssigner(),
216
+ cidParameter: 'clientId',
217
+ }),
218
+ stamper: new TimeStamper(),
219
+ ackTimeout: 10000,
220
+ logger: channelLogger,
221
+ }),
222
+ retryPolicy: new BackoffPolicy({
223
+ minRetryDelay: 1000, // 1 second
224
+ maxRetryDelay: 60 * 1000, // 60 seconds
225
+ backoffFactor: 1.5, // 1.5 ^ attempt
226
+ backoffJitter: 1, // add randomness
227
+ }),
228
+ logger: channelLogger,
229
+ }),
230
+ this.getBeaconQueue(),
231
+ channelLogger,
232
+ );
233
+
234
+ queuedChannel.flush().catch(() => {
235
+ // Suppress errors as they are already reported by the channel
236
+ });
237
+
238
+ return new EncodedChannel<Beacon, string>(queuedChannel, encodeJson);
239
+ }
240
+
241
+ public getCidAssigner(): CidAssigner {
242
+ if (this.cidAssigner === undefined) {
243
+ this.cidAssigner = this.createCidAssigner();
244
+ }
245
+
246
+ return this.cidAssigner;
247
+ }
248
+
249
+ private createCidAssigner(): CidAssigner {
250
+ if (this.configuration.clientId !== undefined) {
251
+ return new FixedAssigner(this.configuration.clientId);
252
+ }
253
+
254
+ if (this.configuration.test) {
255
+ return new FixedAssigner('00000000-0000-0000-0000-000000000000');
256
+ }
257
+
258
+ const logger = this.getLogger('CidAssigner');
259
+
260
+ return new CachedAssigner(
261
+ new RemoteAssigner(
262
+ this.configuration.cidAssignerEndpointUrl,
263
+ logger,
264
+ ),
265
+ new LocalStorageCache(this.getLocalStorage(), 'croct.cid'),
266
+ logger,
267
+ );
268
+ }
269
+
270
+ public getBeaconQueue(): MonitoredQueue<string> {
271
+ if (this.beaconQueue === undefined) {
272
+ this.beaconQueue = this.createBeaconQueue();
273
+ }
274
+
275
+ return this.beaconQueue;
276
+ }
277
+
278
+ private createBeaconQueue(): MonitoredQueue<string> {
279
+ const context = this.getContext();
280
+ const tab = context.getTab();
281
+
282
+ return new MonitoredQueue<string>(
283
+ new CapacityRestrictedQueue(
284
+ new PersistentQueue(this.getGlobalTabStorage('queue'), tab.id),
285
+ this.configuration.beaconQueueSize,
286
+ ),
287
+ this.getLogger('BeaconQueue'),
288
+ );
289
+ }
290
+
291
+ public getLogger(...namespace: string[]): Logger {
292
+ const prefix = `Croct${namespace.length === 0 ? '' : `:${namespace.join(':')}`}`;
293
+
294
+ if (this.configuration.logger !== undefined) {
295
+ return new NamespacedLogger(this.configuration.logger, prefix);
296
+ }
297
+
298
+ if (this.configuration.debug) {
299
+ return new ConsoleLogger(prefix);
300
+ }
301
+
302
+ return new NullLogger();
303
+ }
304
+
305
+ public getTabStorage(namespace: string, ...subnamespace: string[]): Storage {
306
+ return this.getGlobalTabStorage('external', namespace, ...subnamespace);
307
+ }
308
+
309
+ public getBrowserStorage(namespace: string, ...subnamespace: string[]): Storage {
310
+ return this.getGlobalBrowserStorage('external', namespace, ...subnamespace);
311
+ }
312
+
313
+ private getGlobalTabStorage(namespace: string, ...subnamespace: string[]): Storage {
314
+ return new NamespacedStorage(
315
+ this.getSessionStorage(),
316
+ this.resolveStorageNamespace(namespace, ...subnamespace),
317
+ );
318
+ }
319
+
320
+ private getGlobalBrowserStorage(namespace: string, ...subnamespace: string[]): Storage {
321
+ return new NamespacedStorage(
322
+ this.getLocalStorage(),
323
+ this.resolveStorageNamespace(namespace, ...subnamespace),
324
+ );
325
+ }
326
+
327
+ private resolveStorageNamespace(namespace: string, ...subnamespace: string[]): string {
328
+ return `croct[${this.configuration
329
+ .appId
330
+ .toLowerCase()}].${[namespace].concat(subnamespace).join('.')}`;
331
+ }
332
+
333
+ private getLocalStorage(): Storage {
334
+ return localStorage;
335
+ }
336
+
337
+ private getSessionStorage(): Storage {
338
+ return sessionStorage;
339
+ }
340
+
341
+ public getEventManager(): EventManager<SdkEventMap> {
342
+ return this.eventManager;
343
+ }
344
+
345
+ public async dispose(): Promise<void> {
346
+ const logger = this.getLogger();
347
+
348
+ if (this.beaconChannel !== undefined) {
349
+ logger.debug('Closing beacon channel...');
350
+
351
+ await this.beaconChannel.close();
352
+ }
353
+
354
+ if (this.removeTokenSyncListener !== undefined) {
355
+ logger.debug('Removing token sync listener...');
356
+
357
+ this.removeTokenSyncListener();
358
+ }
359
+
360
+ if (this.tracker !== undefined) {
361
+ if (this.beaconQueue !== undefined) {
362
+ logger.debug('Removing queue listeners...');
363
+
364
+ this.beaconQueue.removeCallback('halfEmpty', this.tracker.unsuspend);
365
+ this.beaconQueue.removeCallback('full', this.tracker.suspend);
366
+ }
367
+
368
+ logger.debug('Suspending tracker...');
369
+
370
+ this.tracker.suspend();
371
+
372
+ await this.tracker.flushed;
373
+ }
374
+
375
+ delete this.context;
376
+ delete this.userTokenProvider;
377
+ delete this.previewTokenStore;
378
+ delete this.cidAssigner;
379
+ delete this.tracker;
380
+ delete this.evaluator;
381
+ delete this.contentFetcher;
382
+ delete this.beaconChannel;
383
+ delete this.beaconQueue;
384
+ delete this.removeTokenSyncListener;
385
+
386
+ logger.debug('Container resources released.');
387
+ }
388
+ }
@@ -0,0 +1,226 @@
1
+ import {JsonObject} from '@croct/json';
2
+ import {EvaluationContext} from './evaluator';
3
+ import {Token} from './token';
4
+ import {CONTENT_ENDPOINT_URL} from './constants';
5
+ import {formatMessage} from './error';
6
+
7
+ export type ErrorResponse = {
8
+ type: string,
9
+ title: string,
10
+ status: number,
11
+ detail?: string,
12
+ };
13
+
14
+ export enum ContentErrorType {
15
+ TIMEOUT = 'https://croct.help/api/content#timeout',
16
+ UNEXPECTED_ERROR = 'https://croct.help/api/content#unexpected-error',
17
+ }
18
+
19
+ type FetchPayload = {
20
+ slotId: string,
21
+ version?: string,
22
+ preferredLocale?: string,
23
+ previewToken?: string,
24
+ context?: EvaluationContext,
25
+ };
26
+
27
+ export class ContentError<T extends ErrorResponse = ErrorResponse> extends Error {
28
+ public readonly response: T;
29
+
30
+ public constructor(response: T) {
31
+ super(response.title);
32
+
33
+ this.response = response;
34
+
35
+ Object.setPrototypeOf(this, ContentError.prototype);
36
+ }
37
+ }
38
+
39
+ type BasicOptions = {
40
+ version?: `${number}` | number,
41
+ preferredLocale?: string,
42
+ timeout?: number,
43
+ extra?: ExtraFetchOptions,
44
+ };
45
+
46
+ export type StaticContentOptions = BasicOptions & {
47
+ static: true,
48
+ };
49
+
50
+ export type DynamicContentOptions = BasicOptions & {
51
+ static?: false,
52
+ clientId?: string,
53
+ clientIp?: string,
54
+ userAgent?: string,
55
+ userToken?: Token|string,
56
+ previewToken?: Token|string,
57
+ context?: EvaluationContext,
58
+ };
59
+
60
+ type AllowedFetchOptions = Exclude<keyof RequestInit, 'method' | 'body' | 'headers' | 'signal'>;
61
+
62
+ type ExtraFetchOptions<T extends keyof RequestInit = AllowedFetchOptions> = Pick<RequestInit, T>
63
+ & {[key in Exclude<keyof RequestInit, T>]?: never}
64
+ & Record<string, any>;
65
+
66
+ export type FetchOptions = StaticContentOptions | DynamicContentOptions;
67
+
68
+ export type FetchResponse<P extends JsonObject = JsonObject> = {
69
+ content: P,
70
+ };
71
+
72
+ export type Configuration = {
73
+ appId?: string,
74
+ apiKey?: string,
75
+ endpointUrl?: string,
76
+ };
77
+
78
+ export class ContentFetcher {
79
+ private readonly configuration: Configuration;
80
+
81
+ private readonly dynamicEndpoint: string;
82
+
83
+ private readonly staticEndpoint: string;
84
+
85
+ public constructor(configuration: Configuration) {
86
+ if ((configuration.appId === undefined) === (configuration.apiKey === undefined)) {
87
+ throw new Error('Either the application ID or the API key must be provided.');
88
+ }
89
+
90
+ this.configuration = configuration;
91
+
92
+ const {apiKey, endpointUrl} = this.configuration;
93
+
94
+ // eslint-disable-next-line prefer-template -- Better readability
95
+ const baseEndpoint = (endpointUrl ?? CONTENT_ENDPOINT_URL).replace(/\/+$/, '')
96
+ + (apiKey === undefined ? '/client' : '/external')
97
+ + '/web';
98
+
99
+ this.dynamicEndpoint = `${baseEndpoint}/content`;
100
+ this.staticEndpoint = `${baseEndpoint}/static-content`;
101
+ }
102
+
103
+ public fetch<P extends JsonObject>(slotId: string, options: FetchOptions = {}): Promise<FetchResponse<P>> {
104
+ if (options.static === true && this.configuration.apiKey === undefined) {
105
+ throw new Error('The API key must be provided to fetch static content.');
106
+ }
107
+
108
+ return new Promise((resolve, reject) => {
109
+ const abortController = new AbortController();
110
+
111
+ if (options.timeout !== undefined) {
112
+ setTimeout(
113
+ () => {
114
+ const response: ErrorResponse = {
115
+ title: 'Maximum timeout reached before content could be loaded.',
116
+ type: ContentErrorType.TIMEOUT,
117
+ detail: `The content took more than ${options.timeout}ms to load.`,
118
+ status: 408, // Request Timeout
119
+ };
120
+
121
+ abortController.abort();
122
+
123
+ reject(new ContentError(response));
124
+ },
125
+ options.timeout,
126
+ );
127
+ }
128
+
129
+ this.load(slotId, abortController.signal, options)
130
+ .then(
131
+ response => response.json()
132
+ .then(body => {
133
+ if (response.ok) {
134
+ resolve(body);
135
+ } else {
136
+ reject(new ContentError(body));
137
+ }
138
+ }),
139
+ )
140
+ .catch(error => {
141
+ if (!abortController.signal.aborted) {
142
+ reject(
143
+ new ContentError({
144
+ title: formatMessage(error),
145
+ type: ContentErrorType.UNEXPECTED_ERROR,
146
+ detail: 'Please try again or contact Croct support if the error persists.',
147
+ status: 500, // Internal Server Error
148
+ }),
149
+ );
150
+ }
151
+ });
152
+ });
153
+ }
154
+
155
+ private load(slotId: string, signal: AbortSignal, options: FetchOptions): Promise<Response> {
156
+ const payload: FetchPayload = {
157
+ slotId: slotId,
158
+ };
159
+
160
+ const {apiKey, appId} = this.configuration;
161
+
162
+ const headers = new Headers();
163
+
164
+ if (appId !== undefined) {
165
+ headers.set('X-App-Id', appId);
166
+ }
167
+
168
+ if (apiKey !== undefined) {
169
+ headers.set('X-Api-Key', apiKey);
170
+ }
171
+
172
+ const dynamic = ContentFetcher.isDynamicContent(options);
173
+
174
+ if (dynamic) {
175
+ if (options.clientId !== undefined) {
176
+ headers.set('X-Client-Id', options.clientId);
177
+ }
178
+
179
+ if (options.clientIp !== undefined) {
180
+ headers.set('X-Client-Ip', options.clientIp);
181
+ }
182
+
183
+ if (options.userToken !== undefined) {
184
+ headers.set('X-Token', options.userToken.toString());
185
+ }
186
+
187
+ if (options.userAgent !== undefined) {
188
+ headers.set('User-Agent', options.userAgent);
189
+ }
190
+
191
+ if (options.version !== undefined) {
192
+ payload.version = `${options.version}`;
193
+ }
194
+
195
+ if (options.preferredLocale !== undefined) {
196
+ payload.preferredLocale = options.preferredLocale;
197
+ }
198
+
199
+ if (options.context !== undefined) {
200
+ payload.context = options.context;
201
+ }
202
+
203
+ if (options.previewToken !== undefined) {
204
+ payload.previewToken = `${options.previewToken}`;
205
+ }
206
+ }
207
+
208
+ return fetch(dynamic ? this.dynamicEndpoint : this.staticEndpoint, {
209
+ credentials: 'omit',
210
+ ...options.extra,
211
+ method: 'POST',
212
+ headers: headers,
213
+ signal: signal,
214
+ body: JSON.stringify(payload),
215
+ });
216
+ }
217
+
218
+ private static isDynamicContent(options: FetchOptions): options is DynamicContentOptions {
219
+ return options.static !== true;
220
+ }
221
+
222
+ public toJSON(): never {
223
+ // Prevent sensitive configuration from being serialized
224
+ throw new Error('Unserializable value.');
225
+ }
226
+ }