@drawbridge/drawbridge-utils 0.0.62 → 0.0.63

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/index.cjs CHANGED
@@ -393,28 +393,44 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
393
393
  });
394
394
  };
395
395
  var isDuplicateKey = (error) => (error == null ? void 0 : error.code) === 11e3 || (error == null ? void 0 : error.code) === "11000" || /E11000|duplicate key/i.test((error == null ? void 0 : error.message) || "");
396
- var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
396
+ var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
397
397
  if (!controller || !organization || !usageId || !type || !key) return;
398
398
  const amount = Number(units);
399
399
  if (!Number.isFinite(amount) || amount === 0) return;
400
+ let inserted = false;
400
401
  try {
401
- await controller.create({
402
+ const { lastErrorObject } = await controller.update({
402
403
  collection: "action",
403
404
  data: {
404
- organization,
405
- usage: usageId,
406
- type,
407
- units: amount,
405
+ $setOnInsert: {
406
+ organization,
407
+ usage: usageId,
408
+ type,
409
+ units: amount,
410
+ key,
411
+ meta,
412
+ ...traceId && { traceId }
413
+ }
414
+ },
415
+ options: {
416
+ bypassDocumentValidation: true,
417
+ includeResultMetadata: true,
418
+ upsert: true,
419
+ ...session && { session }
420
+ },
421
+ query: {
408
422
  key,
409
- meta,
410
- ...traceId && { traceId }
423
+ usage: usageId
411
424
  }
412
425
  });
426
+ inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
413
427
  } catch (error) {
414
- if (isDuplicateKey(error)) return;
428
+ if (isDuplicateKey(error) && !session) return;
415
429
  throw error;
416
430
  }
417
- await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount } });
431
+ if (inserted) {
432
+ await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
433
+ }
418
434
  };
419
435
  var percentage = (value1, value2, decimals = 1) => ((value1 || 0) / (value2 || 0) * 100 || 0).toFixed(decimals);
420
436
  var reducers = {
package/dist/index.d.cts CHANGED
@@ -386,18 +386,22 @@ const isDuplicateKey = ( error ) =>
386
386
  || /E11000|duplicate key/i.test( error?.message || '' );
387
387
 
388
388
  // Record a billable action as a row in the append-only `action` ledger — the
389
- // source of truth for usage billing (summed per period at changeover). The unique
390
- // `key` makes recording idempotent: a duplicate (the segment realtime path AND the
391
- // sweep firing for the same membership, or a job retry) hits the unique index and
392
- // is skipped, so it can never double-count. On a genuinely new row we bump the
393
- // display cache `usage.totals.actions` by the same units; the billed number always
394
- // comes from summing the ledger, so a missed cache $inc is only cosmetic (it
395
- // self-heals at changeover). No transaction needed the unique index is the
396
- // atomic dedup.
389
+ // source of truth for usage billing (summed per period at changeover).
390
+ //
391
+ // Idempotent via an UPSERT on the unique { usage, key }: a duplicate event (the
392
+ // segment realtime path AND the sweep firing for the same membership, or a retry)
393
+ // matches the existing row and no-ops no duplicate-key error — so this is safe
394
+ // to call INSIDE a caller's transaction (a thrown E11000 would abort the whole
395
+ // transaction). The display cache `usage.totals.actions` is bumped ONLY when the
396
+ // upsert actually inserts a new row, so a duplicate never double-counts.
397
+ //
398
+ // Pass `session` to make the ledger row + cache bump atomic with the caller's own
399
+ // writes (e.g. the membership writes in a segment-sweep chunk, or the submission
400
+ // docs) — all-or-nothing. Without a session it runs standalone.
397
401
  // key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
398
402
  // units billable units (default 1; N for batch, e.g. accepted entries)
399
403
  // meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
400
- const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
404
+ const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
401
405
 
402
406
  if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
403
407
 
@@ -405,34 +409,56 @@ const recordAction = async ({ controller, organization, usageId, type, units = 1
405
409
 
406
410
  if( ! Number.isFinite( amount ) || amount === 0 ) return;
407
411
 
412
+ let inserted = false;
413
+
408
414
  try {
409
415
 
410
- await controller.create({
416
+ const { lastErrorObject } = await controller.update({
411
417
  collection : 'action',
412
418
  data : {
413
- organization,
414
- usage : usageId,
415
- type,
416
- units : amount,
419
+ $setOnInsert : {
420
+ organization,
421
+ usage : usageId,
422
+ type,
423
+ units : amount,
424
+ key,
425
+ meta,
426
+ ...( traceId && { traceId })
427
+ }
428
+ },
429
+ options : {
430
+ bypassDocumentValidation : true,
431
+ includeResultMetadata : true,
432
+ upsert : true,
433
+ ...( session && { session })
434
+ },
435
+ query : {
417
436
  key,
418
- meta,
419
- ...( traceId && { traceId })
437
+ usage : usageId
420
438
  }
421
439
  });
422
440
 
441
+ inserted = Boolean( lastErrorObject?.upserted );
442
+
423
443
  } catch ( error ) {
424
444
 
425
- // Duplicate key = this exact action was already recorded skip silently.
426
- // This is the dedup that makes billing drift-proof. Re-throw anything else.
427
- if( isDuplicateKey( error ) ) return;
445
+ // A concurrent upsert of the same { usage, key } can still race to E11000.
446
+ // Standalone: the other writer recorded it — skip. Inside a transaction:
447
+ // the txn is already aborted, so re-throw to let the caller retry the whole
448
+ // unit (which then sees the row and no-ops).
449
+ if( isDuplicateKey( error ) && ! session ) return;
428
450
 
429
451
  throw error;
430
452
 
431
453
  }
432
454
 
433
- // New row only keep the display cache in step. Billing sums the ledger, so
434
- // this is cosmetic and self-heals at changeover if it ever fails.
435
- await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount } });
455
+ // Only a genuinely new row bumps the display cache (a matched duplicate must
456
+ // not double-count). Joined to the caller's transaction when a session is given.
457
+ if( inserted ){
458
+
459
+ await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
460
+
461
+ }
436
462
 
437
463
  };
438
464
 
package/dist/index.d.ts CHANGED
@@ -386,18 +386,22 @@ const isDuplicateKey = ( error ) =>
386
386
  || /E11000|duplicate key/i.test( error?.message || '' );
387
387
 
388
388
  // Record a billable action as a row in the append-only `action` ledger — the
389
- // source of truth for usage billing (summed per period at changeover). The unique
390
- // `key` makes recording idempotent: a duplicate (the segment realtime path AND the
391
- // sweep firing for the same membership, or a job retry) hits the unique index and
392
- // is skipped, so it can never double-count. On a genuinely new row we bump the
393
- // display cache `usage.totals.actions` by the same units; the billed number always
394
- // comes from summing the ledger, so a missed cache $inc is only cosmetic (it
395
- // self-heals at changeover). No transaction needed the unique index is the
396
- // atomic dedup.
389
+ // source of truth for usage billing (summed per period at changeover).
390
+ //
391
+ // Idempotent via an UPSERT on the unique { usage, key }: a duplicate event (the
392
+ // segment realtime path AND the sweep firing for the same membership, or a retry)
393
+ // matches the existing row and no-ops no duplicate-key error — so this is safe
394
+ // to call INSIDE a caller's transaction (a thrown E11000 would abort the whole
395
+ // transaction). The display cache `usage.totals.actions` is bumped ONLY when the
396
+ // upsert actually inserts a new row, so a duplicate never double-counts.
397
+ //
398
+ // Pass `session` to make the ledger row + cache bump atomic with the caller's own
399
+ // writes (e.g. the membership writes in a segment-sweep chunk, or the submission
400
+ // docs) — all-or-nothing. Without a session it runs standalone.
397
401
  // key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
398
402
  // units billable units (default 1; N for batch, e.g. accepted entries)
399
403
  // meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
400
- const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
404
+ const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
401
405
 
402
406
  if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
403
407
 
@@ -405,34 +409,56 @@ const recordAction = async ({ controller, organization, usageId, type, units = 1
405
409
 
406
410
  if( ! Number.isFinite( amount ) || amount === 0 ) return;
407
411
 
412
+ let inserted = false;
413
+
408
414
  try {
409
415
 
410
- await controller.create({
416
+ const { lastErrorObject } = await controller.update({
411
417
  collection : 'action',
412
418
  data : {
413
- organization,
414
- usage : usageId,
415
- type,
416
- units : amount,
419
+ $setOnInsert : {
420
+ organization,
421
+ usage : usageId,
422
+ type,
423
+ units : amount,
424
+ key,
425
+ meta,
426
+ ...( traceId && { traceId })
427
+ }
428
+ },
429
+ options : {
430
+ bypassDocumentValidation : true,
431
+ includeResultMetadata : true,
432
+ upsert : true,
433
+ ...( session && { session })
434
+ },
435
+ query : {
417
436
  key,
418
- meta,
419
- ...( traceId && { traceId })
437
+ usage : usageId
420
438
  }
421
439
  });
422
440
 
441
+ inserted = Boolean( lastErrorObject?.upserted );
442
+
423
443
  } catch ( error ) {
424
444
 
425
- // Duplicate key = this exact action was already recorded skip silently.
426
- // This is the dedup that makes billing drift-proof. Re-throw anything else.
427
- if( isDuplicateKey( error ) ) return;
445
+ // A concurrent upsert of the same { usage, key } can still race to E11000.
446
+ // Standalone: the other writer recorded it — skip. Inside a transaction:
447
+ // the txn is already aborted, so re-throw to let the caller retry the whole
448
+ // unit (which then sees the row and no-ops).
449
+ if( isDuplicateKey( error ) && ! session ) return;
428
450
 
429
451
  throw error;
430
452
 
431
453
  }
432
454
 
433
- // New row only keep the display cache in step. Billing sums the ledger, so
434
- // this is cosmetic and self-heals at changeover if it ever fails.
435
- await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount } });
455
+ // Only a genuinely new row bumps the display cache (a matched duplicate must
456
+ // not double-count). Joined to the caller's transaction when a session is given.
457
+ if( inserted ){
458
+
459
+ await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
460
+
461
+ }
436
462
 
437
463
  };
438
464
 
package/dist/index.js CHANGED
@@ -337,28 +337,44 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
337
337
  });
338
338
  };
339
339
  var isDuplicateKey = (error) => (error == null ? void 0 : error.code) === 11e3 || (error == null ? void 0 : error.code) === "11000" || /E11000|duplicate key/i.test((error == null ? void 0 : error.message) || "");
340
- var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
340
+ var recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId, session }) => {
341
341
  if (!controller || !organization || !usageId || !type || !key) return;
342
342
  const amount = Number(units);
343
343
  if (!Number.isFinite(amount) || amount === 0) return;
344
+ let inserted = false;
344
345
  try {
345
- await controller.create({
346
+ const { lastErrorObject } = await controller.update({
346
347
  collection: "action",
347
348
  data: {
348
- organization,
349
- usage: usageId,
350
- type,
351
- units: amount,
349
+ $setOnInsert: {
350
+ organization,
351
+ usage: usageId,
352
+ type,
353
+ units: amount,
354
+ key,
355
+ meta,
356
+ ...traceId && { traceId }
357
+ }
358
+ },
359
+ options: {
360
+ bypassDocumentValidation: true,
361
+ includeResultMetadata: true,
362
+ upsert: true,
363
+ ...session && { session }
364
+ },
365
+ query: {
352
366
  key,
353
- meta,
354
- ...traceId && { traceId }
367
+ usage: usageId
355
368
  }
356
369
  });
370
+ inserted = Boolean(lastErrorObject == null ? void 0 : lastErrorObject.upserted);
357
371
  } catch (error) {
358
- if (isDuplicateKey(error)) return;
372
+ if (isDuplicateKey(error) && !session) return;
359
373
  throw error;
360
374
  }
361
- await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount } });
375
+ if (inserted) {
376
+ await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
377
+ }
362
378
  };
363
379
  var percentage = (value1, value2, decimals = 1) => ((value1 || 0) / (value2 || 0) * 100 || 0).toFixed(decimals);
364
380
  var reducers = {
package/package.json CHANGED
@@ -157,5 +157,5 @@
157
157
  "build": "tsup && npm publish"
158
158
  },
159
159
  "types": "dist/index.d.ts",
160
- "version": "0.0.62"
160
+ "version": "0.0.63"
161
161
  }