@drawbridge/drawbridge-utils 0.0.61 → 0.0.62

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
@@ -43,10 +43,12 @@ __export(index_exports, {
43
43
  gigabyte: () => gigabyte,
44
44
  incrementUsageTotals: () => incrementUsageTotals,
45
45
  infinite: () => infinite,
46
+ isDuplicateKey: () => isDuplicateKey,
46
47
  isInfinite: () => isInfinite,
47
48
  megabyte: () => megabyte,
48
49
  nanoid: () => nanoid,
49
50
  percentage: () => percentage,
51
+ recordAction: () => recordAction,
50
52
  reducers: () => reducers,
51
53
  regex: () => regex,
52
54
  shareUrls: () => shareUrls,
@@ -390,6 +392,30 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
390
392
  query: { id: usageId }
391
393
  });
392
394
  };
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 }) => {
397
+ if (!controller || !organization || !usageId || !type || !key) return;
398
+ const amount = Number(units);
399
+ if (!Number.isFinite(amount) || amount === 0) return;
400
+ try {
401
+ await controller.create({
402
+ collection: "action",
403
+ data: {
404
+ organization,
405
+ usage: usageId,
406
+ type,
407
+ units: amount,
408
+ key,
409
+ meta,
410
+ ...traceId && { traceId }
411
+ }
412
+ });
413
+ } catch (error) {
414
+ if (isDuplicateKey(error)) return;
415
+ throw error;
416
+ }
417
+ await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount } });
418
+ };
393
419
  var percentage = (value1, value2, decimals = 1) => ((value1 || 0) / (value2 || 0) * 100 || 0).toFixed(decimals);
394
420
  var reducers = {
395
421
  fields: (data2 = [], callback = () => ({})) => data2.reduce(
@@ -483,10 +509,12 @@ var currency = (val) => (0, import_currency_codes.code)(val);
483
509
  gigabyte,
484
510
  incrementUsageTotals,
485
511
  infinite,
512
+ isDuplicateKey,
486
513
  isInfinite,
487
514
  megabyte,
488
515
  nanoid,
489
516
  percentage,
517
+ recordAction,
490
518
  reducers,
491
519
  regex,
492
520
  shareUrls,
package/dist/index.d.cts CHANGED
@@ -379,6 +379,63 @@ const incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
379
379
 
380
380
  };
381
381
 
382
+ // True for a MongoDB duplicate-key error (unique-index violation).
383
+ const isDuplicateKey = ( error ) =>
384
+ error?.code === 11000
385
+ || error?.code === '11000'
386
+ || /E11000|duplicate key/i.test( error?.message || '' );
387
+
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.
397
+ // key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
398
+ // units billable units (default 1; N for batch, e.g. accepted entries)
399
+ // meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
400
+ const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
401
+
402
+ if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
403
+
404
+ const amount = Number( units );
405
+
406
+ if( ! Number.isFinite( amount ) || amount === 0 ) return;
407
+
408
+ try {
409
+
410
+ await controller.create({
411
+ collection : 'action',
412
+ data : {
413
+ organization,
414
+ usage : usageId,
415
+ type,
416
+ units : amount,
417
+ key,
418
+ meta,
419
+ ...( traceId && { traceId })
420
+ }
421
+ });
422
+
423
+ } catch ( error ) {
424
+
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;
428
+
429
+ throw error;
430
+
431
+ }
432
+
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 } });
436
+
437
+ };
438
+
382
439
  const percentage = ( value1, value2, decimals = 1 ) => ( ( ( ( value1 || 0 ) / ( value2 || 0 ) ) * 100 ) || 0 ).toFixed( decimals );
383
440
 
384
441
  const reducers = {
@@ -477,4 +534,4 @@ const currencies = data.map( ( item ) => ({
477
534
 
478
535
  const currency = ( val ) => code( val );
479
536
 
480
- export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isInfinite, megabyte, nanoid, percentage, reducers, regex, shareUrls, urlRoot };
537
+ export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isDuplicateKey, isInfinite, megabyte, nanoid, percentage, recordAction, reducers, regex, shareUrls, urlRoot };
package/dist/index.d.ts CHANGED
@@ -379,6 +379,63 @@ const incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
379
379
 
380
380
  };
381
381
 
382
+ // True for a MongoDB duplicate-key error (unique-index violation).
383
+ const isDuplicateKey = ( error ) =>
384
+ error?.code === 11000
385
+ || error?.code === '11000'
386
+ || /E11000|duplicate key/i.test( error?.message || '' );
387
+
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.
397
+ // key deterministic per source event, e.g. 'segment.join:<contact>:<segment>'
398
+ // units billable units (default 1; N for batch, e.g. accepted entries)
399
+ // meta attribution ids { campaign, page, submission, segment, workflow, step, ... }
400
+ const recordAction = async ({ controller, organization, usageId, type, units = 1, key, meta = {}, traceId }) => {
401
+
402
+ if( ! controller || ! organization || ! usageId || ! type || ! key ) return;
403
+
404
+ const amount = Number( units );
405
+
406
+ if( ! Number.isFinite( amount ) || amount === 0 ) return;
407
+
408
+ try {
409
+
410
+ await controller.create({
411
+ collection : 'action',
412
+ data : {
413
+ organization,
414
+ usage : usageId,
415
+ type,
416
+ units : amount,
417
+ key,
418
+ meta,
419
+ ...( traceId && { traceId })
420
+ }
421
+ });
422
+
423
+ } catch ( error ) {
424
+
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;
428
+
429
+ throw error;
430
+
431
+ }
432
+
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 } });
436
+
437
+ };
438
+
382
439
  const percentage = ( value1, value2, decimals = 1 ) => ( ( ( ( value1 || 0 ) / ( value2 || 0 ) ) * 100 ) || 0 ).toFixed( decimals );
383
440
 
384
441
  const reducers = {
@@ -477,4 +534,4 @@ const currencies = data.map( ( item ) => ({
477
534
 
478
535
  const currency = ( val ) => code( val );
479
536
 
480
- export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isInfinite, megabyte, nanoid, percentage, reducers, regex, shareUrls, urlRoot };
537
+ export { bytesToGB, bytesToMB, capitalize, constants, currencies, currency, expiredPaymentMethod, formatCurrency, formatDateString, formatNumber, getPlanFeature, gigabyte, incrementUsageTotals, infinite, isDuplicateKey, isInfinite, megabyte, nanoid, percentage, recordAction, reducers, regex, shareUrls, urlRoot };
package/dist/index.js CHANGED
@@ -336,6 +336,30 @@ var incrementUsageTotals = async ({ controller, usageId, $inc, session }) => {
336
336
  query: { id: usageId }
337
337
  });
338
338
  };
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 }) => {
341
+ if (!controller || !organization || !usageId || !type || !key) return;
342
+ const amount = Number(units);
343
+ if (!Number.isFinite(amount) || amount === 0) return;
344
+ try {
345
+ await controller.create({
346
+ collection: "action",
347
+ data: {
348
+ organization,
349
+ usage: usageId,
350
+ type,
351
+ units: amount,
352
+ key,
353
+ meta,
354
+ ...traceId && { traceId }
355
+ }
356
+ });
357
+ } catch (error) {
358
+ if (isDuplicateKey(error)) return;
359
+ throw error;
360
+ }
361
+ await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount } });
362
+ };
339
363
  var percentage = (value1, value2, decimals = 1) => ((value1 || 0) / (value2 || 0) * 100 || 0).toFixed(decimals);
340
364
  var reducers = {
341
365
  fields: (data2 = [], callback = () => ({})) => data2.reduce(
@@ -428,10 +452,12 @@ export {
428
452
  gigabyte,
429
453
  incrementUsageTotals,
430
454
  infinite,
455
+ isDuplicateKey,
431
456
  isInfinite,
432
457
  megabyte,
433
458
  nanoid,
434
459
  percentage,
460
+ recordAction,
435
461
  reducers,
436
462
  regex,
437
463
  shareUrls,
package/package.json CHANGED
@@ -17,9 +17,6 @@
17
17
  "tsup": "8.5.1",
18
18
  "typescript": "5.9.3"
19
19
  },
20
- "overrides": {
21
- "esbuild": "0.28.1"
22
- },
23
20
  "peerDependencies": {
24
21
  "@node-oauth/oauth2-server": "^5.3.0"
25
22
  },
@@ -160,5 +157,5 @@
160
157
  "build": "tsup && npm publish"
161
158
  },
162
159
  "types": "dist/index.d.ts",
163
- "version": "0.0.61"
160
+ "version": "0.0.62"
164
161
  }