@peopl-health/nexus 5.44.0-dev.6210 → 5.44.0-dev.6227

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.
@@ -9,7 +9,8 @@ const PHI_CATEGORIES = Object.freeze(['PERSONA', 'TELEFONO', 'EMAIL', 'ID']);
9
9
  const tokenMapSchema = new mongoose.Schema({
10
10
  code: { type: String, required: true },
11
11
  encode: { type: Map, of: String, default: () => new Map() },
12
- decode: { type: Map, of: String, default: () => new Map() }
12
+ decode: { type: Map, of: String, default: () => new Map() },
13
+ seq: { type: Number }
13
14
  }, { timestamps: true });
14
15
 
15
16
  tokenMapSchema.index({ code: 1 }, { unique: true });
@@ -24,12 +24,23 @@ const preferredSurface = (current, candidate) => {
24
24
  return richer > 0 ? candidate : current;
25
25
  };
26
26
 
27
- const tokenCache = new MapCache({ maxSize: 500, ttl: 60 * 1000 });
27
+ const CLAIM_ATTEMPTS = 3;
28
+
29
+ const NEXT_INDEX_PIPELINE = [{
30
+ $set: {
31
+ seq: {
32
+ $add: [
33
+ { $ifNull: ['$seq', { $size: { $objectToArray: { $ifNull: ['$encode', {}] } } }] },
34
+ 1,
35
+ ],
36
+ },
37
+ },
38
+ }];
28
39
 
29
- const getSubmap = async (code) => {
30
- const cached = tokenCache.get(code);
31
- if (cached) return cached;
40
+ const tokenCache = new MapCache({ maxSize: 500, ttl: 60 * 1000 });
41
+ const inFlight = new Map();
32
42
 
43
+ const readSubmap = async (code) => {
33
44
  const doc = await getTokenMap().findOne({ code }).lean();
34
45
  const submap = {
35
46
  encode: new Map(Object.entries(doc?.encode || {})),
@@ -39,32 +50,34 @@ const getSubmap = async (code) => {
39
50
  return submap;
40
51
  };
41
52
 
42
- const allocateToken = async (code, plaintext, category, normalized, submap) => {
43
- const counter = submap.encode.size + 1;
44
- const token = `<<${category}_${counter}>>`;
53
+ const getSubmap = async (code) => {
54
+ const cached = tokenCache.get(code);
55
+ if (cached) return cached;
56
+ if (!inFlight.has(code)) {
57
+ inFlight.set(code, readSubmap(code).finally(() => inFlight.delete(code)));
58
+ }
59
+ return inFlight.get(code);
60
+ };
45
61
 
46
- submap.encode.set(normalized, token);
47
- submap.decode.set(token, plaintext);
62
+ const nextIndex = async (code) => {
63
+ const doc = await getTokenMap().findOneAndUpdate({ code }, NEXT_INDEX_PIPELINE, {
64
+ upsert: true, new: true, projection: { seq: 1 }, lean: true,
65
+ });
66
+ return doc.seq;
67
+ };
48
68
 
49
- const res = await getTokenMap().updateOne(
50
- { code },
51
- {
52
- $set: {
53
- encode: Object.fromEntries(submap.encode),
54
- decode: Object.fromEntries(submap.decode),
55
- }
56
- },
57
- { upsert: true }
58
- );
59
- if (!res || (res.modifiedCount === 0 && !res.upsertedCount)) {
60
- logger.error('[allocateToken] Mapping write modified 0 docs', { code, category, token });
61
- throw new Error(`[allocateToken] Mapping write modified 0 docs for token ${token}`);
62
- }
63
- return token;
69
+ const rememberSurface = (code, token, plaintext) => {
70
+ const live = tokenCache.get(code);
71
+ if (live && plaintext !== undefined) live.decode.set(token, plaintext);
72
+ };
73
+
74
+ const rememberMapping = (code, normalized, token, plaintext) => {
75
+ const live = tokenCache.get(code);
76
+ if (live) live.encode.set(normalized, token);
77
+ rememberSurface(code, token, plaintext);
64
78
  };
65
79
 
66
- const upgradeSurface = async (code, token, plaintext, submap) => {
67
- const current = submap.decode.get(token);
80
+ const upgradeSurface = async (code, token, current, plaintext) => {
68
81
  const preferred = preferredSurface(current, plaintext);
69
82
  if (preferred === current) return;
70
83
 
@@ -75,13 +88,55 @@ const upgradeSurface = async (code, token, plaintext, submap) => {
75
88
  );
76
89
 
77
90
  if (res?.modifiedCount) {
78
- submap.decode.set(token, preferred);
91
+ rememberSurface(code, token, preferred);
79
92
  logger.debug('[upgradeSurface] Decode surface upgraded', { code, token });
80
93
  return;
81
94
  }
82
95
  tokenCache.delete(code);
83
96
  };
84
97
 
98
+ const claimToken = async (code, plaintext, category, normalized) => {
99
+ const token = `<<${category}_${await nextIndex(code)}>>`;
100
+ const res = await getTokenMap().updateOne(
101
+ {
102
+ code,
103
+ [`encode.${normalized}`]: { $exists: false },
104
+ [`decode.${token}`]: { $exists: false },
105
+ },
106
+ { $set: { [`encode.${normalized}`]: token, [`decode.${token}`]: plaintext } },
107
+ );
108
+ return res?.modifiedCount ? token : null;
109
+ };
110
+
111
+ const adoptClaimedToken = async (code, plaintext, normalized) => {
112
+ const doc = await getTokenMap().findOne({ code }).lean();
113
+ const winner = doc?.encode?.[normalized];
114
+ if (!winner) return null;
115
+
116
+ rememberMapping(code, normalized, winner, doc.decode?.[winner]);
117
+ await upgradeSurface(code, winner, doc.decode?.[winner], plaintext);
118
+ return winner;
119
+ };
120
+
121
+ const allocateToken = async (code, plaintext, category, normalized) => {
122
+ for (let attempt = 1; attempt <= CLAIM_ATTEMPTS; attempt += 1) {
123
+ let token = null;
124
+ try {
125
+ token = await claimToken(code, plaintext, category, normalized);
126
+ } catch (err) {
127
+ if (err?.code !== 11000 || attempt === CLAIM_ATTEMPTS) throw err;
128
+ }
129
+ if (token) {
130
+ rememberMapping(code, normalized, token, plaintext);
131
+ return token;
132
+ }
133
+ const winner = await adoptClaimedToken(code, plaintext, normalized);
134
+ if (winner) return winner;
135
+ logger.error('[allocateToken] Token claim failed, retrying', { code, category, attempt });
136
+ }
137
+ throw new Error(`[allocateToken] Could not claim a ${category} token for ${code}`);
138
+ };
139
+
85
140
  const encode = async (code, plaintext, category) => {
86
141
  if (!PHI_CATEGORIES.includes(category)) {
87
142
  throw new Error(`[encode] Unknown category: ${category}`);
@@ -96,11 +151,11 @@ const encode = async (code, plaintext, category) => {
96
151
  const submap = await getSubmap(code);
97
152
  const existing = submap.encode.get(normalized);
98
153
  if (existing) {
99
- await upgradeSurface(code, existing, String(plaintext), submap);
154
+ await upgradeSurface(code, existing, submap.decode.get(existing), String(plaintext));
100
155
  return existing;
101
156
  }
102
157
 
103
- const token = await allocateToken(code, String(plaintext), category, normalized, submap);
158
+ const token = await allocateToken(code, String(plaintext), category, normalized);
104
159
  logger.debug('[encode] Allocated', { code, category, token });
105
160
  return token;
106
161
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.44.0-dev.6210",
3
+ "version": "5.44.0-dev.6227",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",