@naturalcycles/abba 1.27.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/abba.d.ts CHANGED
@@ -1,6 +1,5 @@
1
- import { Unsaved } from '@naturalcycles/js-lib';
2
- import { SegmentationData } from '.';
3
- import { AbbaConfig, Bucket, Experiment, ExperimentAssignmentStatistics, ExperimentWithBuckets, GeneratedUserAssignment } from './types';
1
+ import type { Unsaved } from '@naturalcycles/js-lib';
2
+ import type { AbbaConfig, Bucket, Experiment, ExperimentAssignmentStatistics, ExperimentWithBuckets, GeneratedUserAssignment, SegmentationData } from './types.js';
4
3
  export declare class Abba {
5
4
  cfg: AbbaConfig;
6
5
  private experimentDao;
package/dist/abba.js CHANGED
@@ -1,24 +1,21 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Abba = void 0;
4
- const tslib_1 = require("tslib");
5
- const js_lib_1 = require("@naturalcycles/js-lib");
6
- const nodejs_lib_1 = require("@naturalcycles/nodejs-lib");
7
- const bucket_dao_1 = require("./dao/bucket.dao");
8
- const experiment_dao_1 = require("./dao/experiment.dao");
9
- const userAssignment_dao_1 = require("./dao/userAssignment.dao");
10
- const types_1 = require("./types");
11
- const util_1 = require("./util");
1
+ import { __decorate } from "tslib";
2
+ import { _assert, _Memo, _shuffle, localTime, pMap } from '@naturalcycles/js-lib';
3
+ import { LRUMemoCache } from '@naturalcycles/nodejs-lib';
4
+ import { bucketDao } from './dao/bucket.dao.js';
5
+ import { experimentDao } from './dao/experiment.dao.js';
6
+ import { userAssignmentDao } from './dao/userAssignment.dao.js';
7
+ import { AssignmentStatus } from './types.js';
8
+ import { canGenerateNewAssignments, generateUserAssignmentData, getUserExclusionSet, validateTotalBucketRatio, } from './util.js';
12
9
  /**
13
10
  * 10 minutes
14
11
  */
15
12
  const CACHE_TTL = 600_000;
16
- class Abba {
13
+ export class Abba {
17
14
  constructor(cfg) {
18
15
  this.cfg = cfg;
19
- this.experimentDao = (0, experiment_dao_1.experimentDao)(this.cfg.db);
20
- this.bucketDao = (0, bucket_dao_1.bucketDao)(this.cfg.db);
21
- this.userAssignmentDao = (0, userAssignment_dao_1.userAssignmentDao)(this.cfg.db);
16
+ this.experimentDao = experimentDao(this.cfg.db);
17
+ this.bucketDao = bucketDao(this.cfg.db);
18
+ this.userAssignmentDao = userAssignmentDao(this.cfg.db);
22
19
  }
23
20
  /**
24
21
  * Returns all experiments.
@@ -50,8 +47,8 @@ class Abba {
50
47
  * Cold method.
51
48
  */
52
49
  async createExperiment(experiment, buckets) {
53
- if (experiment.status === types_1.AssignmentStatus.Active) {
54
- (0, util_1.validateTotalBucketRatio)(buckets);
50
+ if (experiment.status === AssignmentStatus.Active) {
51
+ validateTotalBucketRatio(buckets);
55
52
  }
56
53
  const created = await this.experimentDao.save(experiment);
57
54
  const createdbuckets = await this.bucketDao.saveBatch(buckets.map(b => ({ ...b, experimentId: created.id })));
@@ -66,11 +63,11 @@ class Abba {
66
63
  * Cold method.
67
64
  */
68
65
  async saveExperiment(experiment, buckets) {
69
- if (experiment.status === types_1.AssignmentStatus.Active) {
70
- (0, util_1.validateTotalBucketRatio)(buckets);
66
+ if (experiment.status === AssignmentStatus.Active) {
67
+ validateTotalBucketRatio(buckets);
71
68
  }
72
69
  const updatedExperiment = await this.experimentDao.save(experiment, { saveMethod: 'update' });
73
- const updatedBuckets = await (0, js_lib_1.pMap)(buckets, async (bucket) => {
70
+ const updatedBuckets = await pMap(buckets, async (bucket) => {
74
71
  return await this.bucketDao.save({ ...bucket, experimentId: updatedExperiment.id }, { saveMethod: bucket.id ? 'update' : undefined });
75
72
  });
76
73
  await this.updateExclusions(updatedExperiment.id, updatedExperiment.exclusions);
@@ -109,9 +106,9 @@ class Abba {
109
106
  */
110
107
  async deleteExperiment(experimentId) {
111
108
  const experiment = await this.experimentDao.requireById(experimentId);
112
- const hasBeenInactiveFor15Mins = experiment.status === types_1.AssignmentStatus.Inactive &&
113
- (0, js_lib_1.localTime)(experiment.updated).isOlderThan(15, 'minute');
114
- (0, js_lib_1._assert)(hasBeenInactiveFor15Mins, 'Experiment must be inactive for at least 15 minutes before deletion');
109
+ const hasBeenInactiveFor15Mins = experiment.status === AssignmentStatus.Inactive &&
110
+ localTime(experiment.updated).isOlderThan(15, 'minute');
111
+ _assert(hasBeenInactiveFor15Mins, 'Experiment must be inactive for at least 15 minutes before deletion');
115
112
  const userAssignmentDeleteQuery = this.userAssignmentDao
116
113
  .query()
117
114
  .filterEq('experimentId', experimentId);
@@ -132,9 +129,9 @@ class Abba {
132
129
  */
133
130
  async getUserAssignment(experimentKey, userId, existingOnly, segmentationData) {
134
131
  const experiment = await this.experimentDao.getOneBy('key', experimentKey);
135
- (0, js_lib_1._assert)(experiment, `Experiment does not exist: ${experimentKey}`);
132
+ _assert(experiment, `Experiment does not exist: ${experimentKey}`);
136
133
  // Inactive experiments should never return an assignment
137
- if (experiment.status === types_1.AssignmentStatus.Inactive) {
134
+ if (experiment.status === AssignmentStatus.Inactive) {
138
135
  return {
139
136
  experiment,
140
137
  assignment: null,
@@ -156,23 +153,23 @@ class Abba {
156
153
  };
157
154
  }
158
155
  // No existing assignment, but we don't want to generate a new one
159
- if (existingOnly || experiment.status === types_1.AssignmentStatus.Paused) {
156
+ if (existingOnly || experiment.status === AssignmentStatus.Paused) {
160
157
  return {
161
158
  experiment,
162
159
  assignment: null,
163
160
  };
164
161
  }
165
162
  const experiments = await this.getAllExperiments();
166
- const exclusionSet = (0, util_1.getUserExclusionSet)(experiments, existingAssignments);
167
- if (!(0, util_1.canGenerateNewAssignments)(experiment, exclusionSet)) {
163
+ const exclusionSet = getUserExclusionSet(experiments, existingAssignments);
164
+ if (!canGenerateNewAssignments(experiment, exclusionSet)) {
168
165
  return {
169
166
  experiment,
170
167
  assignment: null,
171
168
  };
172
169
  }
173
- (0, js_lib_1._assert)(segmentationData, 'Segmentation data required when creating a new assignment');
170
+ _assert(segmentationData, 'Segmentation data required when creating a new assignment');
174
171
  const experimentWithBuckets = { ...experiment, buckets };
175
- const assignment = (0, util_1.generateUserAssignmentData)(experimentWithBuckets, userId, segmentationData);
172
+ const assignment = generateUserAssignmentData(experimentWithBuckets, userId, segmentationData);
176
173
  if (!assignment) {
177
174
  return {
178
175
  experiment,
@@ -199,7 +196,7 @@ class Abba {
199
196
  */
200
197
  async getAllExistingGeneratedUserAssignments(userId) {
201
198
  const assignments = await this.userAssignmentDao.getBy('userId', userId);
202
- return await (0, js_lib_1.pMap)(assignments, async (assignment) => {
199
+ return await pMap(assignments, async (assignment) => {
203
200
  const experiment = await this.experimentDao.requireById(assignment.experimentId);
204
201
  const bucket = await this.bucketDao.getById(assignment.bucketId);
205
202
  return {
@@ -221,13 +218,13 @@ class Abba {
221
218
  async generateUserAssignments(userId, segmentationData, existingOnly = false) {
222
219
  const experiments = await this.getAllExperiments();
223
220
  const existingAssignments = await this.userAssignmentDao.getBy('userId', userId);
224
- const exclusionSet = (0, util_1.getUserExclusionSet)(experiments, existingAssignments);
221
+ const exclusionSet = getUserExclusionSet(experiments, existingAssignments);
225
222
  const assignments = [];
226
223
  const newAssignments = [];
227
224
  // Shuffling means that randomisation occurs in the mutual exclusion
228
225
  // as experiments are looped through sequentially, this removes the risk of the same experiment always being assigned first in the list of mutually exclusive experiments
229
226
  // This is simmpler than trying to resolve after assignments have already been determined
230
- const availableExperiments = (0, js_lib_1._shuffle)(experiments.filter(e => e.status === types_1.AssignmentStatus.Active || e.status === types_1.AssignmentStatus.Paused));
227
+ const availableExperiments = _shuffle(experiments.filter(e => e.status === AssignmentStatus.Active || e.status === AssignmentStatus.Paused));
231
228
  for (const experiment of availableExperiments) {
232
229
  const existing = existingAssignments.find(ua => ua.experimentId === experiment.id);
233
230
  if (existing) {
@@ -242,8 +239,8 @@ class Abba {
242
239
  },
243
240
  });
244
241
  }
245
- else if (!existingOnly && (0, util_1.canGenerateNewAssignments)(experiment, exclusionSet)) {
246
- const assignment = (0, util_1.generateUserAssignmentData)(experiment, userId, segmentationData);
242
+ else if (!existingOnly && canGenerateNewAssignments(experiment, exclusionSet)) {
243
+ const assignment = generateUserAssignmentData(experiment, userId, segmentationData);
247
244
  if (assignment) {
248
245
  const created = this.userAssignmentDao.create(assignment);
249
246
  newAssignments.push(created);
@@ -275,7 +272,7 @@ class Abba {
275
272
  .filterEq('experimentId', experimentId)
276
273
  .runQueryCount();
277
274
  const buckets = await this.bucketDao.getBy('experimentId', experimentId);
278
- const bucketAssignments = await (0, js_lib_1.pMap)(buckets, async (bucket) => {
275
+ const bucketAssignments = await pMap(buckets, async (bucket) => {
279
276
  const totalAssignments = await this.userAssignmentDao
280
277
  .query()
281
278
  .filterEq('bucketId', bucket.id)
@@ -291,7 +288,6 @@ class Abba {
291
288
  };
292
289
  }
293
290
  }
294
- exports.Abba = Abba;
295
- tslib_1.__decorate([
296
- (0, js_lib_1._Memo)({ cacheFactory: () => new nodejs_lib_1.LRUMemoCache({ ttl: CACHE_TTL, max: 1 }) })
291
+ __decorate([
292
+ _Memo({ cacheFactory: () => new LRUMemoCache({ ttl: CACHE_TTL, max: 1 }) })
297
293
  ], Abba.prototype, "getAllExperiments", null);
@@ -1,6 +1,7 @@
1
- import { CommonDao, CommonDB } from '@naturalcycles/db-lib';
2
- import { Saved } from '@naturalcycles/js-lib';
3
- import { BaseBucket, Bucket } from '../types';
1
+ import type { CommonDB } from '@naturalcycles/db-lib';
2
+ import { CommonDao } from '@naturalcycles/db-lib';
3
+ import type { Saved } from '@naturalcycles/js-lib';
4
+ import type { BaseBucket, Bucket } from '../types.js';
4
5
  type BucketDBM = Saved<BaseBucket> & {
5
6
  data: string | null;
6
7
  };
@@ -1,11 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.bucketDao = exports.BucketDao = void 0;
4
- const db_lib_1 = require("@naturalcycles/db-lib");
5
- class BucketDao extends db_lib_1.CommonDao {
1
+ import { CommonDao } from '@naturalcycles/db-lib';
2
+ export class BucketDao extends CommonDao {
6
3
  }
7
- exports.BucketDao = BucketDao;
8
- const bucketDao = (db) => new BucketDao({
4
+ export const bucketDao = (db) => new BucketDao({
9
5
  db,
10
6
  table: 'Bucket',
11
7
  hooks: {
@@ -21,4 +17,3 @@ const bucketDao = (db) => new BucketDao({
21
17
  }),
22
18
  },
23
19
  });
24
- exports.bucketDao = bucketDao;
@@ -1,6 +1,7 @@
1
- import { CommonDao, CommonDB } from '@naturalcycles/db-lib';
2
- import { Saved } from '@naturalcycles/js-lib';
3
- import { BaseExperiment, Experiment } from '../types';
1
+ import type { CommonDB } from '@naturalcycles/db-lib';
2
+ import { CommonDao } from '@naturalcycles/db-lib';
3
+ import type { Saved } from '@naturalcycles/js-lib';
4
+ import type { BaseExperiment, Experiment } from '../types.js';
4
5
  type ExperimentDBM = Saved<BaseExperiment> & {
5
6
  rules: string | null;
6
7
  exclusions: string | null;
@@ -1,12 +1,8 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.experimentDao = exports.ExperimentDao = void 0;
4
- const db_lib_1 = require("@naturalcycles/db-lib");
5
- const js_lib_1 = require("@naturalcycles/js-lib");
6
- class ExperimentDao extends db_lib_1.CommonDao {
1
+ import { CommonDao } from '@naturalcycles/db-lib';
2
+ import { localDate } from '@naturalcycles/js-lib';
3
+ export class ExperimentDao extends CommonDao {
7
4
  }
8
- exports.ExperimentDao = ExperimentDao;
9
- const experimentDao = (db) => new ExperimentDao({
5
+ export const experimentDao = (db) => new ExperimentDao({
10
6
  db,
11
7
  table: 'Experiment',
12
8
  hooks: {
@@ -34,7 +30,6 @@ const experimentDao = (db) => new ExperimentDao({
34
30
  }),
35
31
  },
36
32
  });
37
- exports.experimentDao = experimentDao;
38
33
  /**
39
34
  * https://nc1.slack.com/archives/CCNTHJT7V/p1682514277002739
40
35
  * MySQL Automatically parses Date fields as Date objects
@@ -43,6 +38,6 @@ exports.experimentDao = experimentDao;
43
38
  function parseMySQLDate(date) {
44
39
  // @ts-expect-error
45
40
  if (date instanceof Date)
46
- return (0, js_lib_1.localDate)(date).toISODate();
41
+ return localDate(date).toISODate();
47
42
  return date;
48
43
  }
@@ -1,5 +1,6 @@
1
- import { CommonDao, CommonDB } from '@naturalcycles/db-lib';
2
- import { UserAssignment } from '../types';
1
+ import type { CommonDB } from '@naturalcycles/db-lib';
2
+ import { CommonDao } from '@naturalcycles/db-lib';
3
+ import type { UserAssignment } from '../types.js';
3
4
  export declare class UserAssignmentDao extends CommonDao<UserAssignment> {
4
5
  }
5
6
  export declare const userAssignmentDao: (db: CommonDB) => UserAssignmentDao;
@@ -1,12 +1,7 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.userAssignmentDao = exports.UserAssignmentDao = void 0;
4
- const db_lib_1 = require("@naturalcycles/db-lib");
5
- class UserAssignmentDao extends db_lib_1.CommonDao {
1
+ import { CommonDao } from '@naturalcycles/db-lib';
2
+ export class UserAssignmentDao extends CommonDao {
6
3
  }
7
- exports.UserAssignmentDao = UserAssignmentDao;
8
- const userAssignmentDao = (db) => new UserAssignmentDao({
4
+ export const userAssignmentDao = (db) => new UserAssignmentDao({
9
5
  db,
10
6
  table: 'UserAssignment',
11
7
  });
12
- exports.userAssignmentDao = userAssignmentDao;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from './abba';
2
- export * from './types';
1
+ export * from './abba.js';
2
+ export * from './types.js';
package/dist/index.js CHANGED
@@ -1,5 +1,2 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const tslib_1 = require("tslib");
4
- tslib_1.__exportStar(require("./abba"), exports);
5
- tslib_1.__exportStar(require("./types"), exports);
1
+ export * from './abba.js';
2
+ export * from './types.js';
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { CommonDB } from '@naturalcycles/db-lib';
2
- import { AnyObject, BaseDBEntity, IsoDate, Saved } from '@naturalcycles/js-lib';
1
+ import type { CommonDB } from '@naturalcycles/db-lib';
2
+ import type { AnyObject, BaseDBEntity, IsoDate, Saved } from '@naturalcycles/js-lib';
3
3
  export interface AbbaConfig {
4
4
  db: CommonDB;
5
5
  }
package/dist/types.js CHANGED
@@ -1,7 +1,4 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SegmentationRuleOperator = exports.AssignmentStatus = void 0;
4
- var AssignmentStatus;
1
+ export var AssignmentStatus;
5
2
  (function (AssignmentStatus) {
6
3
  /**
7
4
  * Will return existing assignments and generate new assignments
@@ -15,8 +12,8 @@ var AssignmentStatus;
15
12
  * Will not return any assignments
16
13
  */
17
14
  AssignmentStatus[AssignmentStatus["Inactive"] = 3] = "Inactive";
18
- })(AssignmentStatus || (exports.AssignmentStatus = AssignmentStatus = {}));
19
- var SegmentationRuleOperator;
15
+ })(AssignmentStatus || (AssignmentStatus = {}));
16
+ export var SegmentationRuleOperator;
20
17
  (function (SegmentationRuleOperator) {
21
18
  SegmentationRuleOperator["IsSet"] = "isSet";
22
19
  SegmentationRuleOperator["IsNotSet"] = "isNotSet";
@@ -26,4 +23,4 @@ var SegmentationRuleOperator;
26
23
  SegmentationRuleOperator["Regex"] = "regex";
27
24
  /* eslint-disable id-blacklist*/
28
25
  SegmentationRuleOperator["Boolean"] = "boolean";
29
- })(SegmentationRuleOperator || (exports.SegmentationRuleOperator = SegmentationRuleOperator = {}));
26
+ })(SegmentationRuleOperator || (SegmentationRuleOperator = {}));
package/dist/util.d.ts CHANGED
@@ -1,14 +1,18 @@
1
- import { Unsaved } from '@naturalcycles/js-lib';
2
- import { Bucket, ExclusionSet, Experiment, ExperimentWithBuckets, SegmentationData, SegmentationRule, SegmentationRuleFn, SegmentationRuleOperator, UserAssignment } from './types';
1
+ import type { Unsaved } from '@naturalcycles/js-lib';
2
+ import type { Bucket, ExclusionSet, Experiment, ExperimentWithBuckets, SegmentationData, SegmentationRule, SegmentationRuleFn, UserAssignment } from './types.js';
3
+ import { SegmentationRuleOperator } from './types.js';
3
4
  /**
4
5
  * Generate a new assignment for a given user.
5
6
  * Doesn't save it.
6
7
  */
7
8
  export declare const generateUserAssignmentData: (experiment: ExperimentWithBuckets, userId: string, segmentationData: SegmentationData) => UserAssignment | null;
8
- /**
9
- * Generate a random number between 0 and 100
10
- */
11
- export declare const rollDie: () => number;
9
+ declare class RandomService {
10
+ /**
11
+ * Generate a random number between 0 and 100
12
+ */
13
+ rollDie(): number;
14
+ }
15
+ export declare const randomService: RandomService;
12
16
  /**
13
17
  * Determines a users assignment for this experiment. Returns null if they are not considered to be in the sampling group
14
18
  */
@@ -42,3 +46,4 @@ export declare const canGenerateNewAssignments: (experiment: Experiment, exclusi
42
46
  * based on a combination of existing assignments and mutual exclusion configuration
43
47
  */
44
48
  export declare const getUserExclusionSet: (experiments: Experiment[], existingAssignments: UserAssignment[]) => ExclusionSet;
49
+ export {};
package/dist/util.js CHANGED
@@ -1,49 +1,46 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getUserExclusionSet = exports.canGenerateNewAssignments = exports.segmentationRuleMap = exports.validateSegmentationRules = exports.validateTotalBucketRatio = exports.determineBucket = exports.determineAssignment = exports.rollDie = exports.generateUserAssignmentData = void 0;
4
- const js_lib_1 = require("@naturalcycles/js-lib");
5
- const semver_1 = require("semver");
6
- const types_1 = require("./types");
1
+ import { localDate } from '@naturalcycles/js-lib';
2
+ import { satisfies } from 'semver';
3
+ import { AssignmentStatus, SegmentationRuleOperator } from './types.js';
7
4
  /**
8
5
  * Generate a new assignment for a given user.
9
6
  * Doesn't save it.
10
7
  */
11
- const generateUserAssignmentData = (experiment, userId, segmentationData) => {
12
- const segmentationMatch = (0, exports.validateSegmentationRules)(experiment.rules, segmentationData);
8
+ export const generateUserAssignmentData = (experiment, userId, segmentationData) => {
9
+ const segmentationMatch = validateSegmentationRules(experiment.rules, segmentationData);
13
10
  if (!segmentationMatch)
14
11
  return null;
15
- const bucket = (0, exports.determineAssignment)(experiment.sampling, experiment.buckets);
12
+ const bucket = determineAssignment(experiment.sampling, experiment.buckets);
16
13
  return {
17
14
  userId,
18
15
  experimentId: experiment.id,
19
16
  bucketId: bucket?.id || null,
20
17
  };
21
18
  };
22
- exports.generateUserAssignmentData = generateUserAssignmentData;
23
- /**
24
- * Generate a random number between 0 and 100
25
- */
26
- const rollDie = () => {
27
- return Math.random() * 100;
28
- };
29
- exports.rollDie = rollDie;
19
+ class RandomService {
20
+ /**
21
+ * Generate a random number between 0 and 100
22
+ */
23
+ rollDie() {
24
+ return Math.random() * 100;
25
+ }
26
+ }
27
+ export const randomService = new RandomService();
30
28
  /**
31
29
  * Determines a users assignment for this experiment. Returns null if they are not considered to be in the sampling group
32
30
  */
33
- const determineAssignment = (sampling, buckets) => {
31
+ export const determineAssignment = (sampling, buckets) => {
34
32
  // Should this person be considered for the experiment?
35
- if ((0, exports.rollDie)() > sampling) {
33
+ if (randomService.rollDie() > sampling) {
36
34
  return null;
37
35
  }
38
36
  // get their bucket
39
- return (0, exports.determineBucket)(buckets);
37
+ return determineBucket(buckets);
40
38
  };
41
- exports.determineAssignment = determineAssignment;
42
39
  /**
43
40
  * Determines which bucket a user assignment will recieve
44
41
  */
45
- const determineBucket = (buckets) => {
46
- const bucketRoll = (0, exports.rollDie)();
42
+ export const determineBucket = (buckets) => {
43
+ const bucketRoll = randomService.rollDie();
47
44
  let range;
48
45
  const bucket = buckets.find(b => {
49
46
  if (!range) {
@@ -61,17 +58,15 @@ const determineBucket = (buckets) => {
61
58
  }
62
59
  return bucket;
63
60
  };
64
- exports.determineBucket = determineBucket;
65
61
  /**
66
62
  * Validate the total ratio of the buckets equals 100
67
63
  */
68
- const validateTotalBucketRatio = (buckets) => {
64
+ export const validateTotalBucketRatio = (buckets) => {
69
65
  const bucketSum = buckets.reduce((sum, current) => sum + current.ratio, 0);
70
66
  if (bucketSum !== 100) {
71
67
  throw new Error('Total bucket ratio must be 100 before you can activate an experiment');
72
68
  }
73
69
  };
74
- exports.validateTotalBucketRatio = validateTotalBucketRatio;
75
70
  /**
76
71
  * Validate a users segmentation data against multiple rules. Returns false if any fail
77
72
  *
@@ -79,38 +74,37 @@ exports.validateTotalBucketRatio = validateTotalBucketRatio;
79
74
  * @param segmentationData
80
75
  * @returns
81
76
  */
82
- const validateSegmentationRules = (rules, segmentationData) => {
77
+ export const validateSegmentationRules = (rules, segmentationData) => {
83
78
  for (const rule of rules) {
84
79
  const { key, value, operator } = rule;
85
- if (!exports.segmentationRuleMap[operator](segmentationData[key], value))
80
+ if (!segmentationRuleMap[operator](segmentationData[key], value))
86
81
  return false;
87
82
  }
88
83
  return true;
89
84
  };
90
- exports.validateSegmentationRules = validateSegmentationRules;
91
85
  /**
92
86
  * Map of segmentation rule validators
93
87
  */
94
- exports.segmentationRuleMap = {
95
- [types_1.SegmentationRuleOperator.IsSet](keyValue) {
88
+ export const segmentationRuleMap = {
89
+ [SegmentationRuleOperator.IsSet](keyValue) {
96
90
  return !!keyValue;
97
91
  },
98
- [types_1.SegmentationRuleOperator.IsNotSet](keyValue) {
92
+ [SegmentationRuleOperator.IsNotSet](keyValue) {
99
93
  return !keyValue;
100
94
  },
101
- [types_1.SegmentationRuleOperator.EqualsText](keyValue, ruleValue) {
95
+ [SegmentationRuleOperator.EqualsText](keyValue, ruleValue) {
102
96
  return keyValue?.toString() === ruleValue.toString();
103
97
  },
104
- [types_1.SegmentationRuleOperator.NotEqualsText](keyValue, ruleValue) {
98
+ [SegmentationRuleOperator.NotEqualsText](keyValue, ruleValue) {
105
99
  return keyValue?.toString() !== ruleValue.toString();
106
100
  },
107
- [types_1.SegmentationRuleOperator.Semver](keyValue, ruleValue) {
108
- return (0, semver_1.satisfies)(keyValue?.toString() || '', ruleValue.toString());
101
+ [SegmentationRuleOperator.Semver](keyValue, ruleValue) {
102
+ return satisfies(keyValue?.toString() || '', ruleValue.toString());
109
103
  },
110
- [types_1.SegmentationRuleOperator.Regex](keyValue, ruleValue) {
104
+ [SegmentationRuleOperator.Regex](keyValue, ruleValue) {
111
105
  return new RegExp(ruleValue).test(keyValue?.toString() || '');
112
106
  },
113
- [types_1.SegmentationRuleOperator.Boolean](keyValue, ruleValue) {
107
+ [SegmentationRuleOperator.Boolean](keyValue, ruleValue) {
114
108
  // If it's true, then must be true
115
109
  if (ruleValue === 'true')
116
110
  return keyValue?.toString() === 'true';
@@ -121,17 +115,16 @@ exports.segmentationRuleMap = {
121
115
  /**
122
116
  * Returns true if an experiment is able to generate new assignments based on status and start/end dates
123
117
  */
124
- const canGenerateNewAssignments = (experiment, exclusionSet) => {
118
+ export const canGenerateNewAssignments = (experiment, exclusionSet) => {
125
119
  return (!exclusionSet.has(experiment.id) &&
126
- experiment.status === types_1.AssignmentStatus.Active &&
127
- js_lib_1.localDate.today().isBetween(experiment.startDateIncl, experiment.endDateExcl, '[)'));
120
+ experiment.status === AssignmentStatus.Active &&
121
+ localDate.today().isBetween(experiment.startDateIncl, experiment.endDateExcl, '[)'));
128
122
  };
129
- exports.canGenerateNewAssignments = canGenerateNewAssignments;
130
123
  /**
131
124
  * Returns an object that includes keys of all experimentIds a user should not be assigned to
132
125
  * based on a combination of existing assignments and mutual exclusion configuration
133
126
  */
134
- const getUserExclusionSet = (experiments, existingAssignments) => {
127
+ export const getUserExclusionSet = (experiments, existingAssignments) => {
135
128
  const exclusionSet = new Set();
136
129
  existingAssignments.forEach(assignment => {
137
130
  // Users who are excluded from an experiment due to sampling
@@ -143,4 +136,3 @@ const getUserExclusionSet = (experiments, existingAssignments) => {
143
136
  });
144
137
  return exclusionSet;
145
138
  };
146
- exports.getUserExclusionSet = getUserExclusionSet;
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@naturalcycles/abba",
3
- "version": "1.27.0",
3
+ "type": "module",
4
+ "version": "2.0.0",
4
5
  "scripts": {
5
6
  "prepare": "husky",
6
7
  "build": "dev-lib build",
@@ -10,16 +11,18 @@
10
11
  "lbt": "dev-lib lbt"
11
12
  },
12
13
  "dependencies": {
13
- "@naturalcycles/db-lib": "^9.14.1",
14
- "@naturalcycles/js-lib": "^14.237.0",
15
- "@naturalcycles/nodejs-lib": "^13.1.3",
16
- "semver": "^7.3.5"
14
+ "@naturalcycles/db-lib": "^10",
15
+ "@naturalcycles/js-lib": "^14",
16
+ "@naturalcycles/nodejs-lib": "^13",
17
+ "semver": "^7"
17
18
  },
18
19
  "devDependencies": {
19
- "@naturalcycles/dev-lib": "^15.19.0",
20
- "@types/node": "^22.7.4",
21
- "@types/semver": "^7.3.9",
22
- "jest": "^29.3.1"
20
+ "@naturalcycles/dev-lib": "^17",
21
+ "@types/node": "^22",
22
+ "@types/semver": "^7",
23
+ "@vitest/coverage-v8": "^3",
24
+ "tsx": "^4",
25
+ "vitest": "^3"
23
26
  },
24
27
  "files": [
25
28
  "dist",
package/src/abba.ts CHANGED
@@ -1,26 +1,27 @@
1
- import { _assert, _Memo, _shuffle, localTime, pMap, Unsaved } from '@naturalcycles/js-lib'
1
+ import type { Unsaved } from '@naturalcycles/js-lib'
2
+ import { _assert, _Memo, _shuffle, localTime, pMap } from '@naturalcycles/js-lib'
2
3
  import { LRUMemoCache } from '@naturalcycles/nodejs-lib'
3
- import { SegmentationData } from '.'
4
- import { bucketDao } from './dao/bucket.dao'
5
- import { experimentDao } from './dao/experiment.dao'
6
- import { userAssignmentDao } from './dao/userAssignment.dao'
7
- import {
4
+ import { bucketDao } from './dao/bucket.dao.js'
5
+ import { experimentDao } from './dao/experiment.dao.js'
6
+ import { userAssignmentDao } from './dao/userAssignment.dao.js'
7
+ import type {
8
8
  AbbaConfig,
9
- AssignmentStatus,
10
9
  Bucket,
11
10
  BucketAssignmentStatistics,
12
11
  Experiment,
13
12
  ExperimentAssignmentStatistics,
14
13
  ExperimentWithBuckets,
15
14
  GeneratedUserAssignment,
15
+ SegmentationData,
16
16
  UserAssignment,
17
- } from './types'
17
+ } from './types.js'
18
+ import { AssignmentStatus } from './types.js'
18
19
  import {
19
20
  canGenerateNewAssignments,
20
21
  generateUserAssignmentData,
21
22
  getUserExclusionSet,
22
23
  validateTotalBucketRatio,
23
- } from './util'
24
+ } from './util.js'
24
25
 
25
26
  /**
26
27
  * 10 minutes
@@ -1,6 +1,7 @@
1
- import { CommonDao, CommonDB } from '@naturalcycles/db-lib'
2
- import { Saved } from '@naturalcycles/js-lib'
3
- import { BaseBucket, Bucket } from '../types'
1
+ import type { CommonDB } from '@naturalcycles/db-lib'
2
+ import { CommonDao } from '@naturalcycles/db-lib'
3
+ import type { Saved } from '@naturalcycles/js-lib'
4
+ import type { BaseBucket, Bucket } from '../types.js'
4
5
 
5
6
  type BucketDBM = Saved<BaseBucket> & {
6
7
  data: string | null
@@ -1,6 +1,8 @@
1
- import { CommonDao, CommonDB } from '@naturalcycles/db-lib'
2
- import { IsoDate, localDate, Saved } from '@naturalcycles/js-lib'
3
- import { BaseExperiment, Experiment } from '../types'
1
+ import type { CommonDB } from '@naturalcycles/db-lib'
2
+ import { CommonDao } from '@naturalcycles/db-lib'
3
+ import type { IsoDate, Saved } from '@naturalcycles/js-lib'
4
+ import { localDate } from '@naturalcycles/js-lib'
5
+ import type { BaseExperiment, Experiment } from '../types.js'
4
6
 
5
7
  type ExperimentDBM = Saved<BaseExperiment> & {
6
8
  rules: string | null
@@ -1,5 +1,6 @@
1
- import { CommonDao, CommonDB } from '@naturalcycles/db-lib'
2
- import { UserAssignment } from '../types'
1
+ import type { CommonDB } from '@naturalcycles/db-lib'
2
+ import { CommonDao } from '@naturalcycles/db-lib'
3
+ import type { UserAssignment } from '../types.js'
3
4
 
4
5
  export class UserAssignmentDao extends CommonDao<UserAssignment> {}
5
6
 
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from './abba'
2
- export * from './types'
1
+ export * from './abba.js'
2
+ export * from './types.js'
package/src/types.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { CommonDB } from '@naturalcycles/db-lib'
2
- import { AnyObject, BaseDBEntity, IsoDate, Saved } from '@naturalcycles/js-lib'
1
+ import type { CommonDB } from '@naturalcycles/db-lib'
2
+ import type { AnyObject, BaseDBEntity, IsoDate, Saved } from '@naturalcycles/js-lib'
3
3
 
4
4
  export interface AbbaConfig {
5
5
  db: CommonDB
package/src/util.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { localDate, Unsaved } from '@naturalcycles/js-lib'
1
+ import type { Unsaved } from '@naturalcycles/js-lib'
2
+ import { localDate } from '@naturalcycles/js-lib'
2
3
  import { satisfies } from 'semver'
3
- import {
4
- AssignmentStatus,
4
+ import type {
5
5
  Bucket,
6
6
  ExclusionSet,
7
7
  Experiment,
@@ -9,9 +9,9 @@ import {
9
9
  SegmentationData,
10
10
  SegmentationRule,
11
11
  SegmentationRuleFn,
12
- SegmentationRuleOperator,
13
12
  UserAssignment,
14
- } from './types'
13
+ } from './types.js'
14
+ import { AssignmentStatus, SegmentationRuleOperator } from './types.js'
15
15
 
16
16
  /**
17
17
  * Generate a new assignment for a given user.
@@ -34,19 +34,23 @@ export const generateUserAssignmentData = (
34
34
  } as UserAssignment
35
35
  }
36
36
 
37
- /**
38
- * Generate a random number between 0 and 100
39
- */
40
- export const rollDie = (): number => {
41
- return Math.random() * 100
37
+ class RandomService {
38
+ /**
39
+ * Generate a random number between 0 and 100
40
+ */
41
+ rollDie(): number {
42
+ return Math.random() * 100
43
+ }
42
44
  }
43
45
 
46
+ export const randomService = new RandomService()
47
+
44
48
  /**
45
49
  * Determines a users assignment for this experiment. Returns null if they are not considered to be in the sampling group
46
50
  */
47
51
  export const determineAssignment = (sampling: number, buckets: Bucket[]): Bucket | null => {
48
52
  // Should this person be considered for the experiment?
49
- if (rollDie() > sampling) {
53
+ if (randomService.rollDie() > sampling) {
50
54
  return null
51
55
  }
52
56
 
@@ -58,7 +62,7 @@ export const determineAssignment = (sampling: number, buckets: Bucket[]): Bucket
58
62
  * Determines which bucket a user assignment will recieve
59
63
  */
60
64
  export const determineBucket = (buckets: Bucket[]): Bucket => {
61
- const bucketRoll = rollDie()
65
+ const bucketRoll = randomService.rollDie()
62
66
  let range: [number, number] | undefined
63
67
  const bucket = buckets.find(b => {
64
68
  if (!range) {