@ninetailed/experience.js 7.12.1 → 7.13.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.cjs.js +171 -8
- package/index.esm.js +181 -12
- package/package.json +3 -3
- package/src/index.d.ts +1 -0
- package/src/lib/Ninetailed.d.ts +3 -1
- package/src/lib/experience/index.d.ts +1 -0
- package/src/lib/experience/makeChangesModificationMiddleware.d.ts +31 -0
- package/src/lib/experience/types/index.d.ts +1 -1
- package/src/lib/guards/hasChangesModificationMiddleware.d.ts +8 -0
- package/src/lib/plugins/selectPluginsHavingChangesModificationMiddleware.d.ts +9 -0
- package/src/lib/types/ProfileChangedPayload.d.ts +2 -1
- package/src/lib/types/interfaces/HasChangesModificationMiddleware.d.ts +30 -0
package/index.cjs.js
CHANGED
|
@@ -143,7 +143,6 @@ class NinetailedCorePlugin extends experience_jsPluginAnalytics.NinetailedAnalyt
|
|
|
143
143
|
}));
|
|
144
144
|
});
|
|
145
145
|
this.methods = {
|
|
146
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
147
146
|
reset: (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
148
147
|
experience_jsShared.logger.debug('Resetting profile.');
|
|
149
148
|
const instance = args[args.length - 1];
|
|
@@ -162,7 +161,6 @@ class NinetailedCorePlugin extends experience_jsPluginAnalytics.NinetailedAnalyt
|
|
|
162
161
|
experience_jsShared.logger.info('Profile reset successful.');
|
|
163
162
|
yield delay(10);
|
|
164
163
|
}),
|
|
165
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
164
|
debug: (...args) => __awaiter(this, void 0, void 0, function* () {
|
|
167
165
|
const enabled = args[0];
|
|
168
166
|
const instance = args[args.length - 1];
|
|
@@ -288,7 +286,6 @@ class NinetailedCorePlugin extends experience_jsPluginAnalytics.NinetailedAnalyt
|
|
|
288
286
|
onTrackComponent() {
|
|
289
287
|
return Promise.resolve();
|
|
290
288
|
}
|
|
291
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
292
289
|
setItemStart({
|
|
293
290
|
abort,
|
|
294
291
|
payload
|
|
@@ -562,7 +559,7 @@ const selectPluginsHavingExperienceSelectionMiddleware = plugins => {
|
|
|
562
559
|
return filteredPlugins;
|
|
563
560
|
};
|
|
564
561
|
|
|
565
|
-
const createPassThroughMiddleware = () => {
|
|
562
|
+
const createPassThroughMiddleware$1 = () => {
|
|
566
563
|
return ({
|
|
567
564
|
experience,
|
|
568
565
|
variant,
|
|
@@ -582,7 +579,7 @@ function createExperienceSelectionMiddleware({
|
|
|
582
579
|
profile
|
|
583
580
|
}) {
|
|
584
581
|
if (profile === null) {
|
|
585
|
-
return createPassThroughMiddleware();
|
|
582
|
+
return createPassThroughMiddleware$1();
|
|
586
583
|
}
|
|
587
584
|
const pluginsWithMiddleware = selectPluginsHavingExperienceSelectionMiddleware(plugins);
|
|
588
585
|
const middlewareFunctions = [];
|
|
@@ -634,6 +631,110 @@ const makeExperienceSelectMiddleware = ({
|
|
|
634
631
|
};
|
|
635
632
|
};
|
|
636
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Type guard that checks if an object implements the HasChangesModificationMiddleware interface
|
|
636
|
+
*
|
|
637
|
+
* @param arg Object to check
|
|
638
|
+
* @returns Boolean indicating if the object implements HasChangesModificationMiddleware
|
|
639
|
+
*/
|
|
640
|
+
const hasChangesModificationMiddleware = arg => {
|
|
641
|
+
return typeof arg === 'object' && arg !== null && 'getChangesModificationMiddleware' in arg && typeof arg.getChangesModificationMiddleware === 'function';
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Selects plugins that implement the HasChangesModificationMiddleware interface
|
|
646
|
+
*
|
|
647
|
+
* @param plugins Array of Ninetailed plugins
|
|
648
|
+
* @returns Array of plugins that implement HasChangesModificationMiddleware
|
|
649
|
+
*/
|
|
650
|
+
const selectPluginsHavingChangesModificationMiddleware = plugins => {
|
|
651
|
+
const filteredPlugins = [];
|
|
652
|
+
for (const plugin of plugins) {
|
|
653
|
+
if (hasChangesModificationMiddleware(plugin)) {
|
|
654
|
+
filteredPlugins.push(plugin);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return filteredPlugins;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Creates a pass-through middleware that doesn't modify changes
|
|
662
|
+
*/
|
|
663
|
+
const createPassThroughMiddleware = () => {
|
|
664
|
+
return ({
|
|
665
|
+
changes
|
|
666
|
+
}) => {
|
|
667
|
+
return {
|
|
668
|
+
changes
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
/**
|
|
673
|
+
* Creates a middleware function by composing middleware from multiple plugins
|
|
674
|
+
*/
|
|
675
|
+
function createChangesModificationMiddleware({
|
|
676
|
+
plugins,
|
|
677
|
+
changes
|
|
678
|
+
}) {
|
|
679
|
+
const pluginsWithMiddleware = selectPluginsHavingChangesModificationMiddleware(plugins);
|
|
680
|
+
const middlewareFunctions = [];
|
|
681
|
+
for (const plugin of pluginsWithMiddleware) {
|
|
682
|
+
const middleware = plugin.getChangesModificationMiddleware({
|
|
683
|
+
changes
|
|
684
|
+
});
|
|
685
|
+
if (middleware !== undefined) {
|
|
686
|
+
middlewareFunctions.push(middleware);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// If no middleware functions were found, return a pass-through middleware
|
|
690
|
+
if (middlewareFunctions.length === 0) {
|
|
691
|
+
return createPassThroughMiddleware();
|
|
692
|
+
}
|
|
693
|
+
// Compose middleware functions using pipe
|
|
694
|
+
return experience_jsShared.pipe(...middlewareFunctions);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Creates a changes modification middleware system with change notification
|
|
698
|
+
*
|
|
699
|
+
* This follows the same pattern as the experience selection middleware system
|
|
700
|
+
*/
|
|
701
|
+
const makeChangesModificationMiddleware = ({
|
|
702
|
+
plugins,
|
|
703
|
+
onChange,
|
|
704
|
+
changes
|
|
705
|
+
}) => {
|
|
706
|
+
let removeChangeListeners = [];
|
|
707
|
+
const pluginsHavingChangeEmitters = selectPluginsHavingOnChangeEmitter(plugins);
|
|
708
|
+
const middleware = createChangesModificationMiddleware({
|
|
709
|
+
plugins,
|
|
710
|
+
changes
|
|
711
|
+
});
|
|
712
|
+
const addListeners = () => {
|
|
713
|
+
removeChangeListeners = pluginsHavingChangeEmitters.map(plugin => {
|
|
714
|
+
const listener = () => {
|
|
715
|
+
// When a plugin changes, recreate the middleware and notify
|
|
716
|
+
const updatedMiddleware = createChangesModificationMiddleware({
|
|
717
|
+
plugins,
|
|
718
|
+
changes
|
|
719
|
+
});
|
|
720
|
+
onChange(updatedMiddleware);
|
|
721
|
+
};
|
|
722
|
+
return plugin.onChangeEmitter.addListener(listener);
|
|
723
|
+
});
|
|
724
|
+
};
|
|
725
|
+
// WARNING: This specific implementation using forEach is required.
|
|
726
|
+
// DO NOT replace with for...of or other loop constructs as they will break functionality.
|
|
727
|
+
// The exact reason is uncertain but appears related to the transpiler.
|
|
728
|
+
const removeListeners = () => {
|
|
729
|
+
removeChangeListeners.forEach(removeListener => removeListener());
|
|
730
|
+
};
|
|
731
|
+
return {
|
|
732
|
+
addListeners,
|
|
733
|
+
removeListeners,
|
|
734
|
+
middleware
|
|
735
|
+
};
|
|
736
|
+
};
|
|
737
|
+
|
|
637
738
|
class EventBuilder {
|
|
638
739
|
constructor(buildRequestContext) {
|
|
639
740
|
this.buildRequestContext = buildRequestContext || buildClientNinetailedRequestContext;
|
|
@@ -1043,15 +1144,77 @@ class Ninetailed {
|
|
|
1043
1144
|
};
|
|
1044
1145
|
/**
|
|
1045
1146
|
* Registers a callback to be notified when changes occur in the profile state.
|
|
1147
|
+
* Uses the changes modification middleware system to process changes.
|
|
1148
|
+
* Changes are processed in the following order:
|
|
1046
1149
|
*
|
|
1047
1150
|
* @param cb - Callback function that receives the changes state
|
|
1048
1151
|
* @returns Function to unsubscribe from changes updates
|
|
1049
1152
|
*/
|
|
1050
1153
|
this.onChangesChange = cb => {
|
|
1154
|
+
// Initially notify with current state
|
|
1051
1155
|
this.notifyChangesCallback(cb, this._profileState);
|
|
1052
|
-
|
|
1053
|
-
|
|
1156
|
+
let middlewareChangeListeners = [];
|
|
1157
|
+
const removeProfileChangeListener = this.onProfileChange(profileState => {
|
|
1158
|
+
// Clean up any existing middleware listeners
|
|
1159
|
+
middlewareChangeListeners.forEach(removeListener => removeListener());
|
|
1160
|
+
middlewareChangeListeners = [];
|
|
1161
|
+
// If we're in loading or error state, simply pass through
|
|
1162
|
+
if (profileState.status !== 'success') {
|
|
1163
|
+
this.notifyChangesCallback(cb, profileState);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
// Set up middleware for changes
|
|
1167
|
+
const {
|
|
1168
|
+
addListeners,
|
|
1169
|
+
removeListeners,
|
|
1170
|
+
middleware: changesModificationMiddleware
|
|
1171
|
+
} = makeChangesModificationMiddleware({
|
|
1172
|
+
plugins: this.plugins,
|
|
1173
|
+
changes: profileState.changes || [],
|
|
1174
|
+
onChange: updatedMiddleware => {
|
|
1175
|
+
// When plugin state changes, reapply middleware to current changes
|
|
1176
|
+
if (profileState.status === 'success' && profileState.changes) {
|
|
1177
|
+
const {
|
|
1178
|
+
changes: modifiedChanges
|
|
1179
|
+
} = updatedMiddleware({
|
|
1180
|
+
changes: profileState.changes
|
|
1181
|
+
});
|
|
1182
|
+
cb({
|
|
1183
|
+
status: 'success',
|
|
1184
|
+
changes: modifiedChanges,
|
|
1185
|
+
error: null
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
// Add listeners for plugin changes
|
|
1191
|
+
addListeners();
|
|
1192
|
+
middlewareChangeListeners.push(removeListeners);
|
|
1193
|
+
// Apply middleware to current changes
|
|
1194
|
+
if (profileState.changes) {
|
|
1195
|
+
const {
|
|
1196
|
+
changes: modifiedChanges
|
|
1197
|
+
} = changesModificationMiddleware({
|
|
1198
|
+
changes: profileState.changes
|
|
1199
|
+
});
|
|
1200
|
+
cb({
|
|
1201
|
+
status: 'success',
|
|
1202
|
+
changes: modifiedChanges,
|
|
1203
|
+
error: null
|
|
1204
|
+
});
|
|
1205
|
+
} else {
|
|
1206
|
+
cb({
|
|
1207
|
+
status: 'success',
|
|
1208
|
+
changes: [],
|
|
1209
|
+
error: null
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1054
1212
|
});
|
|
1213
|
+
// Return a function that cleans up all listeners
|
|
1214
|
+
return () => {
|
|
1215
|
+
removeProfileChangeListener();
|
|
1216
|
+
middlewareChangeListeners.forEach(removeListener => removeListener());
|
|
1217
|
+
};
|
|
1055
1218
|
};
|
|
1056
1219
|
this.onIsInitialized = onIsInitializedCallback => {
|
|
1057
1220
|
if (typeof onIsInitializedCallback === 'function') {
|
|
@@ -1189,7 +1352,6 @@ class Ninetailed {
|
|
|
1189
1352
|
}
|
|
1190
1353
|
logInvalidElement(element) {
|
|
1191
1354
|
const isObject = typeof element === 'object' && element !== null;
|
|
1192
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1193
1355
|
const constructorName = isObject ? element.constructor.name : '';
|
|
1194
1356
|
const isConstructorNameNotObject = constructorName && constructorName !== 'Object';
|
|
1195
1357
|
experience_jsShared.logger.warn(`ElementSeenObserver.observeElement was called with an invalid element. Expected an Element but got ${typeof element}${isConstructorNameNotObject ? ` of type ${constructorName}` : ''}. This call will be ignored.`);
|
|
@@ -1415,6 +1577,7 @@ exports.PROFILE_RESET = PROFILE_RESET;
|
|
|
1415
1577
|
exports.SET_ENABLED_FEATURES = SET_ENABLED_FEATURES;
|
|
1416
1578
|
exports.buildClientNinetailedRequestContext = buildClientNinetailedRequestContext;
|
|
1417
1579
|
exports.decodeExperienceVariantsMap = decodeExperienceVariantsMap;
|
|
1580
|
+
exports.makeChangesModificationMiddleware = makeChangesModificationMiddleware;
|
|
1418
1581
|
exports.makeExperienceSelectMiddleware = makeExperienceSelectMiddleware;
|
|
1419
1582
|
exports.selectPluginsHavingExperienceSelectionMiddleware = selectPluginsHavingExperienceSelectionMiddleware;
|
|
1420
1583
|
exports.selectPluginsHavingOnChangeEmitter = selectPluginsHavingOnChangeEmitter;
|
package/index.esm.js
CHANGED
|
@@ -62,9 +62,6 @@ const SET_ENABLED_FEATURES = 'setEnabledFeatures';
|
|
|
62
62
|
const EMPTY_MERGE_ID = 'nt:empty-merge-id';
|
|
63
63
|
|
|
64
64
|
const PLUGIN_NAME = 'ninetailed:core';
|
|
65
|
-
|
|
66
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
-
|
|
68
65
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
69
66
|
class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
|
|
70
67
|
constructor({
|
|
@@ -108,7 +105,6 @@ class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
|
|
|
108
105
|
}));
|
|
109
106
|
};
|
|
110
107
|
this.methods = {
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
112
108
|
reset: async function (...args) {
|
|
113
109
|
logger.debug('Resetting profile.');
|
|
114
110
|
const instance = args[args.length - 1];
|
|
@@ -127,7 +123,6 @@ class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
|
|
|
127
123
|
logger.info('Profile reset successful.');
|
|
128
124
|
await delay(10);
|
|
129
125
|
},
|
|
130
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
126
|
debug: async function (...args) {
|
|
132
127
|
const enabled = args[0];
|
|
133
128
|
const instance = args[args.length - 1];
|
|
@@ -244,7 +239,6 @@ class NinetailedCorePlugin extends NinetailedAnalyticsPlugin {
|
|
|
244
239
|
onTrackComponent() {
|
|
245
240
|
return Promise.resolve();
|
|
246
241
|
}
|
|
247
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
248
242
|
setItemStart({
|
|
249
243
|
abort,
|
|
250
244
|
payload
|
|
@@ -529,7 +523,7 @@ const selectPluginsHavingExperienceSelectionMiddleware = plugins => {
|
|
|
529
523
|
return filteredPlugins;
|
|
530
524
|
};
|
|
531
525
|
|
|
532
|
-
const createPassThroughMiddleware = () => {
|
|
526
|
+
const createPassThroughMiddleware$1 = () => {
|
|
533
527
|
return ({
|
|
534
528
|
experience,
|
|
535
529
|
variant,
|
|
@@ -549,7 +543,7 @@ function createExperienceSelectionMiddleware({
|
|
|
549
543
|
profile
|
|
550
544
|
}) {
|
|
551
545
|
if (profile === null) {
|
|
552
|
-
return createPassThroughMiddleware();
|
|
546
|
+
return createPassThroughMiddleware$1();
|
|
553
547
|
}
|
|
554
548
|
const pluginsWithMiddleware = selectPluginsHavingExperienceSelectionMiddleware(plugins);
|
|
555
549
|
const middlewareFunctions = [];
|
|
@@ -602,6 +596,115 @@ const makeExperienceSelectMiddleware = ({
|
|
|
602
596
|
};
|
|
603
597
|
};
|
|
604
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Type guard that checks if an object implements the HasChangesModificationMiddleware interface
|
|
601
|
+
*
|
|
602
|
+
* @param arg Object to check
|
|
603
|
+
* @returns Boolean indicating if the object implements HasChangesModificationMiddleware
|
|
604
|
+
*/
|
|
605
|
+
const hasChangesModificationMiddleware = arg => {
|
|
606
|
+
return typeof arg === 'object' && arg !== null && 'getChangesModificationMiddleware' in arg && typeof arg.getChangesModificationMiddleware === 'function';
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Selects plugins that implement the HasChangesModificationMiddleware interface
|
|
611
|
+
*
|
|
612
|
+
* @param plugins Array of Ninetailed plugins
|
|
613
|
+
* @returns Array of plugins that implement HasChangesModificationMiddleware
|
|
614
|
+
*/
|
|
615
|
+
const selectPluginsHavingChangesModificationMiddleware = plugins => {
|
|
616
|
+
const filteredPlugins = [];
|
|
617
|
+
for (const plugin of plugins) {
|
|
618
|
+
if (hasChangesModificationMiddleware(plugin)) {
|
|
619
|
+
filteredPlugins.push(plugin);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return filteredPlugins;
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Creates a pass-through middleware that doesn't modify changes
|
|
627
|
+
*/
|
|
628
|
+
const createPassThroughMiddleware = () => {
|
|
629
|
+
return ({
|
|
630
|
+
changes
|
|
631
|
+
}) => {
|
|
632
|
+
return {
|
|
633
|
+
changes
|
|
634
|
+
};
|
|
635
|
+
};
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Creates a middleware function by composing middleware from multiple plugins
|
|
640
|
+
*/
|
|
641
|
+
function createChangesModificationMiddleware({
|
|
642
|
+
plugins,
|
|
643
|
+
changes
|
|
644
|
+
}) {
|
|
645
|
+
const pluginsWithMiddleware = selectPluginsHavingChangesModificationMiddleware(plugins);
|
|
646
|
+
const middlewareFunctions = [];
|
|
647
|
+
for (const plugin of pluginsWithMiddleware) {
|
|
648
|
+
const middleware = plugin.getChangesModificationMiddleware({
|
|
649
|
+
changes
|
|
650
|
+
});
|
|
651
|
+
if (middleware !== undefined) {
|
|
652
|
+
middlewareFunctions.push(middleware);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// If no middleware functions were found, return a pass-through middleware
|
|
657
|
+
if (middlewareFunctions.length === 0) {
|
|
658
|
+
return createPassThroughMiddleware();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Compose middleware functions using pipe
|
|
662
|
+
return pipe(...middlewareFunctions);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Creates a changes modification middleware system with change notification
|
|
667
|
+
*
|
|
668
|
+
* This follows the same pattern as the experience selection middleware system
|
|
669
|
+
*/
|
|
670
|
+
const makeChangesModificationMiddleware = ({
|
|
671
|
+
plugins,
|
|
672
|
+
onChange,
|
|
673
|
+
changes
|
|
674
|
+
}) => {
|
|
675
|
+
let removeChangeListeners = [];
|
|
676
|
+
const pluginsHavingChangeEmitters = selectPluginsHavingOnChangeEmitter(plugins);
|
|
677
|
+
const middleware = createChangesModificationMiddleware({
|
|
678
|
+
plugins,
|
|
679
|
+
changes
|
|
680
|
+
});
|
|
681
|
+
const addListeners = () => {
|
|
682
|
+
removeChangeListeners = pluginsHavingChangeEmitters.map(plugin => {
|
|
683
|
+
const listener = () => {
|
|
684
|
+
// When a plugin changes, recreate the middleware and notify
|
|
685
|
+
const updatedMiddleware = createChangesModificationMiddleware({
|
|
686
|
+
plugins,
|
|
687
|
+
changes
|
|
688
|
+
});
|
|
689
|
+
onChange(updatedMiddleware);
|
|
690
|
+
};
|
|
691
|
+
return plugin.onChangeEmitter.addListener(listener);
|
|
692
|
+
});
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// WARNING: This specific implementation using forEach is required.
|
|
696
|
+
// DO NOT replace with for...of or other loop constructs as they will break functionality.
|
|
697
|
+
// The exact reason is uncertain but appears related to the transpiler.
|
|
698
|
+
const removeListeners = () => {
|
|
699
|
+
removeChangeListeners.forEach(removeListener => removeListener());
|
|
700
|
+
};
|
|
701
|
+
return {
|
|
702
|
+
addListeners,
|
|
703
|
+
removeListeners,
|
|
704
|
+
middleware
|
|
705
|
+
};
|
|
706
|
+
};
|
|
707
|
+
|
|
605
708
|
class EventBuilder {
|
|
606
709
|
constructor(buildRequestContext) {
|
|
607
710
|
this.buildRequestContext = void 0;
|
|
@@ -1029,15 +1132,82 @@ class Ninetailed {
|
|
|
1029
1132
|
};
|
|
1030
1133
|
/**
|
|
1031
1134
|
* Registers a callback to be notified when changes occur in the profile state.
|
|
1135
|
+
* Uses the changes modification middleware system to process changes.
|
|
1136
|
+
* Changes are processed in the following order:
|
|
1032
1137
|
*
|
|
1033
1138
|
* @param cb - Callback function that receives the changes state
|
|
1034
1139
|
* @returns Function to unsubscribe from changes updates
|
|
1035
1140
|
*/
|
|
1036
1141
|
this.onChangesChange = cb => {
|
|
1142
|
+
// Initially notify with current state
|
|
1037
1143
|
this.notifyChangesCallback(cb, this._profileState);
|
|
1038
|
-
|
|
1039
|
-
|
|
1144
|
+
let middlewareChangeListeners = [];
|
|
1145
|
+
const removeProfileChangeListener = this.onProfileChange(profileState => {
|
|
1146
|
+
// Clean up any existing middleware listeners
|
|
1147
|
+
middlewareChangeListeners.forEach(removeListener => removeListener());
|
|
1148
|
+
middlewareChangeListeners = [];
|
|
1149
|
+
|
|
1150
|
+
// If we're in loading or error state, simply pass through
|
|
1151
|
+
if (profileState.status !== 'success') {
|
|
1152
|
+
this.notifyChangesCallback(cb, profileState);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Set up middleware for changes
|
|
1157
|
+
const {
|
|
1158
|
+
addListeners,
|
|
1159
|
+
removeListeners,
|
|
1160
|
+
middleware: changesModificationMiddleware
|
|
1161
|
+
} = makeChangesModificationMiddleware({
|
|
1162
|
+
plugins: this.plugins,
|
|
1163
|
+
changes: profileState.changes || [],
|
|
1164
|
+
onChange: updatedMiddleware => {
|
|
1165
|
+
// When plugin state changes, reapply middleware to current changes
|
|
1166
|
+
if (profileState.status === 'success' && profileState.changes) {
|
|
1167
|
+
const {
|
|
1168
|
+
changes: modifiedChanges
|
|
1169
|
+
} = updatedMiddleware({
|
|
1170
|
+
changes: profileState.changes
|
|
1171
|
+
});
|
|
1172
|
+
cb({
|
|
1173
|
+
status: 'success',
|
|
1174
|
+
changes: modifiedChanges,
|
|
1175
|
+
error: null
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
// Add listeners for plugin changes
|
|
1182
|
+
addListeners();
|
|
1183
|
+
middlewareChangeListeners.push(removeListeners);
|
|
1184
|
+
|
|
1185
|
+
// Apply middleware to current changes
|
|
1186
|
+
if (profileState.changes) {
|
|
1187
|
+
const {
|
|
1188
|
+
changes: modifiedChanges
|
|
1189
|
+
} = changesModificationMiddleware({
|
|
1190
|
+
changes: profileState.changes
|
|
1191
|
+
});
|
|
1192
|
+
cb({
|
|
1193
|
+
status: 'success',
|
|
1194
|
+
changes: modifiedChanges,
|
|
1195
|
+
error: null
|
|
1196
|
+
});
|
|
1197
|
+
} else {
|
|
1198
|
+
cb({
|
|
1199
|
+
status: 'success',
|
|
1200
|
+
changes: [],
|
|
1201
|
+
error: null
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1040
1204
|
});
|
|
1205
|
+
|
|
1206
|
+
// Return a function that cleans up all listeners
|
|
1207
|
+
return () => {
|
|
1208
|
+
removeProfileChangeListener();
|
|
1209
|
+
middlewareChangeListeners.forEach(removeListener => removeListener());
|
|
1210
|
+
};
|
|
1041
1211
|
};
|
|
1042
1212
|
this.onIsInitialized = onIsInitializedCallback => {
|
|
1043
1213
|
if (typeof onIsInitializedCallback === 'function') {
|
|
@@ -1176,7 +1346,6 @@ class Ninetailed {
|
|
|
1176
1346
|
}
|
|
1177
1347
|
logInvalidElement(element) {
|
|
1178
1348
|
const isObject = typeof element === 'object' && element !== null;
|
|
1179
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1180
1349
|
const constructorName = isObject ? element.constructor.name : '';
|
|
1181
1350
|
const isConstructorNameNotObject = constructorName && constructorName !== 'Object';
|
|
1182
1351
|
logger.warn(`ElementSeenObserver.observeElement was called with an invalid element. Expected an Element but got ${typeof element}${isConstructorNameNotObject ? ` of type ${constructorName}` : ''}. This call will be ignored.`);
|
|
@@ -1350,4 +1519,4 @@ const selectVariant = (baseline, variants, {
|
|
|
1350
1519
|
};
|
|
1351
1520
|
};
|
|
1352
1521
|
|
|
1353
|
-
export { ANONYMOUS_ID, CHANGES_FALLBACK_CACHE, COMPONENT, COMPONENT_START, CONSENT, DEBUG_FLAG, EMPTY_MERGE_ID, EXPERIENCES_FALLBACK_CACHE, EventBuilder, HAS_SEEN_STICKY_COMPONENT, LEGACY_ANONYMOUS_ID, Ninetailed, NinetailedCorePlugin, OnChangeEmitter, PAGE_HIDDEN, PLUGIN_NAME, PROFILE_CHANGE, PROFILE_FALLBACK_CACHE, PROFILE_RESET, SET_ENABLED_FEATURES, buildClientNinetailedRequestContext, decodeExperienceVariantsMap, makeExperienceSelectMiddleware, selectPluginsHavingExperienceSelectionMiddleware, selectPluginsHavingOnChangeEmitter, selectVariant };
|
|
1522
|
+
export { ANONYMOUS_ID, CHANGES_FALLBACK_CACHE, COMPONENT, COMPONENT_START, CONSENT, DEBUG_FLAG, EMPTY_MERGE_ID, EXPERIENCES_FALLBACK_CACHE, EventBuilder, HAS_SEEN_STICKY_COMPONENT, LEGACY_ANONYMOUS_ID, Ninetailed, NinetailedCorePlugin, OnChangeEmitter, PAGE_HIDDEN, PLUGIN_NAME, PROFILE_CHANGE, PROFILE_FALLBACK_CACHE, PROFILE_RESET, SET_ENABLED_FEATURES, buildClientNinetailedRequestContext, decodeExperienceVariantsMap, makeChangesModificationMiddleware, makeExperienceSelectMiddleware, selectPluginsHavingExperienceSelectionMiddleware, selectPluginsHavingOnChangeEmitter, selectVariant };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ninetailed/experience.js",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.13.0-beta.1",
|
|
4
4
|
"description": "Ninetailed SDK for javascript",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"directory": "packages/sdks/javascript"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@ninetailed/experience.js-plugin-analytics": "7.
|
|
13
|
-
"@ninetailed/experience.js-shared": "7.
|
|
12
|
+
"@ninetailed/experience.js-plugin-analytics": "7.13.0-beta.1",
|
|
13
|
+
"@ninetailed/experience.js-shared": "7.13.0-beta.1",
|
|
14
14
|
"analytics": "0.8.1",
|
|
15
15
|
"uuid": "9.0.0"
|
|
16
16
|
},
|
package/src/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './lib/plugins/selectPluginsHavingExperienceSelectionMiddleware';
|
|
|
8
8
|
export * from './lib/plugins/selectPluginsHavingOnChangeEmitter';
|
|
9
9
|
export * from './lib/types/interfaces/RequiresEventBuilder';
|
|
10
10
|
export * from './lib/types/interfaces/HasExperienceSelectionMiddleware';
|
|
11
|
+
export * from './lib/types/interfaces/HasChangesModificationMiddleware';
|
|
11
12
|
export * from './lib/types/interfaces/InterestedInSeenElements';
|
|
12
13
|
export * from './lib/types/interfaces/InterestedInProfileChange';
|
|
13
14
|
export * from './lib/types/interfaces/InterestedInHiddenPage';
|
package/src/lib/Ninetailed.d.ts
CHANGED
|
@@ -82,11 +82,13 @@ export declare class Ninetailed implements NinetailedInstance {
|
|
|
82
82
|
onSelectVariant: <Baseline extends Reference, Variant extends Reference>({ baseline, experiences }: OnSelectVariantArgs<Baseline, Variant>, cb: OnSelectVariantCallback<Baseline, Variant>) => () => void;
|
|
83
83
|
/**
|
|
84
84
|
* Registers a callback to be notified when changes occur in the profile state.
|
|
85
|
+
* Uses the changes modification middleware system to process changes.
|
|
86
|
+
* Changes are processed in the following order:
|
|
85
87
|
*
|
|
86
88
|
* @param cb - Callback function that receives the changes state
|
|
87
89
|
* @returns Function to unsubscribe from changes updates
|
|
88
90
|
*/
|
|
89
|
-
onChangesChange: (cb: OnChangesChangeCallback) =>
|
|
91
|
+
onChangesChange: (cb: OnChangesChangeCallback) => () => void;
|
|
90
92
|
/**
|
|
91
93
|
* Helper method to extract changes state from profile state and notify callback
|
|
92
94
|
* @private
|
|
@@ -2,3 +2,4 @@ export * from './types';
|
|
|
2
2
|
export { EXPERIENCE_TRAIT_PREFIX, selectDistribution, isExperienceMatch, selectVariant as selectExperienceVariant, selectHasVariants as selectHasExperienceVariants, selectVariants as selectExperienceVariants, selectBaselineWithVariants as selectExperienceBaselineWithVariants, selectExperience, selectActiveExperiments, } from '@ninetailed/experience.js-shared';
|
|
3
3
|
export { decodeExperienceVariantsMap } from './decodeExperienceVariantsMap';
|
|
4
4
|
export { makeExperienceSelectMiddleware } from './makeExperienceSelectMiddleware';
|
|
5
|
+
export { makeChangesModificationMiddleware } from './makeChangesModificationMiddleware';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Change } from '@ninetailed/experience.js-shared';
|
|
2
|
+
import { NinetailedPlugin } from '@ninetailed/experience.js-plugin-analytics';
|
|
3
|
+
import { ChangesModificationMiddleware } from '../types/interfaces/HasChangesModificationMiddleware';
|
|
4
|
+
/**
|
|
5
|
+
* Arguments for creating a changes modification middleware
|
|
6
|
+
*/
|
|
7
|
+
type CreateChangesModificationMiddlewareArg = {
|
|
8
|
+
plugins: NinetailedPlugin[];
|
|
9
|
+
changes: Change[];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Arguments for making a changes modification middleware with change notification
|
|
13
|
+
*/
|
|
14
|
+
type MakeChangesModificationMiddlewareArg = CreateChangesModificationMiddlewareArg & {
|
|
15
|
+
onChange: (middleware: ChangesModificationMiddleware) => void;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Result of creating a changes modification middleware
|
|
19
|
+
*/
|
|
20
|
+
interface ChangesModificationMiddlewareResult {
|
|
21
|
+
addListeners: () => void;
|
|
22
|
+
removeListeners: () => void;
|
|
23
|
+
middleware: ChangesModificationMiddleware;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a changes modification middleware system with change notification
|
|
27
|
+
*
|
|
28
|
+
* This follows the same pattern as the experience selection middleware system
|
|
29
|
+
*/
|
|
30
|
+
export declare const makeChangesModificationMiddleware: ({ plugins, onChange, changes, }: MakeChangesModificationMiddlewareArg) => ChangesModificationMiddlewareResult;
|
|
31
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export type { Reference, Baseline, VariantRef, ExperienceConfiguration,
|
|
1
|
+
export type { Reference, Baseline, VariantRef, ExperienceConfiguration, EntryReplacement, InlineVariable, Distribution, ExperienceType, } from '@ninetailed/experience.js-shared';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { HasChangesModificationMiddleware } from '../types/interfaces/HasChangesModificationMiddleware';
|
|
2
|
+
/**
|
|
3
|
+
* Type guard that checks if an object implements the HasChangesModificationMiddleware interface
|
|
4
|
+
*
|
|
5
|
+
* @param arg Object to check
|
|
6
|
+
* @returns Boolean indicating if the object implements HasChangesModificationMiddleware
|
|
7
|
+
*/
|
|
8
|
+
export declare const hasChangesModificationMiddleware: (arg: unknown) => arg is HasChangesModificationMiddleware;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NinetailedPlugin } from '@ninetailed/experience.js-plugin-analytics';
|
|
2
|
+
import { HasChangesModificationMiddleware } from '../types/interfaces/HasChangesModificationMiddleware';
|
|
3
|
+
/**
|
|
4
|
+
* Selects plugins that implement the HasChangesModificationMiddleware interface
|
|
5
|
+
*
|
|
6
|
+
* @param plugins Array of Ninetailed plugins
|
|
7
|
+
* @returns Array of plugins that implement HasChangesModificationMiddleware
|
|
8
|
+
*/
|
|
9
|
+
export declare const selectPluginsHavingChangesModificationMiddleware: (plugins: NinetailedPlugin[]) => HasChangesModificationMiddleware[];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Change } from '@ninetailed/experience.js-shared';
|
|
2
|
+
/**
|
|
3
|
+
* Arguments passed to changes modification middleware
|
|
4
|
+
*/
|
|
5
|
+
export interface ChangesModificationMiddlewareArg {
|
|
6
|
+
changes: Change[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* A middleware function that can modify changes
|
|
10
|
+
*/
|
|
11
|
+
export type ChangesModificationMiddleware = (arg: ChangesModificationMiddlewareArg) => ChangesModificationMiddlewareArg;
|
|
12
|
+
/**
|
|
13
|
+
* Arguments for building a changes modification middleware
|
|
14
|
+
*/
|
|
15
|
+
export type BuildChangesModificationMiddlewareArg = {
|
|
16
|
+
changes: Change[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Type for a function that builds changes modification middleware
|
|
20
|
+
*/
|
|
21
|
+
export type BuildChangesModificationMiddleware = (arg: BuildChangesModificationMiddlewareArg) => ChangesModificationMiddleware | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Interface for plugins that can provide changes modification middleware
|
|
24
|
+
*/
|
|
25
|
+
export interface HasChangesModificationMiddleware {
|
|
26
|
+
/**
|
|
27
|
+
* Returns a middleware function that can modify changes
|
|
28
|
+
*/
|
|
29
|
+
getChangesModificationMiddleware: BuildChangesModificationMiddleware;
|
|
30
|
+
}
|