@drawbridge/drawbridge-utils 0.0.71 → 0.0.72

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/billing.cjs CHANGED
@@ -387,6 +387,63 @@ var action = {
387
387
  await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
388
388
  }
389
389
  },
390
+ // Batch variant of record() for a caller writing many ledger rows inside ONE
391
+ // transaction. A session forbids concurrent ops, so the alternative is a serial
392
+ // await-loop costing N round-trips; this does a single bulkWrite of idempotent
393
+ // { usage, key } upserts (ordered:false), then bumps usage.totals.actions by the
394
+ // summed units of ONLY the genuinely-inserted rows (upsertedIds) — mirroring
395
+ // record()'s per-row `inserted` gate. Rows are deduped by key within the batch
396
+ // so two upserts can't target the same { usage, key }. Pass `session` to join
397
+ // the caller's transaction (the intended path).
398
+ bulk: async ({ controller, organization, usageId, rows = [], session }) => {
399
+ if (!controller || !organization || !usageId || !Array.isArray(rows) || rows.length === 0) return;
400
+ const seen = /* @__PURE__ */ new Set();
401
+ const valid = [];
402
+ for (const row of rows) {
403
+ const { type, units = 1, key, meta = {}, traceId } = row || {};
404
+ const amount2 = Number(units);
405
+ if (!type || !key || !Number.isFinite(amount2) || amount2 === 0) continue;
406
+ if (seen.has(key)) continue;
407
+ seen.add(key);
408
+ valid.push({ type, amount: amount2, key, meta, traceId });
409
+ }
410
+ if (valid.length === 0) return;
411
+ const result = await controller.bulk({
412
+ collection: "action",
413
+ pipeline: valid.map(({ type, amount: amount2, key, meta, traceId }) => ({
414
+ updateOne: {
415
+ filter: { key, usage: usageId },
416
+ update: {
417
+ $setOnInsert: {
418
+ organization,
419
+ usage: usageId,
420
+ type,
421
+ units: amount2,
422
+ key,
423
+ meta,
424
+ ...traceId && { traceId }
425
+ }
426
+ },
427
+ upsert: true
428
+ }
429
+ })),
430
+ options: {
431
+ bypassDocumentValidation: true,
432
+ ordered: false,
433
+ ...session && { session }
434
+ }
435
+ });
436
+ const amount = Object.keys((result == null ? void 0 : result.upsertedIds) || {}).reduce(
437
+ (sum, index) => {
438
+ var _a;
439
+ return sum + (((_a = valid[Number(index)]) == null ? void 0 : _a.amount) || 0);
440
+ },
441
+ 0
442
+ );
443
+ if (amount > 0) {
444
+ await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
445
+ }
446
+ },
390
447
  // Authoritative per-period total to bill — sum(units) over the period's ledger
391
448
  // rows. The Stripe meter emit (in the Stripe/sync layer) reports this number.
392
449
  sum: async ({ controller, usageId }) => {
@@ -1,5 +1,5 @@
1
1
  import { credit, debit } from './transactions.cjs';
2
- import { isDuplicateKey, incrementUsageTotals } from './usage.cjs';
2
+ import { incrementUsageTotals, isDuplicateKey } from './usage.cjs';
3
3
  import { createLogger } from '@drawbridge/drawbridge-telemetry';
4
4
 
5
5
  // Billing — the single source of truth for what Drawbridge charges, and the
@@ -9,7 +9,7 @@ import { createLogger } from '@drawbridge/drawbridge-telemetry';
9
9
  // billing.ai { price, cost, bill } AI requests → credit rail (debit)
10
10
  // billing.scrape { fee, bill } scrape fees → credit rail (debit)
11
11
  // billing.grant { bonus, admin } promo credits → credit rail (credit)
12
- // billing.action { record, sum } usage actions → Stripe meter rail
12
+ // billing.action { record, bulk, sum } usage actions → Stripe meter rail
13
13
  //
14
14
  // Sits on top of transactions.js (the generic credit/debit money rail) and
15
15
  // usage.js (the action-ledger + usage-totals primitives). Shopify order fees are
@@ -399,6 +399,76 @@ const action = {
399
399
 
400
400
  },
401
401
 
402
+ // Batch variant of record() for a caller writing many ledger rows inside ONE
403
+ // transaction. A session forbids concurrent ops, so the alternative is a serial
404
+ // await-loop costing N round-trips; this does a single bulkWrite of idempotent
405
+ // { usage, key } upserts (ordered:false), then bumps usage.totals.actions by the
406
+ // summed units of ONLY the genuinely-inserted rows (upsertedIds) — mirroring
407
+ // record()'s per-row `inserted` gate. Rows are deduped by key within the batch
408
+ // so two upserts can't target the same { usage, key }. Pass `session` to join
409
+ // the caller's transaction (the intended path).
410
+ bulk : async ({ controller, organization, usageId, rows = [], session }) => {
411
+
412
+ if( ! controller || ! organization || ! usageId || ! Array.isArray( rows ) || rows.length === 0 ) return;
413
+
414
+ const seen = new Set();
415
+ const valid = [];
416
+
417
+ for( const row of rows ){
418
+
419
+ const { type, units = 1, key, meta = {}, traceId } = row || {};
420
+ const amount = Number( units );
421
+
422
+ if( ! type || ! key || ! Number.isFinite( amount ) || amount === 0 ) continue;
423
+ if( seen.has( key ) ) continue;
424
+
425
+ seen.add( key );
426
+ valid.push({ type, amount, key, meta, traceId });
427
+
428
+ }
429
+
430
+ if( valid.length === 0 ) return;
431
+
432
+ const result = await controller.bulk({
433
+ collection : 'action',
434
+ pipeline : valid.map( ({ type, amount, key, meta, traceId }) => ({
435
+ updateOne : {
436
+ filter : { key, usage : usageId },
437
+ update : {
438
+ $setOnInsert : {
439
+ organization,
440
+ usage : usageId,
441
+ type,
442
+ units : amount,
443
+ key,
444
+ meta,
445
+ ...( traceId && { traceId })
446
+ }
447
+ },
448
+ upsert : true
449
+ }
450
+ })),
451
+ options : {
452
+ bypassDocumentValidation : true,
453
+ ordered : false,
454
+ ...( session && { session })
455
+ }
456
+ });
457
+
458
+ // upsertedIds = { <opIndex>: _id } for newly-inserted rows; sum their units.
459
+ const amount = Object.keys( result?.upsertedIds || {} ).reduce(
460
+ ( sum, index ) => sum + ( valid[ Number( index ) ]?.amount || 0 ),
461
+ 0
462
+ );
463
+
464
+ if( amount > 0 ){
465
+
466
+ await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
467
+
468
+ }
469
+
470
+ },
471
+
402
472
  // Authoritative per-period total to bill — sum(units) over the period's ledger
403
473
  // rows. The Stripe meter emit (in the Stripe/sync layer) reports this number.
404
474
  sum : async ({ controller, usageId }) => {
package/dist/billing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { credit, debit } from './transactions.js';
2
- import { isDuplicateKey, incrementUsageTotals } from './usage.js';
2
+ import { incrementUsageTotals, isDuplicateKey } from './usage.js';
3
3
  import { createLogger } from '@drawbridge/drawbridge-telemetry';
4
4
 
5
5
  // Billing — the single source of truth for what Drawbridge charges, and the
@@ -9,7 +9,7 @@ import { createLogger } from '@drawbridge/drawbridge-telemetry';
9
9
  // billing.ai { price, cost, bill } AI requests → credit rail (debit)
10
10
  // billing.scrape { fee, bill } scrape fees → credit rail (debit)
11
11
  // billing.grant { bonus, admin } promo credits → credit rail (credit)
12
- // billing.action { record, sum } usage actions → Stripe meter rail
12
+ // billing.action { record, bulk, sum } usage actions → Stripe meter rail
13
13
  //
14
14
  // Sits on top of transactions.js (the generic credit/debit money rail) and
15
15
  // usage.js (the action-ledger + usage-totals primitives). Shopify order fees are
@@ -399,6 +399,76 @@ const action = {
399
399
 
400
400
  },
401
401
 
402
+ // Batch variant of record() for a caller writing many ledger rows inside ONE
403
+ // transaction. A session forbids concurrent ops, so the alternative is a serial
404
+ // await-loop costing N round-trips; this does a single bulkWrite of idempotent
405
+ // { usage, key } upserts (ordered:false), then bumps usage.totals.actions by the
406
+ // summed units of ONLY the genuinely-inserted rows (upsertedIds) — mirroring
407
+ // record()'s per-row `inserted` gate. Rows are deduped by key within the batch
408
+ // so two upserts can't target the same { usage, key }. Pass `session` to join
409
+ // the caller's transaction (the intended path).
410
+ bulk : async ({ controller, organization, usageId, rows = [], session }) => {
411
+
412
+ if( ! controller || ! organization || ! usageId || ! Array.isArray( rows ) || rows.length === 0 ) return;
413
+
414
+ const seen = new Set();
415
+ const valid = [];
416
+
417
+ for( const row of rows ){
418
+
419
+ const { type, units = 1, key, meta = {}, traceId } = row || {};
420
+ const amount = Number( units );
421
+
422
+ if( ! type || ! key || ! Number.isFinite( amount ) || amount === 0 ) continue;
423
+ if( seen.has( key ) ) continue;
424
+
425
+ seen.add( key );
426
+ valid.push({ type, amount, key, meta, traceId });
427
+
428
+ }
429
+
430
+ if( valid.length === 0 ) return;
431
+
432
+ const result = await controller.bulk({
433
+ collection : 'action',
434
+ pipeline : valid.map( ({ type, amount, key, meta, traceId }) => ({
435
+ updateOne : {
436
+ filter : { key, usage : usageId },
437
+ update : {
438
+ $setOnInsert : {
439
+ organization,
440
+ usage : usageId,
441
+ type,
442
+ units : amount,
443
+ key,
444
+ meta,
445
+ ...( traceId && { traceId })
446
+ }
447
+ },
448
+ upsert : true
449
+ }
450
+ })),
451
+ options : {
452
+ bypassDocumentValidation : true,
453
+ ordered : false,
454
+ ...( session && { session })
455
+ }
456
+ });
457
+
458
+ // upsertedIds = { <opIndex>: _id } for newly-inserted rows; sum their units.
459
+ const amount = Object.keys( result?.upsertedIds || {} ).reduce(
460
+ ( sum, index ) => sum + ( valid[ Number( index ) ]?.amount || 0 ),
461
+ 0
462
+ );
463
+
464
+ if( amount > 0 ){
465
+
466
+ await incrementUsageTotals({ controller, usageId, $inc : { 'totals.actions' : amount }, session });
467
+
468
+ }
469
+
470
+ },
471
+
402
472
  // Authoritative per-period total to bill — sum(units) over the period's ledger
403
473
  // rows. The Stripe meter emit (in the Stripe/sync layer) reports this number.
404
474
  sum : async ({ controller, usageId }) => {
package/dist/billing.js CHANGED
@@ -358,6 +358,63 @@ var action = {
358
358
  await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
359
359
  }
360
360
  },
361
+ // Batch variant of record() for a caller writing many ledger rows inside ONE
362
+ // transaction. A session forbids concurrent ops, so the alternative is a serial
363
+ // await-loop costing N round-trips; this does a single bulkWrite of idempotent
364
+ // { usage, key } upserts (ordered:false), then bumps usage.totals.actions by the
365
+ // summed units of ONLY the genuinely-inserted rows (upsertedIds) — mirroring
366
+ // record()'s per-row `inserted` gate. Rows are deduped by key within the batch
367
+ // so two upserts can't target the same { usage, key }. Pass `session` to join
368
+ // the caller's transaction (the intended path).
369
+ bulk: async ({ controller, organization, usageId, rows = [], session }) => {
370
+ if (!controller || !organization || !usageId || !Array.isArray(rows) || rows.length === 0) return;
371
+ const seen = /* @__PURE__ */ new Set();
372
+ const valid = [];
373
+ for (const row of rows) {
374
+ const { type, units = 1, key, meta = {}, traceId } = row || {};
375
+ const amount2 = Number(units);
376
+ if (!type || !key || !Number.isFinite(amount2) || amount2 === 0) continue;
377
+ if (seen.has(key)) continue;
378
+ seen.add(key);
379
+ valid.push({ type, amount: amount2, key, meta, traceId });
380
+ }
381
+ if (valid.length === 0) return;
382
+ const result = await controller.bulk({
383
+ collection: "action",
384
+ pipeline: valid.map(({ type, amount: amount2, key, meta, traceId }) => ({
385
+ updateOne: {
386
+ filter: { key, usage: usageId },
387
+ update: {
388
+ $setOnInsert: {
389
+ organization,
390
+ usage: usageId,
391
+ type,
392
+ units: amount2,
393
+ key,
394
+ meta,
395
+ ...traceId && { traceId }
396
+ }
397
+ },
398
+ upsert: true
399
+ }
400
+ })),
401
+ options: {
402
+ bypassDocumentValidation: true,
403
+ ordered: false,
404
+ ...session && { session }
405
+ }
406
+ });
407
+ const amount = Object.keys((result == null ? void 0 : result.upsertedIds) || {}).reduce(
408
+ (sum, index) => {
409
+ var _a;
410
+ return sum + (((_a = valid[Number(index)]) == null ? void 0 : _a.amount) || 0);
411
+ },
412
+ 0
413
+ );
414
+ if (amount > 0) {
415
+ await incrementUsageTotals({ controller, usageId, $inc: { "totals.actions": amount }, session });
416
+ }
417
+ },
361
418
  // Authoritative per-period total to bill — sum(units) over the period's ledger
362
419
  // rows. The Stripe meter emit (in the Stripe/sync layer) reports this number.
363
420
  sum: async ({ controller, usageId }) => {
package/package.json CHANGED
@@ -158,5 +158,5 @@
158
158
  "build": "tsup && npm publish"
159
159
  },
160
160
  "types": "dist/index.d.ts",
161
- "version": "0.0.71"
161
+ "version": "0.0.72"
162
162
  }