@croct/sdk 0.11.0-alpha → 0.11.0-alpha.2
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/activeRecord.js +1 -0
- package/activeRecord.js.map +1 -0
- package/base64Url.js +1 -0
- package/base64Url.js.map +1 -0
- package/cache/cache.js +1 -0
- package/cache/cache.js.map +1 -0
- package/cache/fallbackCache.js +1 -0
- package/cache/fallbackCache.js.map +1 -0
- package/cache/inMemoryCache.js +1 -0
- package/cache/inMemoryCache.js.map +1 -0
- package/cache/index.js +1 -0
- package/cache/index.js.map +1 -0
- package/cache/localStorageCache.js +1 -0
- package/cache/localStorageCache.js.map +1 -0
- package/channel/beaconSocketChannel.js +1 -0
- package/channel/beaconSocketChannel.js.map +1 -0
- package/channel/channel.js +1 -0
- package/channel/channel.js.map +1 -0
- package/channel/encodedChannel.js +1 -0
- package/channel/encodedChannel.js.map +1 -0
- package/channel/guaranteedChannel.js +1 -0
- package/channel/guaranteedChannel.js.map +1 -0
- package/channel/index.js +1 -0
- package/channel/index.js.map +1 -0
- package/channel/queuedChannel.js +1 -0
- package/channel/queuedChannel.js.map +1 -0
- package/channel/retryChannel.js +1 -0
- package/channel/retryChannel.js.map +1 -0
- package/channel/sandboxChannel.js +1 -0
- package/channel/sandboxChannel.js.map +1 -0
- package/channel/socketChannel.js +1 -0
- package/channel/socketChannel.js.map +1 -0
- package/cid/assigner.js +1 -0
- package/cid/assigner.js.map +1 -0
- package/cid/cachedAssigner.js +1 -0
- package/cid/cachedAssigner.js.map +1 -0
- package/cid/fixedAssigner.js +1 -0
- package/cid/fixedAssigner.js.map +1 -0
- package/cid/index.js +1 -0
- package/cid/index.js.map +1 -0
- package/cid/remoteAssigner.js +1 -0
- package/cid/remoteAssigner.js.map +1 -0
- package/constants.d.ts +5 -5
- package/constants.js +2 -1
- package/constants.js.map +1 -0
- package/container.js +1 -0
- package/container.js.map +1 -0
- package/contentFetcher.js +1 -0
- package/contentFetcher.js.map +1 -0
- package/context.js +1 -0
- package/context.js.map +1 -0
- package/error.js +1 -0
- package/error.js.map +1 -0
- package/evaluator.js +1 -0
- package/evaluator.js.map +1 -0
- package/eventManager.js +1 -0
- package/eventManager.js.map +1 -0
- package/facade/contentFetcherFacade.js +1 -0
- package/facade/contentFetcherFacade.js.map +1 -0
- package/facade/evaluatorFacade.js +1 -0
- package/facade/evaluatorFacade.js.map +1 -0
- package/facade/index.js +1 -0
- package/facade/index.js.map +1 -0
- package/facade/sdkFacade.js +1 -0
- package/facade/sdkFacade.js.map +1 -0
- package/facade/sessionFacade.js +1 -0
- package/facade/sessionFacade.js.map +1 -0
- package/facade/sessionPatch.js +1 -0
- package/facade/sessionPatch.js.map +1 -0
- package/facade/trackerFacade.js +1 -0
- package/facade/trackerFacade.js.map +1 -0
- package/facade/userFacade.js +1 -0
- package/facade/userFacade.js.map +1 -0
- package/facade/userPatch.js +1 -0
- package/facade/userPatch.js.map +1 -0
- package/index.js +1 -0
- package/index.js.map +1 -0
- package/logging/consoleLogger.js +1 -0
- package/logging/consoleLogger.js.map +1 -0
- package/logging/index.js +1 -0
- package/logging/index.js.map +1 -0
- package/logging/logger.js +1 -0
- package/logging/logger.js.map +1 -0
- package/logging/namespacedLogger.js +1 -0
- package/logging/namespacedLogger.js.map +1 -0
- package/logging/nullLogger.js +1 -0
- package/logging/nullLogger.js.map +1 -0
- package/namespacedStorage.js +1 -0
- package/namespacedStorage.js.map +1 -0
- package/package.json +3 -2
- package/patch.js +1 -0
- package/patch.js.map +1 -0
- package/queue/capacityRestrictedQueue.js +1 -0
- package/queue/capacityRestrictedQueue.js.map +1 -0
- package/queue/inMemoryQueue.js +1 -0
- package/queue/inMemoryQueue.js.map +1 -0
- package/queue/index.js +1 -0
- package/queue/index.js.map +1 -0
- package/queue/monitoredQueue.js +1 -0
- package/queue/monitoredQueue.js.map +1 -0
- package/queue/persistentQueue.js +1 -0
- package/queue/persistentQueue.js.map +1 -0
- package/queue/queue.js +1 -0
- package/queue/queue.js.map +1 -0
- package/retry/arbitraryPolicy.js +1 -0
- package/retry/arbitraryPolicy.js.map +1 -0
- package/retry/backoffPolicy.js +1 -0
- package/retry/backoffPolicy.js.map +1 -0
- package/retry/index.js +1 -0
- package/retry/index.js.map +1 -0
- package/retry/maxAttemptsPolicy.js +1 -0
- package/retry/maxAttemptsPolicy.js.map +1 -0
- package/retry/neverPolicy.js +1 -0
- package/retry/neverPolicy.js.map +1 -0
- package/retry/policy.js +1 -0
- package/retry/policy.js.map +1 -0
- package/schema/attributeSchema.js +1 -0
- package/schema/attributeSchema.js.map +1 -0
- package/schema/contentFetcherSchemas.js +1 -0
- package/schema/contentFetcherSchemas.js.map +1 -0
- package/schema/contentSchemas.js +1 -0
- package/schema/contentSchemas.js.map +1 -0
- package/schema/contextSchemas.js +1 -0
- package/schema/contextSchemas.js.map +1 -0
- package/schema/ecommerceSchemas.js +1 -0
- package/schema/ecommerceSchemas.js.map +1 -0
- package/schema/evaluatorSchemas.js +1 -0
- package/schema/evaluatorSchemas.js.map +1 -0
- package/schema/eventSchemas.js +1 -0
- package/schema/eventSchemas.js.map +1 -0
- package/schema/index.js +1 -0
- package/schema/index.js.map +1 -0
- package/schema/loggerSchema.js +1 -0
- package/schema/loggerSchema.js.map +1 -0
- package/schema/operationSchemas.js +1 -0
- package/schema/operationSchemas.js.map +1 -0
- package/schema/sdkFacadeSchemas.js +1 -0
- package/schema/sdkFacadeSchemas.js.map +1 -0
- package/schema/sdkSchemas.js +1 -0
- package/schema/sdkSchemas.js.map +1 -0
- package/schema/tokenSchema.js +1 -0
- package/schema/tokenSchema.js.map +1 -0
- package/schema/userSchema.js +1 -0
- package/schema/userSchema.js.map +1 -0
- package/sdk.js +1 -0
- package/sdk.js.map +1 -0
- package/sdkEvents.js +1 -0
- package/sdkEvents.js.map +1 -0
- package/sourceLocation.js +1 -0
- package/sourceLocation.js.map +1 -0
- package/src/activeRecord.ts +150 -0
- package/src/base64Url.ts +18 -0
- package/src/cache/cache.ts +15 -0
- package/src/cache/fallbackCache.ts +29 -0
- package/src/cache/inMemoryCache.ts +21 -0
- package/src/cache/index.ts +4 -0
- package/src/cache/localStorageCache.ts +85 -0
- package/src/channel/beaconSocketChannel.ts +153 -0
- package/src/channel/channel.ts +20 -0
- package/src/channel/encodedChannel.ts +21 -0
- package/src/channel/guaranteedChannel.ts +131 -0
- package/src/channel/index.ts +8 -0
- package/src/channel/queuedChannel.ts +112 -0
- package/src/channel/retryChannel.ts +90 -0
- package/src/channel/sandboxChannel.ts +43 -0
- package/src/channel/socketChannel.ts +217 -0
- package/src/cid/assigner.ts +3 -0
- package/src/cid/cachedAssigner.ts +35 -0
- package/src/cid/fixedAssigner.ts +13 -0
- package/src/cid/index.ts +4 -0
- package/src/cid/remoteAssigner.ts +47 -0
- package/src/constants.ts +6 -0
- package/src/container.ts +388 -0
- package/src/contentFetcher.ts +226 -0
- package/src/context.ts +137 -0
- package/src/error.ts +31 -0
- package/src/evaluator.ts +251 -0
- package/src/eventManager.ts +53 -0
- package/src/facade/contentFetcherFacade.ts +69 -0
- package/src/facade/evaluatorFacade.ts +152 -0
- package/src/facade/index.ts +7 -0
- package/src/facade/sdkFacade.ts +291 -0
- package/src/facade/sessionFacade.ts +14 -0
- package/src/facade/sessionPatch.ts +32 -0
- package/src/facade/trackerFacade.ts +98 -0
- package/src/facade/userFacade.ts +26 -0
- package/src/facade/userPatch.ts +32 -0
- package/src/index.ts +4 -0
- package/src/logging/consoleLogger.ts +37 -0
- package/src/logging/index.ts +4 -0
- package/src/logging/logger.ts +13 -0
- package/src/logging/namespacedLogger.ts +32 -0
- package/src/logging/nullLogger.ts +19 -0
- package/src/namespacedStorage.ts +69 -0
- package/src/patch.ts +64 -0
- package/src/queue/capacityRestrictedQueue.ts +44 -0
- package/src/queue/inMemoryQueue.ts +43 -0
- package/src/queue/index.ts +5 -0
- package/src/queue/monitoredQueue.ts +168 -0
- package/src/queue/persistentQueue.ts +84 -0
- package/src/queue/queue.ts +15 -0
- package/src/retry/arbitraryPolicy.ts +21 -0
- package/src/retry/backoffPolicy.ts +84 -0
- package/src/retry/index.ts +5 -0
- package/src/retry/maxAttemptsPolicy.ts +28 -0
- package/src/retry/neverPolicy.ts +11 -0
- package/src/retry/policy.ts +5 -0
- package/src/schema/attributeSchema.ts +6 -0
- package/src/schema/contentFetcherSchemas.ts +23 -0
- package/src/schema/contentSchemas.ts +44 -0
- package/src/schema/contextSchemas.ts +5 -0
- package/src/schema/ecommerceSchemas.ts +179 -0
- package/src/schema/evaluatorSchemas.ts +11 -0
- package/src/schema/eventSchemas.ts +150 -0
- package/src/schema/index.ts +11 -0
- package/src/schema/loggerSchema.ts +12 -0
- package/src/schema/operationSchemas.ts +102 -0
- package/src/schema/sdkFacadeSchemas.ts +44 -0
- package/src/schema/sdkSchemas.ts +49 -0
- package/src/schema/tokenSchema.ts +42 -0
- package/src/schema/userSchema.ts +184 -0
- package/src/sdk.ts +174 -0
- package/src/sdkEvents.ts +15 -0
- package/src/sourceLocation.ts +85 -0
- package/src/tab.ts +148 -0
- package/src/token/cachedTokenStore.ts +34 -0
- package/src/token/inMemoryTokenStore.ts +13 -0
- package/src/token/index.ts +4 -0
- package/src/token/replicatedTokenStore.ts +21 -0
- package/src/token/token.ts +164 -0
- package/src/tracker.ts +460 -0
- package/src/trackingEvents.ts +456 -0
- package/src/transformer.ts +7 -0
- package/src/utilityTypes.ts +3 -0
- package/src/uuid.ts +43 -0
- package/src/validation/arrayType.ts +71 -0
- package/src/validation/booleanType.ts +22 -0
- package/src/validation/functionType.ts +22 -0
- package/src/validation/index.ts +12 -0
- package/src/validation/jsonType.ts +157 -0
- package/src/validation/mixedSchema.ts +7 -0
- package/src/validation/nullType.ts +22 -0
- package/src/validation/numberType.ts +59 -0
- package/src/validation/objectType.ts +138 -0
- package/src/validation/schema.ts +21 -0
- package/src/validation/stringType.ts +118 -0
- package/src/validation/unionType.ts +53 -0
- package/src/validation/violation.ts +23 -0
- package/tab.js +1 -0
- package/tab.js.map +1 -0
- package/token/cachedTokenStore.js +1 -0
- package/token/cachedTokenStore.js.map +1 -0
- package/token/inMemoryTokenStore.js +1 -0
- package/token/inMemoryTokenStore.js.map +1 -0
- package/token/index.js +1 -0
- package/token/index.js.map +1 -0
- package/token/replicatedTokenStore.js +1 -0
- package/token/replicatedTokenStore.js.map +1 -0
- package/token/token.js +1 -0
- package/token/token.js.map +1 -0
- package/tracker.js +1 -0
- package/tracker.js.map +1 -0
- package/trackingEvents.js +1 -0
- package/trackingEvents.js.map +1 -0
- package/transformer.js +1 -0
- package/transformer.js.map +1 -0
- package/utilityTypes.js +1 -0
- package/utilityTypes.js.map +1 -0
- package/uuid.js +1 -0
- package/uuid.js.map +1 -0
- package/validation/arrayType.js +1 -0
- package/validation/arrayType.js.map +1 -0
- package/validation/booleanType.js +1 -0
- package/validation/booleanType.js.map +1 -0
- package/validation/functionType.js +1 -0
- package/validation/functionType.js.map +1 -0
- package/validation/index.js +1 -0
- package/validation/index.js.map +1 -0
- package/validation/jsonType.js +1 -0
- package/validation/jsonType.js.map +1 -0
- package/validation/mixedSchema.js +1 -0
- package/validation/mixedSchema.js.map +1 -0
- package/validation/nullType.js +1 -0
- package/validation/nullType.js.map +1 -0
- package/validation/numberType.js +1 -0
- package/validation/numberType.js.map +1 -0
- package/validation/objectType.js +1 -0
- package/validation/objectType.js.map +1 -0
- package/validation/schema.js +1 -0
- package/validation/schema.js.map +1 -0
- package/validation/stringType.js +1 -0
- package/validation/stringType.js.map +1 -0
- package/validation/unionType.js +1 -0
- package/validation/unionType.js.map +1 -0
- package/validation/violation.js +1 -0
- package/validation/violation.js.map +1 -0
package/src/context.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {Token, TokenStore, CachedTokenStore, ReplicatedTokenStore, InMemoryTokenStore} from './token';
|
|
2
|
+
import {Tab, UrlSanitizer} from './tab';
|
|
3
|
+
import {uuid4} from './uuid';
|
|
4
|
+
import {EventDispatcher} from './eventManager';
|
|
5
|
+
import {SdkEventMap} from './sdkEvents';
|
|
6
|
+
import {LocalStorageCache} from './cache';
|
|
7
|
+
|
|
8
|
+
export type TokenScope = 'isolated' | 'global' | 'contextual';
|
|
9
|
+
|
|
10
|
+
export type Configuration = {
|
|
11
|
+
tokenScope: TokenScope,
|
|
12
|
+
urlSanitizer?: UrlSanitizer,
|
|
13
|
+
eventDispatcher: ContextEventDispatcher,
|
|
14
|
+
cache: {
|
|
15
|
+
tabId: LocalStorageCache,
|
|
16
|
+
tabToken: LocalStorageCache,
|
|
17
|
+
browserToken: LocalStorageCache,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ContextEventDispatcher = EventDispatcher<Pick<SdkEventMap, 'tokenChanged'>>;
|
|
22
|
+
|
|
23
|
+
function tokenEquals(left: Token|null, right: Token|null): boolean {
|
|
24
|
+
return left === right || (left !== null && right !== null && left.toString() === right.toString());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class Context {
|
|
28
|
+
private readonly tab: Tab;
|
|
29
|
+
|
|
30
|
+
private readonly tokenStore: TokenStore;
|
|
31
|
+
|
|
32
|
+
private readonly eventDispatcher: ContextEventDispatcher;
|
|
33
|
+
|
|
34
|
+
private lastToken: Token|null;
|
|
35
|
+
|
|
36
|
+
private constructor(tab: Tab, tokenStore: TokenStore, eventDispatcher: ContextEventDispatcher) {
|
|
37
|
+
this.tab = tab;
|
|
38
|
+
this.tokenStore = tokenStore;
|
|
39
|
+
this.eventDispatcher = eventDispatcher;
|
|
40
|
+
this.lastToken = tokenStore.getToken();
|
|
41
|
+
this.syncToken = this.syncToken.bind(this);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public static load({cache, tokenScope, eventDispatcher, urlSanitizer}: Configuration): Context {
|
|
45
|
+
let tabId: string | null = cache.tabId.get();
|
|
46
|
+
let newTab = false;
|
|
47
|
+
|
|
48
|
+
if (tabId === null) {
|
|
49
|
+
tabId = uuid4(true);
|
|
50
|
+
newTab = true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tab = new Tab(tabId, newTab, urlSanitizer);
|
|
54
|
+
|
|
55
|
+
cache.tabId.clear();
|
|
56
|
+
|
|
57
|
+
tab.addListener('unload', () => cache.tabId.put(tab.id));
|
|
58
|
+
|
|
59
|
+
switch (tokenScope) {
|
|
60
|
+
case 'isolated':
|
|
61
|
+
return new Context(tab, new InMemoryTokenStore(), eventDispatcher);
|
|
62
|
+
|
|
63
|
+
case 'global': {
|
|
64
|
+
const context = new Context(tab, new CachedTokenStore(cache.browserToken), eventDispatcher);
|
|
65
|
+
|
|
66
|
+
cache.browserToken.addListener(context.syncToken);
|
|
67
|
+
|
|
68
|
+
return context;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case 'contextual': {
|
|
72
|
+
const primaryStorage = new CachedTokenStore(cache.tabToken);
|
|
73
|
+
const secondaryStorage = new CachedTokenStore(cache.browserToken);
|
|
74
|
+
|
|
75
|
+
if (tab.isNew) {
|
|
76
|
+
primaryStorage.setToken(secondaryStorage.getToken());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
tab.addListener('visibilityChange', event => {
|
|
80
|
+
if (event.detail.visible) {
|
|
81
|
+
secondaryStorage.setToken(primaryStorage.getToken());
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return new Context(tab, new ReplicatedTokenStore(primaryStorage, secondaryStorage), eventDispatcher);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public getTab(): Tab {
|
|
91
|
+
return this.tab;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public isAnonymous(): boolean {
|
|
95
|
+
const token = this.getToken();
|
|
96
|
+
|
|
97
|
+
return token == null || token.isAnonymous();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public getUser(): string | null {
|
|
101
|
+
const token = this.getToken();
|
|
102
|
+
|
|
103
|
+
return token == null ? null : token.getSubject();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public getToken(): Token | null {
|
|
107
|
+
return this.tokenStore.getToken();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public setToken(token: Token | null): void {
|
|
111
|
+
const oldToken = this.lastToken;
|
|
112
|
+
|
|
113
|
+
this.lastToken = token;
|
|
114
|
+
this.tokenStore.setToken(token);
|
|
115
|
+
|
|
116
|
+
if (!tokenEquals(oldToken, token)) {
|
|
117
|
+
this.eventDispatcher.dispatch('tokenChanged', {
|
|
118
|
+
oldToken: oldToken,
|
|
119
|
+
newToken: token,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private syncToken(): void {
|
|
125
|
+
const newToken = this.tokenStore.getToken();
|
|
126
|
+
const oldToken = this.lastToken;
|
|
127
|
+
|
|
128
|
+
if (!tokenEquals(oldToken, newToken)) {
|
|
129
|
+
this.lastToken = newToken;
|
|
130
|
+
|
|
131
|
+
this.eventDispatcher.dispatch('tokenChanged', {
|
|
132
|
+
oldToken: oldToken,
|
|
133
|
+
newToken: newToken,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function extractMessage(error: unknown): string {
|
|
2
|
+
if (error instanceof Error) {
|
|
3
|
+
return error.message;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (typeof error === 'string' && error !== '') {
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return 'unknown error';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function formatMessage(error: unknown): string {
|
|
14
|
+
const message = extractMessage(error);
|
|
15
|
+
|
|
16
|
+
if (message.length === 0) {
|
|
17
|
+
return message;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return message.charAt(0).toUpperCase() + message.slice(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatCause(error: unknown): string {
|
|
24
|
+
const message = formatMessage(error);
|
|
25
|
+
|
|
26
|
+
if (message.length === 0) {
|
|
27
|
+
return message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return message.charAt(0).toLowerCase() + message.slice(1);
|
|
31
|
+
}
|
package/src/evaluator.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import {JsonObject, JsonValue} from '@croct/json';
|
|
2
|
+
import {Token} from './token';
|
|
3
|
+
import {EVALUATION_ENDPOINT_URL, MAX_QUERY_LENGTH} from './constants';
|
|
4
|
+
import {formatMessage} from './error';
|
|
5
|
+
import {getLength, getLocation, Location} from './sourceLocation';
|
|
6
|
+
|
|
7
|
+
export type Campaign = {
|
|
8
|
+
name?: string,
|
|
9
|
+
source?: string,
|
|
10
|
+
medium?: string,
|
|
11
|
+
term?: string,
|
|
12
|
+
content?: string,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type Page = {
|
|
16
|
+
url: string,
|
|
17
|
+
title?: string,
|
|
18
|
+
referrer?: string,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type EvaluationContext = {
|
|
22
|
+
timeZone?: string,
|
|
23
|
+
campaign?: Campaign,
|
|
24
|
+
page?: Page,
|
|
25
|
+
attributes?: JsonObject,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type AllowedFetchOptions = Exclude<keyof RequestInit, 'method' | 'body' | 'headers' | 'signal'>;
|
|
29
|
+
|
|
30
|
+
type ExtraFetchOptions<T extends keyof RequestInit = AllowedFetchOptions> = Pick<RequestInit, T>
|
|
31
|
+
& {[key in Exclude<keyof RequestInit, T>]?: never}
|
|
32
|
+
& Record<string, any>;
|
|
33
|
+
|
|
34
|
+
export type EvaluationOptions = {
|
|
35
|
+
clientId?: string,
|
|
36
|
+
clientIp?: string,
|
|
37
|
+
userAgent?: string,
|
|
38
|
+
userToken?: Token|string,
|
|
39
|
+
timeout?: number,
|
|
40
|
+
context?: EvaluationContext,
|
|
41
|
+
extra?: ExtraFetchOptions,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export enum EvaluationErrorType {
|
|
45
|
+
TIMEOUT = 'https://croct.help/api/evaluation#timeout',
|
|
46
|
+
UNEXPECTED_ERROR = 'https://croct.help/api/evaluation#unexpected-error',
|
|
47
|
+
INVALID_QUERY = 'https://croct.help/api/evaluation#invalid-query',
|
|
48
|
+
TOO_COMPLEX_QUERY = 'https://croct.help/api/evaluation#too-complex-query',
|
|
49
|
+
EVALUATION_FAILED = 'https://croct.help/api/evaluation#evaluation-failed',
|
|
50
|
+
UNALLOWED_RESULT = 'https://croct.help/api/evaluation#unallowed-result',
|
|
51
|
+
UNSERIALIZABLE_RESULT = 'https://croct.help/api/evaluation#unserializable-result',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ErrorResponse = {
|
|
55
|
+
type: EvaluationErrorType,
|
|
56
|
+
title: string,
|
|
57
|
+
status: number,
|
|
58
|
+
detail?: string,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export class EvaluationError<T extends ErrorResponse = ErrorResponse> extends Error {
|
|
62
|
+
public readonly response: T;
|
|
63
|
+
|
|
64
|
+
public constructor(response: T) {
|
|
65
|
+
super(response.title);
|
|
66
|
+
|
|
67
|
+
this.response = response;
|
|
68
|
+
|
|
69
|
+
Object.setPrototypeOf(this, EvaluationError.prototype);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type QueryErrorDetail = {
|
|
74
|
+
cause: string,
|
|
75
|
+
location: Location,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type QueryErrorResponse = ErrorResponse & {
|
|
79
|
+
errors: QueryErrorDetail[],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export class QueryError extends EvaluationError<QueryErrorResponse> {
|
|
83
|
+
public constructor(response: QueryErrorResponse) {
|
|
84
|
+
super(response);
|
|
85
|
+
|
|
86
|
+
Object.setPrototypeOf(this, QueryError.prototype);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type Configuration = {
|
|
91
|
+
appId?: string,
|
|
92
|
+
apiKey?: string,
|
|
93
|
+
endpointUrl?: string,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export class Evaluator {
|
|
97
|
+
public static readonly MAX_QUERY_LENGTH = MAX_QUERY_LENGTH;
|
|
98
|
+
|
|
99
|
+
private readonly configuration: Configuration;
|
|
100
|
+
|
|
101
|
+
private readonly endpoint: string;
|
|
102
|
+
|
|
103
|
+
public constructor(configuration: Configuration) {
|
|
104
|
+
if ((configuration.appId === undefined) === (configuration.apiKey === undefined)) {
|
|
105
|
+
throw new Error('Either the application ID or the API key must be provided.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const {endpointUrl, apiKey} = configuration;
|
|
109
|
+
|
|
110
|
+
// eslint-disable-next-line prefer-template -- Better readability
|
|
111
|
+
this.endpoint = (endpointUrl ?? EVALUATION_ENDPOINT_URL).replace(/\/+$/, '')
|
|
112
|
+
+ (apiKey === undefined ? '/client' : '/external')
|
|
113
|
+
+ '/web/evaluate';
|
|
114
|
+
|
|
115
|
+
this.configuration = configuration;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public evaluate(query: string, options: EvaluationOptions = {}): Promise<JsonValue> {
|
|
119
|
+
const length = getLength(query);
|
|
120
|
+
|
|
121
|
+
if (length > Evaluator.MAX_QUERY_LENGTH) {
|
|
122
|
+
const response: QueryErrorResponse = {
|
|
123
|
+
title: 'The query is too complex.',
|
|
124
|
+
status: 422, // Unprocessable Entity
|
|
125
|
+
type: EvaluationErrorType.TOO_COMPLEX_QUERY,
|
|
126
|
+
detail: `The query must be at most ${Evaluator.MAX_QUERY_LENGTH} characters long, `
|
|
127
|
+
+ `but it is ${length} characters long.`,
|
|
128
|
+
errors: [{
|
|
129
|
+
cause: 'The query is longer than expected.',
|
|
130
|
+
location: getLocation(query, 0, Math.max(length - 1, 0)),
|
|
131
|
+
}],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
return Promise.reject(new QueryError(response));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const body: JsonObject = {
|
|
138
|
+
query: query,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (options.context !== undefined) {
|
|
142
|
+
body.context = options.context;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
const abortController = new AbortController();
|
|
147
|
+
|
|
148
|
+
if (options.timeout !== undefined) {
|
|
149
|
+
setTimeout(
|
|
150
|
+
() => {
|
|
151
|
+
const response: ErrorResponse = {
|
|
152
|
+
title: 'Maximum evaluation timeout reached before evaluation could complete.',
|
|
153
|
+
type: EvaluationErrorType.TIMEOUT,
|
|
154
|
+
detail: `The evaluation took more than ${options.timeout}ms to complete.`,
|
|
155
|
+
status: 408, // Request Timeout
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
abortController.abort();
|
|
159
|
+
|
|
160
|
+
reject(new EvaluationError(response));
|
|
161
|
+
},
|
|
162
|
+
options.timeout,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const promise = this.fetch(body, abortController.signal, options);
|
|
167
|
+
|
|
168
|
+
promise.then(
|
|
169
|
+
response => response.json()
|
|
170
|
+
.then(data => {
|
|
171
|
+
if (response.ok) {
|
|
172
|
+
return resolve(data);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const errorResponse: ErrorResponse = data;
|
|
176
|
+
|
|
177
|
+
switch (errorResponse.type) {
|
|
178
|
+
case EvaluationErrorType.INVALID_QUERY:
|
|
179
|
+
case EvaluationErrorType.EVALUATION_FAILED:
|
|
180
|
+
case EvaluationErrorType.TOO_COMPLEX_QUERY:
|
|
181
|
+
reject(new QueryError(errorResponse as QueryErrorResponse));
|
|
182
|
+
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
default:
|
|
186
|
+
reject(new EvaluationError(errorResponse));
|
|
187
|
+
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}),
|
|
191
|
+
)
|
|
192
|
+
.catch(
|
|
193
|
+
error => {
|
|
194
|
+
if (!abortController.signal.aborted) {
|
|
195
|
+
reject(
|
|
196
|
+
new EvaluationError({
|
|
197
|
+
title: formatMessage(error),
|
|
198
|
+
type: EvaluationErrorType.UNEXPECTED_ERROR,
|
|
199
|
+
detail: 'Please try again or contact Croct support if the error persists.',
|
|
200
|
+
status: 500, // Internal Server Error
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private fetch(body: JsonObject, signal: AbortSignal, options: EvaluationOptions): Promise<Response> {
|
|
210
|
+
const {appId, apiKey} = this.configuration;
|
|
211
|
+
const {clientId, clientIp, userAgent, userToken} = options;
|
|
212
|
+
|
|
213
|
+
const headers = new Headers();
|
|
214
|
+
|
|
215
|
+
if (apiKey !== undefined) {
|
|
216
|
+
headers.set('X-Api-Key', apiKey);
|
|
217
|
+
} else if (appId !== undefined) {
|
|
218
|
+
headers.set('X-App-Id', appId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (clientId !== undefined) {
|
|
222
|
+
headers.set('X-Client-Id', clientId);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (clientIp !== undefined) {
|
|
226
|
+
headers.set('X-Client-Ip', clientIp);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (userToken !== undefined) {
|
|
230
|
+
headers.set('X-Token', userToken.toString());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (userAgent !== undefined) {
|
|
234
|
+
headers.set('User-Agent', userAgent);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return fetch(this.endpoint, {
|
|
238
|
+
credentials: 'omit',
|
|
239
|
+
...options.extra,
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: headers,
|
|
242
|
+
signal: signal,
|
|
243
|
+
body: JSON.stringify(body),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
public toJSON(): never {
|
|
248
|
+
// Prevent sensitive configuration from being serialized
|
|
249
|
+
throw new Error('Unserializable value.');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface EventListener<T> {
|
|
2
|
+
(event: T): void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type EventMap = Record<string, Record<string, any>>;
|
|
6
|
+
|
|
7
|
+
export interface EventDispatcher<TEvents extends EventMap> {
|
|
8
|
+
dispatch<T extends keyof TEvents>(eventName: T, event: TEvents[T]): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface EventSubscriber<TEvents extends EventMap> {
|
|
12
|
+
addListener<T extends keyof TEvents>(eventName: T, listener: EventListener<TEvents[T]>): void;
|
|
13
|
+
|
|
14
|
+
removeListener<T extends keyof TEvents>(eventName: T, listener: EventListener<TEvents[T]>): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EventManager<DEvents extends EventMap, SEvents extends EventMap = DEvents>
|
|
18
|
+
extends EventDispatcher<DEvents>, EventSubscriber<SEvents> {
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class SynchronousEventManager<TEvents extends EventMap> implements EventManager<TEvents> {
|
|
22
|
+
private readonly listeners: {[type in keyof TEvents]?: Array<EventListener<TEvents[type]>>} = {};
|
|
23
|
+
|
|
24
|
+
public addListener<T extends keyof TEvents>(type: T, listener: EventListener<TEvents[T]>): void {
|
|
25
|
+
const listeners: Array<EventListener<TEvents[T]>> = this.listeners[type] ?? [];
|
|
26
|
+
|
|
27
|
+
listeners.push(listener);
|
|
28
|
+
|
|
29
|
+
this.listeners[type] = listeners;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public removeListener<T extends keyof TEvents>(eventName: T, listener: EventListener<TEvents[T]>): void {
|
|
33
|
+
const listeners = this.listeners[eventName];
|
|
34
|
+
|
|
35
|
+
if (listeners === undefined) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const index = listeners.indexOf(listener);
|
|
40
|
+
|
|
41
|
+
if (index >= 0) {
|
|
42
|
+
listeners.splice(index, 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public dispatch<T extends keyof TEvents>(eventName: T, event: TEvents[T]): void {
|
|
47
|
+
const listeners = this.listeners[eventName];
|
|
48
|
+
|
|
49
|
+
if (listeners !== undefined) {
|
|
50
|
+
listeners.forEach(listener => listener(event));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {JsonObject} from '@croct/json';
|
|
2
|
+
import {formatCause} from '../error';
|
|
3
|
+
import {ContentFetcher, FetchResponse} from '../contentFetcher';
|
|
4
|
+
import {ContextFactory} from './evaluatorFacade';
|
|
5
|
+
import {fetchOptionsSchema as optionsSchema} from '../schema';
|
|
6
|
+
import {TokenProvider} from '../token';
|
|
7
|
+
import {CidAssigner} from '../cid';
|
|
8
|
+
|
|
9
|
+
export type FetchOptions = {
|
|
10
|
+
version?: `${number}`|number,
|
|
11
|
+
preferredLocale?: string,
|
|
12
|
+
timeout?: number,
|
|
13
|
+
attributes?: JsonObject,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function validate(options: unknown): asserts options is FetchOptions {
|
|
17
|
+
try {
|
|
18
|
+
optionsSchema.validate(options);
|
|
19
|
+
} catch (violation) {
|
|
20
|
+
throw new Error(`Invalid options: ${formatCause(violation)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Configuration = {
|
|
25
|
+
contentFetcher: ContentFetcher,
|
|
26
|
+
contextFactory: ContextFactory,
|
|
27
|
+
previewTokenProvider: TokenProvider,
|
|
28
|
+
userTokenProvider: TokenProvider,
|
|
29
|
+
cidAssigner: CidAssigner,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export class ContentFetcherFacade {
|
|
33
|
+
private readonly fetcher: ContentFetcher;
|
|
34
|
+
|
|
35
|
+
private readonly contextFactory: ContextFactory;
|
|
36
|
+
|
|
37
|
+
private readonly previewTokenProvider: TokenProvider;
|
|
38
|
+
|
|
39
|
+
private readonly userTokenProvider: TokenProvider;
|
|
40
|
+
|
|
41
|
+
private readonly cidAssigner: CidAssigner;
|
|
42
|
+
|
|
43
|
+
public constructor(configuration: Configuration) {
|
|
44
|
+
this.fetcher = configuration.contentFetcher;
|
|
45
|
+
this.previewTokenProvider = configuration.previewTokenProvider;
|
|
46
|
+
this.userTokenProvider = configuration.userTokenProvider;
|
|
47
|
+
this.cidAssigner = configuration.cidAssigner;
|
|
48
|
+
this.contextFactory = configuration.contextFactory;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public async fetch<P extends JsonObject>(slotId: string, options: FetchOptions = {}): Promise<FetchResponse<P>> {
|
|
52
|
+
if (typeof slotId !== 'string' || slotId.length === 0) {
|
|
53
|
+
throw new Error('The slot ID must be a non-empty string.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
validate(options);
|
|
57
|
+
|
|
58
|
+
return this.fetcher.fetch(slotId, {
|
|
59
|
+
static: false,
|
|
60
|
+
clientId: await this.cidAssigner.assignCid(),
|
|
61
|
+
userToken: this.userTokenProvider.getToken() ?? undefined,
|
|
62
|
+
previewToken: this.previewTokenProvider.getToken() ?? undefined,
|
|
63
|
+
version: options.version,
|
|
64
|
+
preferredLocale: options.preferredLocale,
|
|
65
|
+
context: this.contextFactory.createContext(options.attributes),
|
|
66
|
+
timeout: options.timeout,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {JsonObject, JsonValue} from '@croct/json';
|
|
2
|
+
import {Evaluator, Campaign, EvaluationContext, Page} from '../evaluator';
|
|
3
|
+
import {Tab} from '../tab';
|
|
4
|
+
import {evaluationOptionsSchema as optionsSchema} from '../schema';
|
|
5
|
+
import {formatCause} from '../error';
|
|
6
|
+
import {TokenProvider} from '../token';
|
|
7
|
+
import {CidAssigner} from '../cid';
|
|
8
|
+
|
|
9
|
+
export type EvaluationOptions = {
|
|
10
|
+
timeout?: number,
|
|
11
|
+
attributes?: JsonObject,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function validate(options: unknown): asserts options is EvaluationOptions {
|
|
15
|
+
try {
|
|
16
|
+
optionsSchema.validate(options);
|
|
17
|
+
} catch (violation) {
|
|
18
|
+
throw new Error(`Invalid options: ${formatCause(violation)}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ContextFactory {
|
|
23
|
+
createContext(attributes?: JsonObject): EvaluationContext;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type Configuration = {
|
|
27
|
+
evaluator: Evaluator,
|
|
28
|
+
contextFactory: ContextFactory,
|
|
29
|
+
userTokenProvider: TokenProvider,
|
|
30
|
+
cidAssigner: CidAssigner,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class EvaluatorFacade {
|
|
34
|
+
private readonly evaluator: Evaluator;
|
|
35
|
+
|
|
36
|
+
private readonly contextFactory: ContextFactory;
|
|
37
|
+
|
|
38
|
+
private readonly tokenProvider: TokenProvider;
|
|
39
|
+
|
|
40
|
+
private readonly cidAssigner: CidAssigner;
|
|
41
|
+
|
|
42
|
+
public constructor(configuration: Configuration) {
|
|
43
|
+
this.evaluator = configuration.evaluator;
|
|
44
|
+
this.contextFactory = configuration.contextFactory;
|
|
45
|
+
this.tokenProvider = configuration.userTokenProvider;
|
|
46
|
+
this.cidAssigner = configuration.cidAssigner;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async evaluate(query: string, options: EvaluationOptions = {}): Promise<JsonValue> {
|
|
50
|
+
if (typeof query !== 'string' || query.length === 0) {
|
|
51
|
+
throw new Error('The query must be a non-empty string.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
validate(options);
|
|
55
|
+
|
|
56
|
+
return this.evaluator.evaluate(query, {
|
|
57
|
+
clientId: await this.cidAssigner.assignCid(),
|
|
58
|
+
userToken: this.tokenProvider.getToken() ?? undefined,
|
|
59
|
+
timeout: options.timeout,
|
|
60
|
+
context: this.contextFactory.createContext(options.attributes),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class MinimalContextFactory implements ContextFactory {
|
|
66
|
+
public createContext(attributes?: JsonObject): EvaluationContext {
|
|
67
|
+
if (attributes === undefined) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {attributes: attributes};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class TabContextFactory implements ContextFactory {
|
|
76
|
+
private readonly tab: Tab;
|
|
77
|
+
|
|
78
|
+
public constructor(tab: Tab) {
|
|
79
|
+
this.tab = tab;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public createContext(attributes?: JsonObject): EvaluationContext {
|
|
83
|
+
const url = new URL(this.tab.url);
|
|
84
|
+
const context: EvaluationContext = {};
|
|
85
|
+
|
|
86
|
+
const page: Page = {
|
|
87
|
+
title: this.tab.title,
|
|
88
|
+
url: url.toString(),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const {referrer} = this.tab;
|
|
92
|
+
|
|
93
|
+
if (referrer.length > 0) {
|
|
94
|
+
page.referrer = referrer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
context.page = page;
|
|
98
|
+
|
|
99
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? null;
|
|
100
|
+
|
|
101
|
+
if (timeZone !== null) {
|
|
102
|
+
context.timeZone = timeZone;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const campaign = TabContextFactory.createCampaign(url);
|
|
106
|
+
|
|
107
|
+
if (Object.keys(campaign).length > 0) {
|
|
108
|
+
context.campaign = campaign;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (attributes !== undefined && Object.keys(attributes).length > 0) {
|
|
112
|
+
context.attributes = attributes;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return context;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private static createCampaign(url: URL): Campaign {
|
|
119
|
+
const campaign: Campaign = {};
|
|
120
|
+
|
|
121
|
+
for (const [parameter, value] of url.searchParams.entries()) {
|
|
122
|
+
switch (parameter.toLowerCase()) {
|
|
123
|
+
case 'utm_campaign':
|
|
124
|
+
campaign.name = value;
|
|
125
|
+
|
|
126
|
+
break;
|
|
127
|
+
|
|
128
|
+
case 'utm_source':
|
|
129
|
+
campaign.source = value;
|
|
130
|
+
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'utm_term':
|
|
134
|
+
campaign.term = value;
|
|
135
|
+
|
|
136
|
+
break;
|
|
137
|
+
|
|
138
|
+
case 'utm_medium':
|
|
139
|
+
campaign.medium = value;
|
|
140
|
+
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
case 'utm_content':
|
|
144
|
+
campaign.content = value;
|
|
145
|
+
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return campaign;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export {EvaluatorFacade, ContextFactory, MinimalContextFactory, TabContextFactory} from './evaluatorFacade';
|
|
2
|
+
export {SdkFacade} from './sdkFacade';
|
|
3
|
+
export {SessionFacade} from './sessionFacade';
|
|
4
|
+
export {SessionPatch} from './sessionPatch';
|
|
5
|
+
export {TrackerFacade} from './trackerFacade';
|
|
6
|
+
export {UserFacade} from './userFacade';
|
|
7
|
+
export {UserPatch} from './userPatch';
|