@cross-deck/buckets 0.9.0 → 0.10.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/README.md +31 -4
- package/dist/index.d.mts +37 -14
- package/dist/index.d.ts +37 -14
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +43 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -501,12 +501,14 @@ Firestore is simply the first place it found the leak.
|
|
|
501
501
|
| **Firestore — server** (`firebase-admin`) | reads | ✅ Supported |
|
|
502
502
|
| **Firestore — browser** (`firebase` JS SDK) | reads | ✅ Supported — `@cross-deck/buckets/web` |
|
|
503
503
|
| **MongoDB** (`mongodb` driver) | documents read | ✅ Supported — `installMongoMeter` |
|
|
504
|
-
| Postgres
|
|
504
|
+
| **Postgres** (`pg` driver) — incl. Supabase, Neon, Vercel Postgres, RDS | rows read | ✅ Supported — `installPgMeter` |
|
|
505
|
+
| DynamoDB · Cosmos · Redis | (per-source unit) | 🔜 Adapter interface is public — contributions welcome |
|
|
505
506
|
|
|
506
507
|
Each adapter measures its source's **raw unit** — never a dollar bill. Firestore
|
|
507
|
-
counts reads; MongoDB counts the documents your queries return
|
|
508
|
-
|
|
509
|
-
|
|
508
|
+
counts reads; MongoDB counts the documents your queries return; Postgres counts the
|
|
509
|
+
rows your queries return. (Supabase, Neon, and RDS bill by compute — instance size ×
|
|
510
|
+
hours, not per row — so "rows read" is the read *load* by feature: the work that
|
|
511
|
+
sizes your instance and the place to optimise, not your invoice.)
|
|
510
512
|
|
|
511
513
|
### MongoDB
|
|
512
514
|
|
|
@@ -529,6 +531,31 @@ documents it returns, attributed to the bucket — observe-only (it reads the re
|
|
|
529
531
|
already in hand, runs no `explain()` and no profiler scan, so it never becomes a read
|
|
530
532
|
monster). The dashboard shows it in MongoDB's own language ("docs read").
|
|
531
533
|
|
|
534
|
+
### Postgres
|
|
535
|
+
|
|
536
|
+
One adapter covers node-postgres (`pg`) and everything built on it — Supabase, Neon,
|
|
537
|
+
Vercel Postgres, Amazon RDS, and plain Postgres. Install the trap once; name your
|
|
538
|
+
paths with `bucket()`. Pass the `Client` class from your `pg` import (an optional peer
|
|
539
|
+
dep):
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { Client } from "pg";
|
|
543
|
+
import { installPgMeter, bucket } from "@cross-deck/buckets";
|
|
544
|
+
|
|
545
|
+
installPgMeter({ Client }); // once, at startup
|
|
546
|
+
|
|
547
|
+
await bucket("billing-page", async () => {
|
|
548
|
+
const { rows } = await pool.query("SELECT * FROM invoices WHERE user_id = $1", [id]); // → billing-page
|
|
549
|
+
});
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Every `SELECT` is counted as the rows it returns, attributed to the bucket. One patch
|
|
553
|
+
on `Client` covers a `Pool` too — `pool.query()` runs through the same client, so
|
|
554
|
+
there's no double counting. Observe-only: it reads `result.rows` already in hand, runs
|
|
555
|
+
no `EXPLAIN` and no `pg_stat_statements` scan, so it never becomes a read monster.
|
|
556
|
+
Writes (`INSERT`/`UPDATE`/`DELETE`, even with `RETURNING`) are not reads and are not
|
|
557
|
+
counted. The dashboard shows it in Postgres's own language ("rows read").
|
|
558
|
+
|
|
532
559
|
---
|
|
533
560
|
|
|
534
561
|
## API
|
package/dist/index.d.mts
CHANGED
|
@@ -40,7 +40,7 @@ declare function recordDeletes(n?: number): void;
|
|
|
40
40
|
*/
|
|
41
41
|
declare function flush(): Promise<void>;
|
|
42
42
|
|
|
43
|
-
type AnyFn$
|
|
43
|
+
type AnyFn$2 = (...args: any[]) => any;
|
|
44
44
|
/**
|
|
45
45
|
* The firebase-admin Firestore classes to patch. Pass the module namespace from
|
|
46
46
|
* `firebase-admin/firestore` — only the prototypes present are patched.
|
|
@@ -48,31 +48,31 @@ type AnyFn$1 = (...args: any[]) => any;
|
|
|
48
48
|
interface FirestoreClasses {
|
|
49
49
|
Query?: {
|
|
50
50
|
prototype: {
|
|
51
|
-
get?: AnyFn$
|
|
52
|
-
onSnapshot?: AnyFn$
|
|
51
|
+
get?: AnyFn$2;
|
|
52
|
+
onSnapshot?: AnyFn$2;
|
|
53
53
|
};
|
|
54
54
|
};
|
|
55
55
|
DocumentReference?: {
|
|
56
56
|
prototype: {
|
|
57
|
-
get?: AnyFn$
|
|
58
|
-
onSnapshot?: AnyFn$
|
|
57
|
+
get?: AnyFn$2;
|
|
58
|
+
onSnapshot?: AnyFn$2;
|
|
59
59
|
};
|
|
60
60
|
};
|
|
61
61
|
Transaction?: {
|
|
62
62
|
prototype: {
|
|
63
|
-
get?: AnyFn$
|
|
64
|
-
getAll?: AnyFn$
|
|
63
|
+
get?: AnyFn$2;
|
|
64
|
+
getAll?: AnyFn$2;
|
|
65
65
|
};
|
|
66
66
|
};
|
|
67
67
|
Firestore?: {
|
|
68
68
|
prototype: {
|
|
69
|
-
getAll?: AnyFn$
|
|
69
|
+
getAll?: AnyFn$2;
|
|
70
70
|
};
|
|
71
71
|
};
|
|
72
72
|
/** count() / sum() / average() — aggregation queries bill reads too. */
|
|
73
73
|
AggregateQuery?: {
|
|
74
74
|
prototype: {
|
|
75
|
-
get?: AnyFn$
|
|
75
|
+
get?: AnyFn$2;
|
|
76
76
|
};
|
|
77
77
|
};
|
|
78
78
|
}
|
|
@@ -112,7 +112,7 @@ declare function currentCostTag(): CostTag;
|
|
|
112
112
|
*/
|
|
113
113
|
declare function bucket<T>(name: string, fn: () => T): T;
|
|
114
114
|
|
|
115
|
-
type AnyFn = (...args: any[]) => any;
|
|
115
|
+
type AnyFn$1 = (...args: any[]) => any;
|
|
116
116
|
/** MongoDB's raw read unit — documents returned by a read operation. A count. */
|
|
117
117
|
declare const MONGO_READ_UNIT = "mongo.docs_read";
|
|
118
118
|
/**
|
|
@@ -124,19 +124,19 @@ interface MongoClasses {
|
|
|
124
124
|
/** find() cursor — `.toArray()` resolves the matched documents. */
|
|
125
125
|
FindCursor?: {
|
|
126
126
|
prototype: {
|
|
127
|
-
toArray?: AnyFn;
|
|
127
|
+
toArray?: AnyFn$1;
|
|
128
128
|
};
|
|
129
129
|
};
|
|
130
130
|
/** aggregate() cursor — `.toArray()` resolves the pipeline output documents. */
|
|
131
131
|
AggregationCursor?: {
|
|
132
132
|
prototype: {
|
|
133
|
-
toArray?: AnyFn;
|
|
133
|
+
toArray?: AnyFn$1;
|
|
134
134
|
};
|
|
135
135
|
};
|
|
136
136
|
/** Collection — `.findOne()` resolves a single document (or null). */
|
|
137
137
|
Collection?: {
|
|
138
138
|
prototype: {
|
|
139
|
-
findOne?: AnyFn;
|
|
139
|
+
findOne?: AnyFn$1;
|
|
140
140
|
};
|
|
141
141
|
};
|
|
142
142
|
}
|
|
@@ -146,6 +146,29 @@ interface MongoClasses {
|
|
|
146
146
|
*/
|
|
147
147
|
declare function installMongoMeter(classes: MongoClasses): void;
|
|
148
148
|
|
|
149
|
+
type AnyFn = (...args: any[]) => any;
|
|
150
|
+
/** Postgres's raw read unit — rows returned by a SELECT. A count. */
|
|
151
|
+
declare const PG_READ_UNIT = "postgres.rows_read";
|
|
152
|
+
/**
|
|
153
|
+
* The `pg` driver class to patch. Pass `Client` from your `pg` import; `Pool.query`
|
|
154
|
+
* delegates to it, so this single patch covers pool usage too. Only the prototype
|
|
155
|
+
* present is patched, so a driver-version mismatch degrades to "counts nothing",
|
|
156
|
+
* never a crash.
|
|
157
|
+
*/
|
|
158
|
+
interface PgClasses {
|
|
159
|
+
/** node-postgres Client — `.query()` runs a statement and resolves a Result. */
|
|
160
|
+
Client?: {
|
|
161
|
+
prototype: {
|
|
162
|
+
query?: AnyFn;
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Install the Postgres read meter on `Client.prototype.query`. Call ONCE at process
|
|
168
|
+
* start, before any reads. Pass the `Client` class from your `pg` import.
|
|
169
|
+
*/
|
|
170
|
+
declare function installPgMeter(classes: PgClasses): void;
|
|
171
|
+
|
|
149
172
|
declare const DEFAULT_MIRROR_DIR = ".crossdeck";
|
|
150
173
|
/**
|
|
151
174
|
* Wraps an optional upstream sink. On each flush it writes the running day-total
|
|
@@ -227,4 +250,4 @@ interface InitOptions {
|
|
|
227
250
|
*/
|
|
228
251
|
declare function init(options?: InitOptions): void;
|
|
229
252
|
|
|
230
|
-
export { BucketsReport, type CostHint, type CostTag, DEFAULT_MIRROR_DIR, type FirestoreClasses, type InitOptions, MONGO_READ_UNIT, type MeterConfig, MirrorSink, type MongoClasses, type OpType, READOUT_FOOTER, type ResourceUnit, Sink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, installMongoMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, renderReadout, runWithCostTag };
|
|
253
|
+
export { BucketsReport, type CostHint, type CostTag, DEFAULT_MIRROR_DIR, type FirestoreClasses, type InitOptions, MONGO_READ_UNIT, type MeterConfig, MirrorSink, type MongoClasses, type OpType, PG_READ_UNIT, type PgClasses, READOUT_FOOTER, type ResourceUnit, Sink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, installMongoMeter, installPgMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, renderReadout, runWithCostTag };
|
package/dist/index.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ declare function recordDeletes(n?: number): void;
|
|
|
40
40
|
*/
|
|
41
41
|
declare function flush(): Promise<void>;
|
|
42
42
|
|
|
43
|
-
type AnyFn$
|
|
43
|
+
type AnyFn$2 = (...args: any[]) => any;
|
|
44
44
|
/**
|
|
45
45
|
* The firebase-admin Firestore classes to patch. Pass the module namespace from
|
|
46
46
|
* `firebase-admin/firestore` — only the prototypes present are patched.
|
|
@@ -48,31 +48,31 @@ type AnyFn$1 = (...args: any[]) => any;
|
|
|
48
48
|
interface FirestoreClasses {
|
|
49
49
|
Query?: {
|
|
50
50
|
prototype: {
|
|
51
|
-
get?: AnyFn$
|
|
52
|
-
onSnapshot?: AnyFn$
|
|
51
|
+
get?: AnyFn$2;
|
|
52
|
+
onSnapshot?: AnyFn$2;
|
|
53
53
|
};
|
|
54
54
|
};
|
|
55
55
|
DocumentReference?: {
|
|
56
56
|
prototype: {
|
|
57
|
-
get?: AnyFn$
|
|
58
|
-
onSnapshot?: AnyFn$
|
|
57
|
+
get?: AnyFn$2;
|
|
58
|
+
onSnapshot?: AnyFn$2;
|
|
59
59
|
};
|
|
60
60
|
};
|
|
61
61
|
Transaction?: {
|
|
62
62
|
prototype: {
|
|
63
|
-
get?: AnyFn$
|
|
64
|
-
getAll?: AnyFn$
|
|
63
|
+
get?: AnyFn$2;
|
|
64
|
+
getAll?: AnyFn$2;
|
|
65
65
|
};
|
|
66
66
|
};
|
|
67
67
|
Firestore?: {
|
|
68
68
|
prototype: {
|
|
69
|
-
getAll?: AnyFn$
|
|
69
|
+
getAll?: AnyFn$2;
|
|
70
70
|
};
|
|
71
71
|
};
|
|
72
72
|
/** count() / sum() / average() — aggregation queries bill reads too. */
|
|
73
73
|
AggregateQuery?: {
|
|
74
74
|
prototype: {
|
|
75
|
-
get?: AnyFn$
|
|
75
|
+
get?: AnyFn$2;
|
|
76
76
|
};
|
|
77
77
|
};
|
|
78
78
|
}
|
|
@@ -112,7 +112,7 @@ declare function currentCostTag(): CostTag;
|
|
|
112
112
|
*/
|
|
113
113
|
declare function bucket<T>(name: string, fn: () => T): T;
|
|
114
114
|
|
|
115
|
-
type AnyFn = (...args: any[]) => any;
|
|
115
|
+
type AnyFn$1 = (...args: any[]) => any;
|
|
116
116
|
/** MongoDB's raw read unit — documents returned by a read operation. A count. */
|
|
117
117
|
declare const MONGO_READ_UNIT = "mongo.docs_read";
|
|
118
118
|
/**
|
|
@@ -124,19 +124,19 @@ interface MongoClasses {
|
|
|
124
124
|
/** find() cursor — `.toArray()` resolves the matched documents. */
|
|
125
125
|
FindCursor?: {
|
|
126
126
|
prototype: {
|
|
127
|
-
toArray?: AnyFn;
|
|
127
|
+
toArray?: AnyFn$1;
|
|
128
128
|
};
|
|
129
129
|
};
|
|
130
130
|
/** aggregate() cursor — `.toArray()` resolves the pipeline output documents. */
|
|
131
131
|
AggregationCursor?: {
|
|
132
132
|
prototype: {
|
|
133
|
-
toArray?: AnyFn;
|
|
133
|
+
toArray?: AnyFn$1;
|
|
134
134
|
};
|
|
135
135
|
};
|
|
136
136
|
/** Collection — `.findOne()` resolves a single document (or null). */
|
|
137
137
|
Collection?: {
|
|
138
138
|
prototype: {
|
|
139
|
-
findOne?: AnyFn;
|
|
139
|
+
findOne?: AnyFn$1;
|
|
140
140
|
};
|
|
141
141
|
};
|
|
142
142
|
}
|
|
@@ -146,6 +146,29 @@ interface MongoClasses {
|
|
|
146
146
|
*/
|
|
147
147
|
declare function installMongoMeter(classes: MongoClasses): void;
|
|
148
148
|
|
|
149
|
+
type AnyFn = (...args: any[]) => any;
|
|
150
|
+
/** Postgres's raw read unit — rows returned by a SELECT. A count. */
|
|
151
|
+
declare const PG_READ_UNIT = "postgres.rows_read";
|
|
152
|
+
/**
|
|
153
|
+
* The `pg` driver class to patch. Pass `Client` from your `pg` import; `Pool.query`
|
|
154
|
+
* delegates to it, so this single patch covers pool usage too. Only the prototype
|
|
155
|
+
* present is patched, so a driver-version mismatch degrades to "counts nothing",
|
|
156
|
+
* never a crash.
|
|
157
|
+
*/
|
|
158
|
+
interface PgClasses {
|
|
159
|
+
/** node-postgres Client — `.query()` runs a statement and resolves a Result. */
|
|
160
|
+
Client?: {
|
|
161
|
+
prototype: {
|
|
162
|
+
query?: AnyFn;
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Install the Postgres read meter on `Client.prototype.query`. Call ONCE at process
|
|
168
|
+
* start, before any reads. Pass the `Client` class from your `pg` import.
|
|
169
|
+
*/
|
|
170
|
+
declare function installPgMeter(classes: PgClasses): void;
|
|
171
|
+
|
|
149
172
|
declare const DEFAULT_MIRROR_DIR = ".crossdeck";
|
|
150
173
|
/**
|
|
151
174
|
* Wraps an optional upstream sink. On each flush it writes the running day-total
|
|
@@ -227,4 +250,4 @@ interface InitOptions {
|
|
|
227
250
|
*/
|
|
228
251
|
declare function init(options?: InitOptions): void;
|
|
229
252
|
|
|
230
|
-
export { BucketsReport, type CostHint, type CostTag, DEFAULT_MIRROR_DIR, type FirestoreClasses, type InitOptions, MONGO_READ_UNIT, type MeterConfig, MirrorSink, type MongoClasses, type OpType, READOUT_FOOTER, type ResourceUnit, Sink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, installMongoMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, renderReadout, runWithCostTag };
|
|
253
|
+
export { BucketsReport, type CostHint, type CostTag, DEFAULT_MIRROR_DIR, type FirestoreClasses, type InitOptions, MONGO_READ_UNIT, type MeterConfig, MirrorSink, type MongoClasses, type OpType, PG_READ_UNIT, type PgClasses, READOUT_FOOTER, type ResourceUnit, Sink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, installMongoMeter, installPgMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, renderReadout, runWithCostTag };
|
package/dist/index.js
CHANGED
|
@@ -430,6 +430,47 @@ function installMongoMeter(classes) {
|
|
|
430
430
|
};
|
|
431
431
|
}
|
|
432
432
|
}
|
|
433
|
+
var installed3 = false;
|
|
434
|
+
var PG_READ_UNIT = "postgres.rows_read";
|
|
435
|
+
function meterResult(res) {
|
|
436
|
+
try {
|
|
437
|
+
if (res && res.command === "SELECT" && Array.isArray(res.rows) && res.rows.length > 0) {
|
|
438
|
+
record(PG_READ_UNIT, res.rows.length);
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function installPgMeter(classes) {
|
|
444
|
+
if (installed3) return;
|
|
445
|
+
installed3 = true;
|
|
446
|
+
const proto = classes.Client?.prototype;
|
|
447
|
+
const real = proto?.query;
|
|
448
|
+
if (!proto || !real) return;
|
|
449
|
+
proto.query = function(...args) {
|
|
450
|
+
const last = args.length > 0 ? args[args.length - 1] : void 0;
|
|
451
|
+
if (typeof last === "function") {
|
|
452
|
+
const meterHere = async_hooks.AsyncResource.bind((res) => meterResult(res));
|
|
453
|
+
args[args.length - 1] = function(err, res) {
|
|
454
|
+
if (!err) {
|
|
455
|
+
try {
|
|
456
|
+
meterHere(res);
|
|
457
|
+
} catch {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return last.apply(this, arguments);
|
|
461
|
+
};
|
|
462
|
+
return real.apply(this, args);
|
|
463
|
+
}
|
|
464
|
+
const ret = real.apply(this, args);
|
|
465
|
+
if (ret && typeof ret.then === "function") {
|
|
466
|
+
return ret.then((res) => {
|
|
467
|
+
meterResult(res);
|
|
468
|
+
return res;
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
return ret;
|
|
472
|
+
};
|
|
473
|
+
}
|
|
433
474
|
|
|
434
475
|
// src/index.ts
|
|
435
476
|
function init(options = {}) {
|
|
@@ -443,6 +484,7 @@ exports.DEFAULT_MIRROR_DIR = DEFAULT_MIRROR_DIR;
|
|
|
443
484
|
exports.MONGO_READ_UNIT = MONGO_READ_UNIT;
|
|
444
485
|
exports.MirrorSink = MirrorSink;
|
|
445
486
|
exports.NullSink = NullSink;
|
|
487
|
+
exports.PG_READ_UNIT = PG_READ_UNIT;
|
|
446
488
|
exports.READOUT_FOOTER = READOUT_FOOTER;
|
|
447
489
|
exports.ReportSink = ReportSink;
|
|
448
490
|
exports.bucket = bucket;
|
|
@@ -453,6 +495,7 @@ exports.init = init;
|
|
|
453
495
|
exports.initBuckets = init;
|
|
454
496
|
exports.installFirestoreMeter = installFirestoreMeter;
|
|
455
497
|
exports.installMongoMeter = installMongoMeter;
|
|
498
|
+
exports.installPgMeter = installPgMeter;
|
|
456
499
|
exports.record = record;
|
|
457
500
|
exports.recordDeletes = recordDeletes;
|
|
458
501
|
exports.recordReads = recordReads;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cost-context.ts","../src/cost-meter.ts","../src/sink.ts","../src/readout.ts","../src/mirror.ts","../src/adapters/firestore.ts","../src/mongo/index.ts","../src/index.ts"],"names":["AsyncLocalStorage","join","readFileSync","mkdirSync","writeFileSync","installed","hintFrom","sink"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * cost-context — the request-scoped tag every counted operation attributes\n * itself to. Set it ONCE at a boundary (or wrap a path with `bucket()`); it\n * rides Node's AsyncLocalStorage down through every async fan-out, so one\n * handler that triggers 15 reads attributes all 15 to the same bucket — with\n * zero per-call-site work.\n *\n * Generic by design: unlike a hardcoded product taxonomy, the only meaningful\n * field a consumer sets is the free-form `label` (the bucket name). `feature`\n * is an optional coarse grouping if you want one; nothing here is\n * Crossdeck-specific.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nexport interface CostTag {\n /** Optional coarse grouping (a caller-defined surface name). */\n feature?: string;\n /** The bucket name — what `bucket()` sets. Drives the report's `byLabel`. */\n label?: string;\n}\n\nconst DEFAULT_TAG: CostTag = {};\nconst store = new AsyncLocalStorage<CostTag>();\n\n/** Run `fn` with `tag` bound for its entire async subtree. */\nexport function runWithCostTag<T>(tag: CostTag, fn: () => T): T {\n return store.run({ ...tag }, fn);\n}\n\n/** Bind a tag for the remainder of the current async context (no closure to wrap). */\nexport function enterCostTag(tag: CostTag): void {\n store.enterWith({ ...tag });\n}\n\n/** Refine the live tag in place (e.g. stamp a feature after the boundary). */\nexport function refineCostTag(patch: Partial<CostTag>): void {\n const cur = store.getStore();\n if (cur) Object.assign(cur, patch);\n}\n\n/** The current tag, or a safe empty default outside any bound context. */\nexport function currentCostTag(): CostTag {\n return store.getStore() ?? DEFAULT_TAG;\n}\n\n/** Hierarchy separator for bucket paths — Firestore-map-key-safe and distinct from\n * the \"col:\" leaf prefix. `bucket(\"a\", () => bucket(\"b\", …))` → \"a>b\". */\nexport const BUCKET_SEP = \">\";\n\n/**\n * `bucket(name, fn)` — the headline verb, the `track()` of cost. Run `fn` with\n * every operation inside it attributed to the bucket `name`; the attribution rides\n * the async subtree automatically. NESTS: a `bucket()` inside another COMPOSES into\n * a path (`\"analytics\" > \"rollup\"` → `\"analytics>rollup\"`), so the dashboard can\n * drill from the coarse bucket down into its parts. The one verb most developers touch:\n *\n * await bucket(\"analytics\", () =>\n * bucket(\"rollup\", () => db.collection(\"events\").where(...).get())); // → \"analytics>rollup>col:events\"\n */\nexport function bucket<T>(name: string, fn: () => T): T {\n const parent = currentCostTag().label;\n const path = parent ? `${parent}${BUCKET_SEP}${name}` : name;\n return runWithCostTag({ ...currentCostTag(), label: path }, fn);\n}\n","/**\n * cost-meter — counts operations against the ambient tag and flushes them to the\n * configured Sink cheaply.\n *\n * LOW-OVERHEAD CONTRACT (the thing that warns you about reads must not run them\n * up): counts accumulate in an in-memory buffer and flush periodically — NEVER one\n * network call per counted operation. A flush coalesces the whole window into one\n * report per UTC day and hands it to the Sink. At steady state that is ~1 small\n * request a minute, regardless of how many ops you served.\n *\n * BEST-EFFORT CONTRACT: metering must never throw into your code. Every recorder\n * swallows its own errors; a failed flush drops that window's counts (surfaced via\n * `onError` if you pass one) rather than disturbing the app.\n */\nimport { currentCostTag } from \"./cost-context\";\nimport type { Sink, BucketsReport, ResourceCounts } from \"./sink\";\n\n/**\n * A resource unit — what an adapter counts. Firestore emits `read`/`write`/\n * `delete`; other adapters emit their own (`clickhouse.query_ms`, `openai.tokens`).\n * It is a free identifier on purpose, BUT each one is kept entirely distinct: the\n * meter only ever sums quantities WITHIN a single resource, never across two.\n */\nexport type ResourceUnit = string;\n/** @deprecated — Firestore-era name; use {@link ResourceUnit}. */\nexport type OpType = ResourceUnit;\n\n/** Optional read-site hint — the collection touched, derived at the trap from the\n * path. Lets an UNtagged read cascade to `col:<collection>` instead of vanishing. */\nexport interface CostHint {\n collection?: string;\n projectId?: string;\n}\n\n// NUL separator — a bucket/collection name can contain almost anything except\n// this, so the key never collides with a name that has a \"|\" or \":\" in it.\nconst SEP = \"\\u001f\"; // ASCII Unit Separator\n\n/** key = date <NUL> resource <NUL> label → count */\nconst labelBuffer = new Map<string, number>();\n/** key = date <NUL> resource <NUL> hour → count */\nconst hourBuffer = new Map<string, number>();\n/** key = date <NUL> resource <NUL> 5-min-slot (\"HHMM\") → count. The fine grain\n * that lets a developer verify against the provider console's \"last hour\" within\n * minutes of installing — not after a day. Still one maintained doc, zero reads. */\nconst minuteBuffer = new Map<string, number>();\n\nlet sink: Sink | null = null;\nlet flushIntervalMs = 60_000;\nlet onError: ((e: unknown) => void) | null = null;\nlet timer: ReturnType<typeof setInterval> | null = null;\nlet flushing = false;\n/** Safety valve — flush early if a burst fills the buffer between intervals. */\nconst MAX_BUFFER_KEYS = 5_000;\n\nexport interface MeterConfig {\n sink: Sink;\n flushIntervalMs?: number;\n onError?: (e: unknown) => void;\n}\n\n/** Point the meter at a sink. Called by `init()`; pass your own sink to self-host. */\nexport function configureMeter(config: MeterConfig): void {\n sink = config.sink;\n if (config.flushIntervalMs && config.flushIntervalMs > 0) flushIntervalMs = config.flushIntervalMs;\n onError = config.onError ?? null;\n}\n\nconst utcDate = (): string => new Date().toISOString().slice(0, 10);\nconst utcHour = (): string => new Date().toISOString().slice(11, 13);\n/** UTC 5-minute slot of the day as \"HHMM\" — the slot START (e.g. 08:47 → \"0845\"). */\nconst utcFiveMin = (): string => {\n const iso = new Date().toISOString();\n const hh = iso.slice(11, 13);\n const mm = Math.floor(Number(iso.slice(14, 16)) / 5) * 5;\n return hh + String(mm).padStart(2, \"0\");\n};\n\nfunction ensureFlushLoop(): void {\n if (timer) return;\n timer = setInterval(() => void flush(), flushIntervalMs);\n // Don't keep the event loop alive just for metering.\n (timer as { unref?: () => void }).unref?.();\n // Flush the last window on shutdown.\n process.once?.(\"SIGTERM\", () => void flush());\n process.once?.(\"beforeExit\", () => void flush());\n}\n\n/**\n * Count `quantity` of a `resource` against the live tag. THE adapter primitive —\n * a Firestore adapter records \"read\"; a ClickHouse adapter records\n * \"clickhouse.query_ms\"; an OpenAI adapter records \"openai.tokens\". Each resource\n * is bucketed entirely on its own; nothing is ever added across resources. Never\n * throws.\n */\nexport function record(resource: ResourceUnit, quantity: number, hint?: CostHint): void {\n try {\n if (!Number.isFinite(quantity) || quantity <= 0) return;\n const t = currentCostTag();\n const date = utcDate();\n // HIERARCHY — the bucket path is the trunk; the collection is the LEAF kept\n // beneath it (so a tagged bucket still drills down to which collections it\n // read). \"analytics\" + events → \"analytics>col:events\"; untagged → \"col:events\";\n // nothing derivable → \"uncategorized\". A unit is never invisible, and a tagged\n // one never loses where it actually went.\n const coll = hint?.collection ? `col:${hint.collection}` : null;\n const label = t.label\n ? coll\n ? `${t.label}>${coll}`\n : t.label\n : coll ?? \"uncategorized\";\n // Key includes the resource, so each resource accumulates in its OWN slot.\n const lk = date + SEP + resource + SEP + label;\n labelBuffer.set(lk, (labelBuffer.get(lk) ?? 0) + quantity);\n const hk = date + SEP + resource + SEP + utcHour();\n hourBuffer.set(hk, (hourBuffer.get(hk) ?? 0) + quantity);\n const mk = date + SEP + resource + SEP + utcFiveMin();\n minuteBuffer.set(mk, (minuteBuffer.get(mk) ?? 0) + quantity);\n ensureFlushLoop();\n if (labelBuffer.size + hourBuffer.size + minuteBuffer.size > MAX_BUFFER_KEYS) void flush();\n } catch {\n /* metering is best-effort — never disturb the caller */\n }\n}\n\n/** @deprecated — use {@link record}. Firestore-era alias. */\nexport const recordFirestore = record;\n\n/** Firestore bills a minimum of one read even for an empty result, so 0 counts as 1. */\nexport function recordReads(n: number, hint?: CostHint): void {\n record(\"read\", Math.max(n, 1), hint);\n}\nexport function recordWrites(n = 1): void {\n record(\"write\", n);\n}\nexport function recordDeletes(n = 1): void {\n record(\"delete\", n);\n}\n\nfunction add(target: Record<string, ResourceCounts>, key: string, resource: ResourceUnit, n: number): void {\n const bag = (target[key] ??= {});\n // Accumulate WITHIN this resource only — never merge resources.\n bag[resource] = (bag[resource] ?? 0) + n;\n}\n\n/**\n * Coalesce the buffer into one report per UTC day and hand each to the Sink.\n * Snapshots + clears up front so concurrent records land in the next window.\n * Never throws; a sink failure drops that window (surfaced via `onError`).\n */\nexport async function flush(): Promise<void> {\n if (flushing) return;\n // Not configured (init not called) — drop, don't grow unbounded.\n if (!sink) {\n labelBuffer.clear();\n hourBuffer.clear();\n minuteBuffer.clear();\n return;\n }\n if (labelBuffer.size === 0 && hourBuffer.size === 0 && minuteBuffer.size === 0) return;\n flushing = true;\n\n const labels = new Map(labelBuffer);\n const hours = new Map(hourBuffer);\n const minutes = new Map(minuteBuffer);\n labelBuffer.clear();\n hourBuffer.clear();\n minuteBuffer.clear();\n\n try {\n const byDate = new Map<string, BucketsReport>();\n const reportFor = (date: string): BucketsReport => {\n let r = byDate.get(date);\n if (!r) {\n r = { date, byLabel: {}, byHour: {}, byMinute: {} };\n byDate.set(date, r);\n }\n return r;\n };\n for (const [k, n] of labels) {\n const [date, op, label] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byLabel, label, op, n);\n }\n for (const [k, n] of hours) {\n const [date, op, hour] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byHour!, hour, op, n);\n }\n for (const [k, n] of minutes) {\n const [date, op, slot] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byMinute!, slot, op, n);\n }\n for (const report of byDate.values()) {\n await sink.flush(report);\n }\n } catch (e) {\n // Drop this window rather than risk a partial/double report on retry.\n onError?.(e);\n } finally {\n flushing = false;\n }\n}\n","/**\n * sink — where the meter sends a coalesced rollup, and the wire shape it sends.\n *\n * Abstracting the sink is what makes Buckets storage-agnostic: the meter never\n * knows where counts go. The DEFAULT sink (`ReportSink`) reports up to Crossdeck's\n * ingest endpoint so the numbers surface on your Crossdeck dashboard. A team that\n * wants to self-host can implement `Sink` against anything (Postgres, a file, your\n * own API) without touching the meter.\n */\n\n/**\n * Counts for ONE bucket, keyed by RESOURCE UNIT — the raw quantity of each unit,\n * nothing more (no money; you verify cost on your provider's bill).\n *\n * Firestore, the first adapter, emits `read` / `write` / `delete`. Other adapters\n * emit their own units (`clickhouse.query_ms`, `openai.tokens`, …). The rule that\n * keeps this honest: **each resource is its own line. Counts are only ever summed\n * WITHIN a resource, NEVER across one.** A read is not a query-millisecond; the two\n * never land in the same number. (There is deliberately no \"total units\" field.)\n */\nexport interface ResourceCounts {\n read?: number;\n write?: number;\n delete?: number;\n /** Any other resource unit an adapter emits — kept distinct, never merged. */\n [resource: string]: number | undefined;\n}\n\n/** @deprecated name — kept for back-compat; use {@link ResourceCounts}. */\nexport type OpCounts = ResourceCounts;\n\n/**\n * One coalesced report — the wire contract (see docs/ROLLUP_SCHEMA.md). The meter\n * produces one of these per UTC day in a flush window (usually exactly one).\n */\nexport interface BucketsReport {\n /** UTC day \"YYYY-MM-DD\". */\n date: string;\n /** bucket name → counts. The heart of the report. */\n byLabel: Record<string, ResourceCounts>;\n /** UTC hour \"HH\" → counts, for the hourly \"did my fix land this hour?\" view. */\n byHour?: Record<string, ResourceCounts>;\n /** UTC 5-minute slot \"HHMM\" (slot start) → counts. The fine grain for fast\n * verification against a provider console's \"last hour\" view. */\n byMinute?: Record<string, ResourceCounts>;\n}\n\n/**\n * A destination for coalesced rollups. `flush` MAY throw on failure — the meter\n * catches it, drops that one window, and never lets it reach your app.\n */\nexport interface Sink {\n flush(report: BucketsReport): Promise<void>;\n}\n\nexport interface ReportSinkConfig {\n /** The project's `cd_sk_` secret key. Server-to-server only. */\n apiKey: string;\n /** Defaults to https://api.cross-deck.com/v1/buckets/report */\n endpoint?: string;\n /** Request timeout (ms); a slow Crossdeck must never stall your flush. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_ENDPOINT = \"https://api.cross-deck.com/v1/buckets/report\";\n\n/**\n * The default sink: POST one coalesced rollup to Crossdeck's ingest endpoint.\n * The ingest folds it into the day's maintained doc with `increment`, so many\n * reports a minute coalesce safely. This path does ZERO database reads — it sends\n * a summary, it does not read. Throws on a non-202 so the meter can log/drop the\n * window; the meter guarantees it never reaches your app.\n */\nexport class ReportSink implements Sink {\n private readonly endpoint: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n\n constructor(config: ReportSinkConfig) {\n this.endpoint = config.endpoint ?? DEFAULT_ENDPOINT;\n this.apiKey = config.apiKey;\n this.timeoutMs = config.timeoutMs ?? 5000;\n }\n\n async flush(report: BucketsReport): Promise<void> {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n signal: AbortSignal.timeout(this.timeoutMs),\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(report),\n });\n if (res.status !== 202) {\n throw new Error(`Buckets report rejected: HTTP ${res.status}`);\n }\n }\n}\n\n/**\n * A sink that does nothing. Used when the local mirror is turned off AND no upstream\n * was configured — the meter still needs a sink to flush into, but there's nowhere to\n * send. Counts are dropped by the developer's explicit choice.\n */\nexport class NullSink implements Sink {\n async flush(): Promise<void> {\n /* intentionally empty */\n }\n}\n","/**\n * readout — renders the local file a developer (or their AI session) reads back with\n * \"read me my buckets\". PURE string building: no I/O, no database reads. The node\n * mirror (./mirror) writes this to `.crossdeck/buckets.md` on each flush, so the\n * readout works offline, for free, with no account.\n */\nimport type { BucketsReport, ResourceCounts } from \"./sink\";\n\n/**\n * The one line that closes every readout. Plain and factual: what the OSS shows you\n * here, and what signing up adds — for free. No invented numbers, no urgency, no pitch.\n */\nexport const READOUT_FOOTER =\n \"Buckets OSS shows the reads on this surface. Sign up to Crossdeck (free) to see \" +\n \"every surface in one view, drill any bucket down to the exact query, track a fix \" +\n \"before and after, and get paged when reads spike — cross-deck.com\";\n\nfunction fmt(n: number): string {\n if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + \"M\";\n if (n >= 10_000) return Math.round(n / 1000) + \"K\";\n if (n >= 1000) return (n / 1000).toFixed(1) + \"K\";\n return String(Math.round(n));\n}\n\n/** A bucket is untagged when its ROOT segment is a bare collection / catch-all. */\nfunction isUntagged(label: string): boolean {\n const root = label.split(\">\")[0];\n return root.startsWith(\"col:\") || root === \"uncategorized\" || root === \"unknown\";\n}\n\n/** Pretty path: strip the \"col:\" leaf prefix, join the hierarchy with \" › \". */\nfunction displayLabel(label: string): string {\n return label\n .split(\">\")\n .map((s) => (s.startsWith(\"col:\") ? s.slice(4) : s))\n .join(\" › \");\n}\n\n/** Render the day's coalesced report as a human/AI-readable markdown readout. */\nexport function renderReadout(report: BucketsReport): string {\n const entries = Object.entries(report.byLabel ?? {})\n .map(([label, counts]) => ({ label, reads: (counts as ResourceCounts).read ?? 0 }))\n .filter((e) => e.reads > 0)\n .sort((a, b) => b.reads - a.reads);\n\n const total = entries.reduce((s, e) => s + e.reads, 0);\n const out: string[] = [];\n out.push(`# Buckets — reads on this surface`);\n out.push(``);\n out.push(`**${fmt(total)} reads** · ${report.date} (UTC)`);\n out.push(``);\n\n if (entries.length === 0) {\n out.push(`No reads metered yet — install the collector and let your app serve some traffic.`);\n } else {\n out.push(`| bucket | named | reads |`);\n out.push(`| --- | :---: | ---: |`);\n for (const e of entries) {\n out.push(`| ${displayLabel(e.label)} | ${isUntagged(e.label) ? \"—\" : \"✓\"} | ${fmt(e.reads)} |`);\n }\n }\n\n out.push(``);\n out.push(`---`);\n out.push(READOUT_FOOTER);\n out.push(``);\n return out.join(\"\\n\");\n}\n","/**\n * mirror — tees every coalesced report to a local file so \"read me my buckets\" works\n * offline, free, with no account. Writes a human/AI-readable readout\n * (`.crossdeck/buckets.md`) plus the raw report (`.crossdeck/buckets.json`).\n *\n * NODE ONLY — never imported by the browser build (it touches the filesystem).\n *\n * Two contracts it keeps:\n * - NO MONSTER: it only ever WRITES local files (~one small write a minute). It never\n * reads your database; the report is already in hand.\n * - BEST-EFFORT: a write error is swallowed — the local mirror must never disturb a\n * flush or reach your app.\n *\n * The meter hands each flush a DELTA (the window's counts, then clears). To show the\n * day's running total, the mirror accumulates deltas in memory, seeded once from any\n * existing file so a process restart doesn't shrink the readout.\n */\nimport { mkdirSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Sink, BucketsReport, ResourceCounts } from \"./sink\";\nimport { renderReadout } from \"./readout\";\n\nexport const DEFAULT_MIRROR_DIR = \".crossdeck\";\n\nfunction mergeInto(target: Record<string, ResourceCounts>, src?: Record<string, ResourceCounts>): void {\n if (!src) return;\n for (const [key, counts] of Object.entries(src)) {\n const bag = (target[key] ??= {});\n for (const [res, n] of Object.entries(counts)) {\n if (typeof n === \"number\") bag[res] = (bag[res] ?? 0) + n;\n }\n }\n}\n\n/**\n * Wraps an optional upstream sink. On each flush it writes the running day-total\n * locally, THEN (if an upstream sink was given — i.e. a key) reports onward.\n * With no upstream it is a pure local meter: the wedge, working with no account.\n */\nexport class MirrorSink implements Sink {\n private acc: BucketsReport | null = null;\n private announced = false;\n private seeded = false;\n\n constructor(\n private readonly upstream: Sink | null,\n private readonly dir: string = DEFAULT_MIRROR_DIR,\n ) {}\n\n private jsonPath(): string {\n return join(this.dir, \"buckets.json\");\n }\n\n /** Seed the running total once from an existing same-day file (survives restarts). */\n private seed(date: string): void {\n if (this.seeded) return;\n this.seeded = true;\n try {\n const prior = JSON.parse(readFileSync(this.jsonPath(), \"utf8\")) as BucketsReport;\n if (prior?.date === date && prior.byLabel) this.acc = prior;\n } catch {\n /* no prior file (or unreadable) — start fresh */\n }\n }\n\n async flush(report: BucketsReport): Promise<void> {\n // Local first — the part that always works, key or no key.\n try {\n this.seed(report.date);\n if (!this.acc || this.acc.date !== report.date) {\n this.acc = { date: report.date, byLabel: {}, byHour: {}, byMinute: {} };\n }\n mergeInto(this.acc.byLabel, report.byLabel);\n mergeInto((this.acc.byHour ??= {}), report.byHour);\n mergeInto((this.acc.byMinute ??= {}), report.byMinute);\n\n mkdirSync(this.dir, { recursive: true });\n writeFileSync(join(this.dir, \"buckets.md\"), renderReadout(this.acc));\n writeFileSync(this.jsonPath(), JSON.stringify(this.acc, null, 2));\n\n if (!this.announced) {\n this.announced = true;\n // One quiet line, once, so a developer knows where to read it back.\n // eslint-disable-next-line no-console\n console.log(\n `Buckets: readout at ${join(this.dir, \"buckets.md\")} — open it, or ask your AI session to \"read me my buckets\".`,\n );\n }\n } catch {\n /* local mirror is best-effort */\n }\n\n if (this.upstream) await this.upstream.flush(report);\n }\n}\n","/**\n * adapters/firestore — the universal Firestore read meter (the trap).\n *\n * THE LESSON (learned on a real product): per-call-site `recordReads()`\n * instrumentation MISSES paths. You meter the read sites you're looking at and\n * leave the cron / trigger / ingest path uncounted — often the majority of reads,\n * invisible. Humans tag what they see and miss the path that matters.\n *\n * THE FIX: patch the admin SDK's read methods ONCE. From install onward, EVERY\n * read — anywhere, on any code path — is counted under the ambient tag, with zero\n * per-call-site work and no blind spots.\n *\n * SAFETY CONTRACT — this sits on your production read path, so it is defensive by\n * construction. Each wrapper:\n * 1. calls the REAL method first and captures the result,\n * 2. counts in a try/catch that can never throw into the caller,\n * 3. ALWAYS returns the real result, untouched.\n * It cannot break a read, change a result, or add latency beyond one in-memory\n * counter increment. A wrong count is a measurement error, never a correctness or\n * availability one. Idempotent — calling it twice patches once.\n *\n * COUNTING MODEL — a query returning N docs = N reads (an empty result still bills\n * 1, which the meter enforces). A document get = 1. getAll(...) = the ref count.\n * CollectionReference.get IS Query.get (shared prototype method), so patching Query\n * covers collections with no double-count.\n */\nimport { recordReads, type CostHint } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/**\n * The firebase-admin Firestore classes to patch. Pass the module namespace from\n * `firebase-admin/firestore` — only the prototypes present are patched.\n */\nexport interface FirestoreClasses {\n Query?: { prototype: { get?: AnyFn; onSnapshot?: AnyFn } };\n DocumentReference?: { prototype: { get?: AnyFn; onSnapshot?: AnyFn } };\n Transaction?: { prototype: { get?: AnyFn; getAll?: AnyFn } };\n Firestore?: { prototype: { getAll?: AnyFn } };\n /** count() / sum() / average() — aggregation queries bill reads too. */\n AggregateQuery?: { prototype: { get?: AnyFn } };\n}\n\n/** `projects/{id}/…` → the project id, else undefined. Pure string op. */\nfunction projectFromPath(path: string): string | undefined {\n const parts = path.split(\"/\");\n const i = parts.indexOf(\"projects\");\n return i >= 0 && parts[i + 1] ? parts[i + 1] : undefined;\n}\n\n/**\n * Derive { collection, projectId } from the read target's path so an UNtagged read\n * cascades to `col:<collection>` instead of \"uncategorized\". PURE CPU; never reads,\n * never throws. Falls back to firebase-admin's internal `_queryOptions` for filtered\n * queries (which don't expose `.path`).\n */\nfunction hintFrom(target: any): CostHint | undefined {\n try {\n const p = typeof target?.path === \"string\" ? target.path : \"\";\n if (p) {\n const parts = p.split(\"/\").filter(Boolean);\n const collection = parts.length % 2 === 0 ? parts[parts.length - 2] : parts[parts.length - 1];\n return { collection, projectId: projectFromPath(p) };\n }\n const qo = target?._queryOptions;\n if (qo) {\n const collection = typeof qo.collectionId === \"string\" ? qo.collectionId : undefined;\n const parent =\n typeof qo.parentPath?.relativeName === \"string\"\n ? qo.parentPath.relativeName\n : typeof qo.parentPath?.toString === \"function\"\n ? String(qo.parentPath.toString())\n : \"\";\n return { collection, projectId: parent ? projectFromPath(parent) : undefined };\n }\n } catch {\n /* the meter must never throw */\n }\n return undefined;\n}\n\nfunction meterSnap(snap: unknown, hint?: CostHint): void {\n try {\n const size = (snap as { size?: number } | null)?.size;\n recordReads(typeof size === \"number\" ? size : 1, hint);\n } catch {\n /* best-effort */\n }\n}\nfunction meterCount(n: number, hint?: CostHint): void {\n try {\n recordReads(n, hint);\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * Install the universal read meter on the firebase-admin Firestore classes. Call\n * ONCE at process start, before any reads. Pass the namespace from\n * `firebase-admin/firestore` so the exact prototypes the SDK uses are patched:\n *\n * import * as Firestore from \"firebase-admin/firestore\";\n * installFirestoreMeter(Firestore);\n */\nexport function installFirestoreMeter(classes: FirestoreClasses): void {\n if (installed) return;\n installed = true;\n const { Query, DocumentReference, Transaction, Firestore, AggregateQuery } = classes;\n\n // Query.get — covers Query AND CollectionReference (shared prototype method).\n const qGet = Query?.prototype?.get;\n if (qGet) {\n Query!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await qGet.apply(this, args);\n meterSnap(snap, hintFrom(this));\n return snap;\n };\n }\n\n // DocumentReference.get — a single doc = 1 read.\n const dGet = DocumentReference?.prototype?.get;\n if (dGet) {\n DocumentReference!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await dGet.apply(this, args);\n meterCount(1, hintFrom(this));\n return snap;\n };\n }\n\n // Transaction.get — query or doc; size when present, else 1.\n const tGet = Transaction?.prototype?.get;\n if (tGet) {\n Transaction!.prototype.get = async function (this: unknown, ...args: any[]) {\n const res = await tGet.apply(this, args);\n meterSnap(res, hintFrom(args[0]));\n return res;\n };\n }\n\n // Transaction.getAll(...refs) — one read per ref.\n const tGetAll = Transaction?.prototype?.getAll;\n if (tGetAll) {\n Transaction!.prototype.getAll = async function (this: unknown, ...args: any[]) {\n const res = await tGetAll.apply(this, args);\n meterCount(Array.isArray(res) ? res.length : args.length || 1, hintFrom(args[0]));\n return res;\n };\n }\n\n // Firestore.getAll(...refs) — batched doc reads.\n const fGetAll = Firestore?.prototype?.getAll;\n if (fGetAll) {\n Firestore!.prototype.getAll = async function (this: unknown, ...args: any[]) {\n const res = await fGetAll.apply(this, args);\n meterCount(Array.isArray(res) ? res.length : args.length || 1, hintFrom(args[0]));\n return res;\n };\n }\n\n // AggregateQuery.get — count() / sum() / average(). Firestore bills one read per\n // up to 1000 index entries scanned, and the client never sees that entry count —\n // so this is an HONEST ESTIMATE: for count() we derive it from the result\n // (ceil(count / 1000)), else the billed minimum of 1. Observe-only: it reads the\n // result you already got, adds zero reads. (Closes the aggregation blind spot.)\n const aGet = AggregateQuery?.prototype?.get;\n if (aGet) {\n AggregateQuery!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await aGet.apply(this, args);\n try {\n const data = (snap as { data?: () => { count?: number } } | null)?.data?.();\n const count = typeof data?.count === \"number\" ? data.count : 0;\n meterCount(Math.max(1, Math.ceil(count / 1000)), hintFrom(this));\n } catch {\n /* best-effort */\n }\n return snap;\n };\n }\n\n // Query.onSnapshot / DocumentReference.onSnapshot — server-side realtime\n // listeners. We OBSERVE: wrap onNext and count the docs each fire delivers (a\n // query's changed docs — first fire = all matching; a doc = 1). We attach no\n // listener and issue no read; the meter just sees what the listener is already\n // billed. (Closes the server-listener blind spot.)\n const patchOnSnapshot = (proto: { onSnapshot?: AnyFn } | undefined, perDoc: boolean): void => {\n const real = proto?.onSnapshot;\n if (!real) return;\n proto!.onSnapshot = function (this: unknown, ...args: any[]) {\n const hint = hintFrom(this);\n const i = args.findIndex((a) => typeof a === \"function\");\n if (i >= 0) {\n const onNext = args[i];\n args[i] = function (snap: any) {\n try {\n const n = perDoc\n ? 1\n : typeof snap?.docChanges === \"function\"\n ? snap.docChanges().length\n : typeof snap?.size === \"number\"\n ? snap.size\n : 1;\n if (n > 0) meterCount(n, hint);\n } catch {\n /* best-effort */\n }\n return onNext(snap);\n };\n }\n return real.apply(this, args);\n };\n };\n patchOnSnapshot(Query?.prototype, false);\n patchOnSnapshot(DocumentReference?.prototype, true);\n}\n\n/** Test-only: reset the install guard so a suite can re-patch fresh prototypes. */\nexport function __resetFirestoreMeterForTests(): void {\n installed = false;\n}\n","/**\n * adapters/mongo — the MongoDB read meter (the trap), mirroring the Firestore one.\n *\n * THE RAW UNIT: like Firestore (a query returning N docs = N reads), MongoDB's raw\n * unit is DOCUMENTS READ — the documents each read operation returns, attributed to\n * the feature (bucket) that ran it. This is a real, countable number, NOT a dollar\n * bill. (MongoDB bills by cluster/compute, not per read — so this is the read LOAD by\n * feature: which queries pull the most documents, the thing you index/narrow/cache to\n * run a smaller cluster.) Raw counts only, no money — the two laws hold.\n *\n * THE FIX it brings: per-call-site instrumentation misses paths. Patch the driver's\n * result-returning read methods ONCE, and from install on every read — anywhere, any\n * code path — is counted under the ambient tag with no blind spots.\n *\n * MECHANISM: the wrappers run in the CALLER's own async context (a `find().toArray()`\n * inside `bucket(\"feed\")` resolves in that context), so attribution survives with zero\n * per-call-site work. Observe-only: it counts the result already in hand — it adds NO\n * query (no `explain()`, no profiler scan), so it can never become a read monster.\n *\n * SAFETY CONTRACT (it sits on your production read path): each wrapper calls the REAL\n * method first, counts in a try/catch that can never throw into the caller, and ALWAYS\n * returns the real result untouched. A wrong count is a measurement error, never a\n * correctness or availability one. Idempotent — calling it twice patches once.\n *\n * Pass the classes from your `mongodb` import (an OPTIONAL peer dep — installing this\n * never forces the driver on a Firestore user):\n *\n * import { FindCursor, AggregationCursor, Collection } from \"mongodb\";\n * import { installMongoMeter } from \"@cross-deck/buckets/mongo\";\n * installMongoMeter({ FindCursor, AggregationCursor, Collection });\n */\nimport { record, type CostHint } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/** MongoDB's raw read unit — documents returned by a read operation. A count. */\nexport const MONGO_READ_UNIT = \"mongo.docs_read\";\n\n/**\n * The `mongodb` driver classes to patch. Pass them from your `mongodb` import — only\n * the prototypes present are patched, so a driver-version mismatch degrades to\n * \"counts fewer paths\", never a crash.\n */\nexport interface MongoClasses {\n /** find() cursor — `.toArray()` resolves the matched documents. */\n FindCursor?: { prototype: { toArray?: AnyFn } };\n /** aggregate() cursor — `.toArray()` resolves the pipeline output documents. */\n AggregationCursor?: { prototype: { toArray?: AnyFn } };\n /** Collection — `.findOne()` resolves a single document (or null). */\n Collection?: { prototype: { findOne?: AnyFn } };\n}\n\n/** Best-effort `col:<collection>` cascade for an UNtagged read. PURE; never throws. */\nfunction hintFrom(target: any): CostHint | undefined {\n try {\n // FindCursor / AggregationCursor expose `.namespace` (MongoDBNamespace); a\n // Collection exposes `.collectionName`. Either gives us the collection.\n const ns = target?.namespace;\n if (ns && typeof ns.collection === \"string\" && ns.collection) {\n return { collection: ns.collection };\n }\n if (typeof target?.collectionName === \"string\" && target.collectionName) {\n return { collection: target.collectionName };\n }\n } catch {\n /* the meter must never throw */\n }\n return undefined;\n}\n\nfunction meter(n: number, hint?: CostHint): void {\n try {\n if (n > 0) record(MONGO_READ_UNIT, n, hint);\n } catch {\n /* best-effort — never disturb the caller */\n }\n}\n\n/**\n * Install the MongoDB read meter on the driver's read-result methods. Call ONCE at\n * process start, before any reads. Pass the classes from your `mongodb` import.\n */\nexport function installMongoMeter(classes: MongoClasses): void {\n if (installed) return;\n installed = true;\n const { FindCursor, AggregationCursor, Collection } = classes;\n\n // find().toArray() / aggregate().toArray() — the documents the query returned.\n const patchToArray = (proto: { toArray?: AnyFn } | undefined): void => {\n const real = proto?.toArray;\n if (!real) return;\n proto!.toArray = async function (this: unknown, ...args: any[]) {\n const out = await real.apply(this, args);\n meter(Array.isArray(out) ? out.length : 0, hintFrom(this));\n return out;\n };\n };\n patchToArray(FindCursor?.prototype);\n patchToArray(AggregationCursor?.prototype);\n\n // findOne() — one document (or null). A found doc is 1 read; null returned nothing.\n const realFindOne = Collection?.prototype?.findOne;\n if (realFindOne) {\n Collection!.prototype.findOne = async function (this: unknown, ...args: any[]) {\n const out = await realFindOne.apply(this, args);\n meter(out == null ? 0 : 1, hintFrom(this));\n return out;\n };\n }\n}\n\n/** Test-only: reset the install guard so a suite can re-patch fresh prototypes. */\nexport function __resetMongoMeterForTests(): void {\n installed = false;\n}\n","/**\n * @cross-deck/buckets — know exactly what every database read costs you, and who\n * caused it. A tiny, never-throws collector for Firestore.\n *\n * The whole footprint a consumer sees:\n * 1. init({ apiKey, firestore }) — configure once, install the trap once\n * 2. bucket(name, fn) — name the read paths that matter\n * 3. (the dashboard shows the rest — and names the ones you haven't yet)\n */\nimport { configureMeter, type MeterConfig } from \"./cost-meter\";\nimport { ReportSink, NullSink, type Sink } from \"./sink\";\nimport { MirrorSink, DEFAULT_MIRROR_DIR } from \"./mirror\";\nimport { installFirestoreMeter, type FirestoreClasses } from \"./adapters/firestore\";\n\nexport interface InitOptions {\n /**\n * The project's `cd_sk_` SECRET key. Server-to-server only — never a browser key.\n * OPTIONAL: with no key, Buckets still meters locally and writes the readout to\n * disk (`.crossdeck/buckets.md`) — the free, no-account wedge. Add a key and it\n * also reports up to Crossdeck so the numbers surface on your dashboard.\n */\n apiKey?: string;\n /**\n * Pass the namespace from `firebase-admin/firestore` to auto-install the read\n * trap (recommended — this is what makes every read count with no per-call work).\n * Omit it if you'd rather call `installFirestoreMeter()` yourself, or you only\n * use the manual `recordReads()` recorders.\n */\n firestore?: FirestoreClasses;\n /** Override the report endpoint (defaults to Crossdeck's ingest). */\n endpoint?: string;\n /** How often to flush coalesced counts (ms). Default 60_000. */\n flushIntervalMs?: number;\n /** Bring your own sink (self-host the rollups). Defaults to reporting up to Crossdeck. */\n sink?: Sink;\n /**\n * Where to write the local readout — the file \"read me my buckets\" reads back.\n * Defaults to `.crossdeck`. Pass `false` to turn the local mirror off entirely.\n */\n mirror?: string | false;\n /** Notified when a flush fails, so a dropped window is never silent. Best-effort. */\n onError?: MeterConfig[\"onError\"];\n}\n\n/**\n * Configure Buckets once, at process start. Always meters locally and writes the\n * readout to disk; if you pass `apiKey` (or your own `sink`) it ALSO reports up to\n * Crossdeck. Pass `firestore` to install the universal read trap so every read counts\n * automatically.\n */\nexport function init(options: InitOptions = {}): void {\n // Upstream: your sink, else a Crossdeck reporter if a key was given, else nothing.\n const upstream: Sink | null =\n options.sink ?? (options.apiKey ? new ReportSink({ apiKey: options.apiKey, endpoint: options.endpoint }) : null);\n // Default behaviour tees every flush to a local readout; `mirror:false` opts out.\n const sink: Sink =\n options.mirror === false\n ? upstream ?? new NullSink()\n : new MirrorSink(upstream, typeof options.mirror === \"string\" ? options.mirror : DEFAULT_MIRROR_DIR);\n configureMeter({ sink, flushIntervalMs: options.flushIntervalMs, onError: options.onError });\n if (options.firestore) installFirestoreMeter(options.firestore);\n}\n\n/** Alias — reads well next to `bucket()` at a call site. */\nexport { init as initBuckets };\n\n// The headline verb + the lower-level tag controls it is sugar over.\nexport {\n bucket,\n runWithCostTag,\n enterCostTag,\n refineCostTag,\n currentCostTag,\n type CostTag,\n} from \"./cost-context\";\n\n// Recorders. `record(resource, quantity)` is the generic adapter primitive — count\n// any resource unit (a future adapter records \"clickhouse.query_ms\"); recordReads/\n// Writes/Deletes are the Firestore conveniences over it.\nexport {\n record,\n recordReads,\n recordWrites,\n recordDeletes,\n flush,\n type CostHint,\n type ResourceUnit,\n type OpType,\n type MeterConfig,\n} from \"./cost-meter\";\n\n// The datastore traps + their class shapes. Re-exported from THIS entry so they\n// share the meter's module-level state — a separate bundle would get its own meter\n// instance and silently drop the counts.\nexport { installFirestoreMeter, type FirestoreClasses } from \"./adapters/firestore\";\nexport { installMongoMeter, type MongoClasses, MONGO_READ_UNIT } from \"./mongo\";\n\n// The sink seam — for self-hosting rollups instead of reporting to Crossdeck.\nexport {\n ReportSink,\n NullSink,\n type Sink,\n type BucketsReport,\n type ResourceCounts,\n type OpCounts,\n type ReportSinkConfig,\n} from \"./sink\";\n\n// The local readout — the file \"read me my buckets\" reads back, and its renderer.\nexport { MirrorSink, DEFAULT_MIRROR_DIR } from \"./mirror\";\nexport { renderReadout, READOUT_FOOTER } from \"./readout\";\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/cost-context.ts","../src/cost-meter.ts","../src/sink.ts","../src/readout.ts","../src/mirror.ts","../src/adapters/firestore.ts","../src/mongo/index.ts","../src/postgres/index.ts","../src/index.ts"],"names":["AsyncLocalStorage","join","readFileSync","mkdirSync","writeFileSync","installed","hintFrom","AsyncResource","sink"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * cost-context — the request-scoped tag every counted operation attributes\n * itself to. Set it ONCE at a boundary (or wrap a path with `bucket()`); it\n * rides Node's AsyncLocalStorage down through every async fan-out, so one\n * handler that triggers 15 reads attributes all 15 to the same bucket — with\n * zero per-call-site work.\n *\n * Generic by design: unlike a hardcoded product taxonomy, the only meaningful\n * field a consumer sets is the free-form `label` (the bucket name). `feature`\n * is an optional coarse grouping if you want one; nothing here is\n * Crossdeck-specific.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nexport interface CostTag {\n /** Optional coarse grouping (a caller-defined surface name). */\n feature?: string;\n /** The bucket name — what `bucket()` sets. Drives the report's `byLabel`. */\n label?: string;\n}\n\nconst DEFAULT_TAG: CostTag = {};\nconst store = new AsyncLocalStorage<CostTag>();\n\n/** Run `fn` with `tag` bound for its entire async subtree. */\nexport function runWithCostTag<T>(tag: CostTag, fn: () => T): T {\n return store.run({ ...tag }, fn);\n}\n\n/** Bind a tag for the remainder of the current async context (no closure to wrap). */\nexport function enterCostTag(tag: CostTag): void {\n store.enterWith({ ...tag });\n}\n\n/** Refine the live tag in place (e.g. stamp a feature after the boundary). */\nexport function refineCostTag(patch: Partial<CostTag>): void {\n const cur = store.getStore();\n if (cur) Object.assign(cur, patch);\n}\n\n/** The current tag, or a safe empty default outside any bound context. */\nexport function currentCostTag(): CostTag {\n return store.getStore() ?? DEFAULT_TAG;\n}\n\n/** Hierarchy separator for bucket paths — Firestore-map-key-safe and distinct from\n * the \"col:\" leaf prefix. `bucket(\"a\", () => bucket(\"b\", …))` → \"a>b\". */\nexport const BUCKET_SEP = \">\";\n\n/**\n * `bucket(name, fn)` — the headline verb, the `track()` of cost. Run `fn` with\n * every operation inside it attributed to the bucket `name`; the attribution rides\n * the async subtree automatically. NESTS: a `bucket()` inside another COMPOSES into\n * a path (`\"analytics\" > \"rollup\"` → `\"analytics>rollup\"`), so the dashboard can\n * drill from the coarse bucket down into its parts. The one verb most developers touch:\n *\n * await bucket(\"analytics\", () =>\n * bucket(\"rollup\", () => db.collection(\"events\").where(...).get())); // → \"analytics>rollup>col:events\"\n */\nexport function bucket<T>(name: string, fn: () => T): T {\n const parent = currentCostTag().label;\n const path = parent ? `${parent}${BUCKET_SEP}${name}` : name;\n return runWithCostTag({ ...currentCostTag(), label: path }, fn);\n}\n","/**\n * cost-meter — counts operations against the ambient tag and flushes them to the\n * configured Sink cheaply.\n *\n * LOW-OVERHEAD CONTRACT (the thing that warns you about reads must not run them\n * up): counts accumulate in an in-memory buffer and flush periodically — NEVER one\n * network call per counted operation. A flush coalesces the whole window into one\n * report per UTC day and hands it to the Sink. At steady state that is ~1 small\n * request a minute, regardless of how many ops you served.\n *\n * BEST-EFFORT CONTRACT: metering must never throw into your code. Every recorder\n * swallows its own errors; a failed flush drops that window's counts (surfaced via\n * `onError` if you pass one) rather than disturbing the app.\n */\nimport { currentCostTag } from \"./cost-context\";\nimport type { Sink, BucketsReport, ResourceCounts } from \"./sink\";\n\n/**\n * A resource unit — what an adapter counts. Firestore emits `read`/`write`/\n * `delete`; other adapters emit their own (`clickhouse.query_ms`, `openai.tokens`).\n * It is a free identifier on purpose, BUT each one is kept entirely distinct: the\n * meter only ever sums quantities WITHIN a single resource, never across two.\n */\nexport type ResourceUnit = string;\n/** @deprecated — Firestore-era name; use {@link ResourceUnit}. */\nexport type OpType = ResourceUnit;\n\n/** Optional read-site hint — the collection touched, derived at the trap from the\n * path. Lets an UNtagged read cascade to `col:<collection>` instead of vanishing. */\nexport interface CostHint {\n collection?: string;\n projectId?: string;\n}\n\n// NUL separator — a bucket/collection name can contain almost anything except\n// this, so the key never collides with a name that has a \"|\" or \":\" in it.\nconst SEP = \"\\u001f\"; // ASCII Unit Separator\n\n/** key = date <NUL> resource <NUL> label → count */\nconst labelBuffer = new Map<string, number>();\n/** key = date <NUL> resource <NUL> hour → count */\nconst hourBuffer = new Map<string, number>();\n/** key = date <NUL> resource <NUL> 5-min-slot (\"HHMM\") → count. The fine grain\n * that lets a developer verify against the provider console's \"last hour\" within\n * minutes of installing — not after a day. Still one maintained doc, zero reads. */\nconst minuteBuffer = new Map<string, number>();\n\nlet sink: Sink | null = null;\nlet flushIntervalMs = 60_000;\nlet onError: ((e: unknown) => void) | null = null;\nlet timer: ReturnType<typeof setInterval> | null = null;\nlet flushing = false;\n/** Safety valve — flush early if a burst fills the buffer between intervals. */\nconst MAX_BUFFER_KEYS = 5_000;\n\nexport interface MeterConfig {\n sink: Sink;\n flushIntervalMs?: number;\n onError?: (e: unknown) => void;\n}\n\n/** Point the meter at a sink. Called by `init()`; pass your own sink to self-host. */\nexport function configureMeter(config: MeterConfig): void {\n sink = config.sink;\n if (config.flushIntervalMs && config.flushIntervalMs > 0) flushIntervalMs = config.flushIntervalMs;\n onError = config.onError ?? null;\n}\n\nconst utcDate = (): string => new Date().toISOString().slice(0, 10);\nconst utcHour = (): string => new Date().toISOString().slice(11, 13);\n/** UTC 5-minute slot of the day as \"HHMM\" — the slot START (e.g. 08:47 → \"0845\"). */\nconst utcFiveMin = (): string => {\n const iso = new Date().toISOString();\n const hh = iso.slice(11, 13);\n const mm = Math.floor(Number(iso.slice(14, 16)) / 5) * 5;\n return hh + String(mm).padStart(2, \"0\");\n};\n\nfunction ensureFlushLoop(): void {\n if (timer) return;\n timer = setInterval(() => void flush(), flushIntervalMs);\n // Don't keep the event loop alive just for metering.\n (timer as { unref?: () => void }).unref?.();\n // Flush the last window on shutdown.\n process.once?.(\"SIGTERM\", () => void flush());\n process.once?.(\"beforeExit\", () => void flush());\n}\n\n/**\n * Count `quantity` of a `resource` against the live tag. THE adapter primitive —\n * a Firestore adapter records \"read\"; a ClickHouse adapter records\n * \"clickhouse.query_ms\"; an OpenAI adapter records \"openai.tokens\". Each resource\n * is bucketed entirely on its own; nothing is ever added across resources. Never\n * throws.\n */\nexport function record(resource: ResourceUnit, quantity: number, hint?: CostHint): void {\n try {\n if (!Number.isFinite(quantity) || quantity <= 0) return;\n const t = currentCostTag();\n const date = utcDate();\n // HIERARCHY — the bucket path is the trunk; the collection is the LEAF kept\n // beneath it (so a tagged bucket still drills down to which collections it\n // read). \"analytics\" + events → \"analytics>col:events\"; untagged → \"col:events\";\n // nothing derivable → \"uncategorized\". A unit is never invisible, and a tagged\n // one never loses where it actually went.\n const coll = hint?.collection ? `col:${hint.collection}` : null;\n const label = t.label\n ? coll\n ? `${t.label}>${coll}`\n : t.label\n : coll ?? \"uncategorized\";\n // Key includes the resource, so each resource accumulates in its OWN slot.\n const lk = date + SEP + resource + SEP + label;\n labelBuffer.set(lk, (labelBuffer.get(lk) ?? 0) + quantity);\n const hk = date + SEP + resource + SEP + utcHour();\n hourBuffer.set(hk, (hourBuffer.get(hk) ?? 0) + quantity);\n const mk = date + SEP + resource + SEP + utcFiveMin();\n minuteBuffer.set(mk, (minuteBuffer.get(mk) ?? 0) + quantity);\n ensureFlushLoop();\n if (labelBuffer.size + hourBuffer.size + minuteBuffer.size > MAX_BUFFER_KEYS) void flush();\n } catch {\n /* metering is best-effort — never disturb the caller */\n }\n}\n\n/** @deprecated — use {@link record}. Firestore-era alias. */\nexport const recordFirestore = record;\n\n/** Firestore bills a minimum of one read even for an empty result, so 0 counts as 1. */\nexport function recordReads(n: number, hint?: CostHint): void {\n record(\"read\", Math.max(n, 1), hint);\n}\nexport function recordWrites(n = 1): void {\n record(\"write\", n);\n}\nexport function recordDeletes(n = 1): void {\n record(\"delete\", n);\n}\n\nfunction add(target: Record<string, ResourceCounts>, key: string, resource: ResourceUnit, n: number): void {\n const bag = (target[key] ??= {});\n // Accumulate WITHIN this resource only — never merge resources.\n bag[resource] = (bag[resource] ?? 0) + n;\n}\n\n/**\n * Coalesce the buffer into one report per UTC day and hand each to the Sink.\n * Snapshots + clears up front so concurrent records land in the next window.\n * Never throws; a sink failure drops that window (surfaced via `onError`).\n */\nexport async function flush(): Promise<void> {\n if (flushing) return;\n // Not configured (init not called) — drop, don't grow unbounded.\n if (!sink) {\n labelBuffer.clear();\n hourBuffer.clear();\n minuteBuffer.clear();\n return;\n }\n if (labelBuffer.size === 0 && hourBuffer.size === 0 && minuteBuffer.size === 0) return;\n flushing = true;\n\n const labels = new Map(labelBuffer);\n const hours = new Map(hourBuffer);\n const minutes = new Map(minuteBuffer);\n labelBuffer.clear();\n hourBuffer.clear();\n minuteBuffer.clear();\n\n try {\n const byDate = new Map<string, BucketsReport>();\n const reportFor = (date: string): BucketsReport => {\n let r = byDate.get(date);\n if (!r) {\n r = { date, byLabel: {}, byHour: {}, byMinute: {} };\n byDate.set(date, r);\n }\n return r;\n };\n for (const [k, n] of labels) {\n const [date, op, label] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byLabel, label, op, n);\n }\n for (const [k, n] of hours) {\n const [date, op, hour] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byHour!, hour, op, n);\n }\n for (const [k, n] of minutes) {\n const [date, op, slot] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byMinute!, slot, op, n);\n }\n for (const report of byDate.values()) {\n await sink.flush(report);\n }\n } catch (e) {\n // Drop this window rather than risk a partial/double report on retry.\n onError?.(e);\n } finally {\n flushing = false;\n }\n}\n","/**\n * sink — where the meter sends a coalesced rollup, and the wire shape it sends.\n *\n * Abstracting the sink is what makes Buckets storage-agnostic: the meter never\n * knows where counts go. The DEFAULT sink (`ReportSink`) reports up to Crossdeck's\n * ingest endpoint so the numbers surface on your Crossdeck dashboard. A team that\n * wants to self-host can implement `Sink` against anything (Postgres, a file, your\n * own API) without touching the meter.\n */\n\n/**\n * Counts for ONE bucket, keyed by RESOURCE UNIT — the raw quantity of each unit,\n * nothing more (no money; you verify cost on your provider's bill).\n *\n * Firestore, the first adapter, emits `read` / `write` / `delete`. Other adapters\n * emit their own units (`clickhouse.query_ms`, `openai.tokens`, …). The rule that\n * keeps this honest: **each resource is its own line. Counts are only ever summed\n * WITHIN a resource, NEVER across one.** A read is not a query-millisecond; the two\n * never land in the same number. (There is deliberately no \"total units\" field.)\n */\nexport interface ResourceCounts {\n read?: number;\n write?: number;\n delete?: number;\n /** Any other resource unit an adapter emits — kept distinct, never merged. */\n [resource: string]: number | undefined;\n}\n\n/** @deprecated name — kept for back-compat; use {@link ResourceCounts}. */\nexport type OpCounts = ResourceCounts;\n\n/**\n * One coalesced report — the wire contract (see docs/ROLLUP_SCHEMA.md). The meter\n * produces one of these per UTC day in a flush window (usually exactly one).\n */\nexport interface BucketsReport {\n /** UTC day \"YYYY-MM-DD\". */\n date: string;\n /** bucket name → counts. The heart of the report. */\n byLabel: Record<string, ResourceCounts>;\n /** UTC hour \"HH\" → counts, for the hourly \"did my fix land this hour?\" view. */\n byHour?: Record<string, ResourceCounts>;\n /** UTC 5-minute slot \"HHMM\" (slot start) → counts. The fine grain for fast\n * verification against a provider console's \"last hour\" view. */\n byMinute?: Record<string, ResourceCounts>;\n}\n\n/**\n * A destination for coalesced rollups. `flush` MAY throw on failure — the meter\n * catches it, drops that one window, and never lets it reach your app.\n */\nexport interface Sink {\n flush(report: BucketsReport): Promise<void>;\n}\n\nexport interface ReportSinkConfig {\n /** The project's `cd_sk_` secret key. Server-to-server only. */\n apiKey: string;\n /** Defaults to https://api.cross-deck.com/v1/buckets/report */\n endpoint?: string;\n /** Request timeout (ms); a slow Crossdeck must never stall your flush. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_ENDPOINT = \"https://api.cross-deck.com/v1/buckets/report\";\n\n/**\n * The default sink: POST one coalesced rollup to Crossdeck's ingest endpoint.\n * The ingest folds it into the day's maintained doc with `increment`, so many\n * reports a minute coalesce safely. This path does ZERO database reads — it sends\n * a summary, it does not read. Throws on a non-202 so the meter can log/drop the\n * window; the meter guarantees it never reaches your app.\n */\nexport class ReportSink implements Sink {\n private readonly endpoint: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n\n constructor(config: ReportSinkConfig) {\n this.endpoint = config.endpoint ?? DEFAULT_ENDPOINT;\n this.apiKey = config.apiKey;\n this.timeoutMs = config.timeoutMs ?? 5000;\n }\n\n async flush(report: BucketsReport): Promise<void> {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n signal: AbortSignal.timeout(this.timeoutMs),\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(report),\n });\n if (res.status !== 202) {\n throw new Error(`Buckets report rejected: HTTP ${res.status}`);\n }\n }\n}\n\n/**\n * A sink that does nothing. Used when the local mirror is turned off AND no upstream\n * was configured — the meter still needs a sink to flush into, but there's nowhere to\n * send. Counts are dropped by the developer's explicit choice.\n */\nexport class NullSink implements Sink {\n async flush(): Promise<void> {\n /* intentionally empty */\n }\n}\n","/**\n * readout — renders the local file a developer (or their AI session) reads back with\n * \"read me my buckets\". PURE string building: no I/O, no database reads. The node\n * mirror (./mirror) writes this to `.crossdeck/buckets.md` on each flush, so the\n * readout works offline, for free, with no account.\n */\nimport type { BucketsReport, ResourceCounts } from \"./sink\";\n\n/**\n * The one line that closes every readout. Plain and factual: what the OSS shows you\n * here, and what signing up adds — for free. No invented numbers, no urgency, no pitch.\n */\nexport const READOUT_FOOTER =\n \"Buckets OSS shows the reads on this surface. Sign up to Crossdeck (free) to see \" +\n \"every surface in one view, drill any bucket down to the exact query, track a fix \" +\n \"before and after, and get paged when reads spike — cross-deck.com\";\n\nfunction fmt(n: number): string {\n if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + \"M\";\n if (n >= 10_000) return Math.round(n / 1000) + \"K\";\n if (n >= 1000) return (n / 1000).toFixed(1) + \"K\";\n return String(Math.round(n));\n}\n\n/** A bucket is untagged when its ROOT segment is a bare collection / catch-all. */\nfunction isUntagged(label: string): boolean {\n const root = label.split(\">\")[0];\n return root.startsWith(\"col:\") || root === \"uncategorized\" || root === \"unknown\";\n}\n\n/** Pretty path: strip the \"col:\" leaf prefix, join the hierarchy with \" › \". */\nfunction displayLabel(label: string): string {\n return label\n .split(\">\")\n .map((s) => (s.startsWith(\"col:\") ? s.slice(4) : s))\n .join(\" › \");\n}\n\n/** Render the day's coalesced report as a human/AI-readable markdown readout. */\nexport function renderReadout(report: BucketsReport): string {\n const entries = Object.entries(report.byLabel ?? {})\n .map(([label, counts]) => ({ label, reads: (counts as ResourceCounts).read ?? 0 }))\n .filter((e) => e.reads > 0)\n .sort((a, b) => b.reads - a.reads);\n\n const total = entries.reduce((s, e) => s + e.reads, 0);\n const out: string[] = [];\n out.push(`# Buckets — reads on this surface`);\n out.push(``);\n out.push(`**${fmt(total)} reads** · ${report.date} (UTC)`);\n out.push(``);\n\n if (entries.length === 0) {\n out.push(`No reads metered yet — install the collector and let your app serve some traffic.`);\n } else {\n out.push(`| bucket | named | reads |`);\n out.push(`| --- | :---: | ---: |`);\n for (const e of entries) {\n out.push(`| ${displayLabel(e.label)} | ${isUntagged(e.label) ? \"—\" : \"✓\"} | ${fmt(e.reads)} |`);\n }\n }\n\n out.push(``);\n out.push(`---`);\n out.push(READOUT_FOOTER);\n out.push(``);\n return out.join(\"\\n\");\n}\n","/**\n * mirror — tees every coalesced report to a local file so \"read me my buckets\" works\n * offline, free, with no account. Writes a human/AI-readable readout\n * (`.crossdeck/buckets.md`) plus the raw report (`.crossdeck/buckets.json`).\n *\n * NODE ONLY — never imported by the browser build (it touches the filesystem).\n *\n * Two contracts it keeps:\n * - NO MONSTER: it only ever WRITES local files (~one small write a minute). It never\n * reads your database; the report is already in hand.\n * - BEST-EFFORT: a write error is swallowed — the local mirror must never disturb a\n * flush or reach your app.\n *\n * The meter hands each flush a DELTA (the window's counts, then clears). To show the\n * day's running total, the mirror accumulates deltas in memory, seeded once from any\n * existing file so a process restart doesn't shrink the readout.\n */\nimport { mkdirSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Sink, BucketsReport, ResourceCounts } from \"./sink\";\nimport { renderReadout } from \"./readout\";\n\nexport const DEFAULT_MIRROR_DIR = \".crossdeck\";\n\nfunction mergeInto(target: Record<string, ResourceCounts>, src?: Record<string, ResourceCounts>): void {\n if (!src) return;\n for (const [key, counts] of Object.entries(src)) {\n const bag = (target[key] ??= {});\n for (const [res, n] of Object.entries(counts)) {\n if (typeof n === \"number\") bag[res] = (bag[res] ?? 0) + n;\n }\n }\n}\n\n/**\n * Wraps an optional upstream sink. On each flush it writes the running day-total\n * locally, THEN (if an upstream sink was given — i.e. a key) reports onward.\n * With no upstream it is a pure local meter: the wedge, working with no account.\n */\nexport class MirrorSink implements Sink {\n private acc: BucketsReport | null = null;\n private announced = false;\n private seeded = false;\n\n constructor(\n private readonly upstream: Sink | null,\n private readonly dir: string = DEFAULT_MIRROR_DIR,\n ) {}\n\n private jsonPath(): string {\n return join(this.dir, \"buckets.json\");\n }\n\n /** Seed the running total once from an existing same-day file (survives restarts). */\n private seed(date: string): void {\n if (this.seeded) return;\n this.seeded = true;\n try {\n const prior = JSON.parse(readFileSync(this.jsonPath(), \"utf8\")) as BucketsReport;\n if (prior?.date === date && prior.byLabel) this.acc = prior;\n } catch {\n /* no prior file (or unreadable) — start fresh */\n }\n }\n\n async flush(report: BucketsReport): Promise<void> {\n // Local first — the part that always works, key or no key.\n try {\n this.seed(report.date);\n if (!this.acc || this.acc.date !== report.date) {\n this.acc = { date: report.date, byLabel: {}, byHour: {}, byMinute: {} };\n }\n mergeInto(this.acc.byLabel, report.byLabel);\n mergeInto((this.acc.byHour ??= {}), report.byHour);\n mergeInto((this.acc.byMinute ??= {}), report.byMinute);\n\n mkdirSync(this.dir, { recursive: true });\n writeFileSync(join(this.dir, \"buckets.md\"), renderReadout(this.acc));\n writeFileSync(this.jsonPath(), JSON.stringify(this.acc, null, 2));\n\n if (!this.announced) {\n this.announced = true;\n // One quiet line, once, so a developer knows where to read it back.\n // eslint-disable-next-line no-console\n console.log(\n `Buckets: readout at ${join(this.dir, \"buckets.md\")} — open it, or ask your AI session to \"read me my buckets\".`,\n );\n }\n } catch {\n /* local mirror is best-effort */\n }\n\n if (this.upstream) await this.upstream.flush(report);\n }\n}\n","/**\n * adapters/firestore — the universal Firestore read meter (the trap).\n *\n * THE LESSON (learned on a real product): per-call-site `recordReads()`\n * instrumentation MISSES paths. You meter the read sites you're looking at and\n * leave the cron / trigger / ingest path uncounted — often the majority of reads,\n * invisible. Humans tag what they see and miss the path that matters.\n *\n * THE FIX: patch the admin SDK's read methods ONCE. From install onward, EVERY\n * read — anywhere, on any code path — is counted under the ambient tag, with zero\n * per-call-site work and no blind spots.\n *\n * SAFETY CONTRACT — this sits on your production read path, so it is defensive by\n * construction. Each wrapper:\n * 1. calls the REAL method first and captures the result,\n * 2. counts in a try/catch that can never throw into the caller,\n * 3. ALWAYS returns the real result, untouched.\n * It cannot break a read, change a result, or add latency beyond one in-memory\n * counter increment. A wrong count is a measurement error, never a correctness or\n * availability one. Idempotent — calling it twice patches once.\n *\n * COUNTING MODEL — a query returning N docs = N reads (an empty result still bills\n * 1, which the meter enforces). A document get = 1. getAll(...) = the ref count.\n * CollectionReference.get IS Query.get (shared prototype method), so patching Query\n * covers collections with no double-count.\n */\nimport { recordReads, type CostHint } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/**\n * The firebase-admin Firestore classes to patch. Pass the module namespace from\n * `firebase-admin/firestore` — only the prototypes present are patched.\n */\nexport interface FirestoreClasses {\n Query?: { prototype: { get?: AnyFn; onSnapshot?: AnyFn } };\n DocumentReference?: { prototype: { get?: AnyFn; onSnapshot?: AnyFn } };\n Transaction?: { prototype: { get?: AnyFn; getAll?: AnyFn } };\n Firestore?: { prototype: { getAll?: AnyFn } };\n /** count() / sum() / average() — aggregation queries bill reads too. */\n AggregateQuery?: { prototype: { get?: AnyFn } };\n}\n\n/** `projects/{id}/…` → the project id, else undefined. Pure string op. */\nfunction projectFromPath(path: string): string | undefined {\n const parts = path.split(\"/\");\n const i = parts.indexOf(\"projects\");\n return i >= 0 && parts[i + 1] ? parts[i + 1] : undefined;\n}\n\n/**\n * Derive { collection, projectId } from the read target's path so an UNtagged read\n * cascades to `col:<collection>` instead of \"uncategorized\". PURE CPU; never reads,\n * never throws. Falls back to firebase-admin's internal `_queryOptions` for filtered\n * queries (which don't expose `.path`).\n */\nfunction hintFrom(target: any): CostHint | undefined {\n try {\n const p = typeof target?.path === \"string\" ? target.path : \"\";\n if (p) {\n const parts = p.split(\"/\").filter(Boolean);\n const collection = parts.length % 2 === 0 ? parts[parts.length - 2] : parts[parts.length - 1];\n return { collection, projectId: projectFromPath(p) };\n }\n const qo = target?._queryOptions;\n if (qo) {\n const collection = typeof qo.collectionId === \"string\" ? qo.collectionId : undefined;\n const parent =\n typeof qo.parentPath?.relativeName === \"string\"\n ? qo.parentPath.relativeName\n : typeof qo.parentPath?.toString === \"function\"\n ? String(qo.parentPath.toString())\n : \"\";\n return { collection, projectId: parent ? projectFromPath(parent) : undefined };\n }\n } catch {\n /* the meter must never throw */\n }\n return undefined;\n}\n\nfunction meterSnap(snap: unknown, hint?: CostHint): void {\n try {\n const size = (snap as { size?: number } | null)?.size;\n recordReads(typeof size === \"number\" ? size : 1, hint);\n } catch {\n /* best-effort */\n }\n}\nfunction meterCount(n: number, hint?: CostHint): void {\n try {\n recordReads(n, hint);\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * Install the universal read meter on the firebase-admin Firestore classes. Call\n * ONCE at process start, before any reads. Pass the namespace from\n * `firebase-admin/firestore` so the exact prototypes the SDK uses are patched:\n *\n * import * as Firestore from \"firebase-admin/firestore\";\n * installFirestoreMeter(Firestore);\n */\nexport function installFirestoreMeter(classes: FirestoreClasses): void {\n if (installed) return;\n installed = true;\n const { Query, DocumentReference, Transaction, Firestore, AggregateQuery } = classes;\n\n // Query.get — covers Query AND CollectionReference (shared prototype method).\n const qGet = Query?.prototype?.get;\n if (qGet) {\n Query!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await qGet.apply(this, args);\n meterSnap(snap, hintFrom(this));\n return snap;\n };\n }\n\n // DocumentReference.get — a single doc = 1 read.\n const dGet = DocumentReference?.prototype?.get;\n if (dGet) {\n DocumentReference!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await dGet.apply(this, args);\n meterCount(1, hintFrom(this));\n return snap;\n };\n }\n\n // Transaction.get — query or doc; size when present, else 1.\n const tGet = Transaction?.prototype?.get;\n if (tGet) {\n Transaction!.prototype.get = async function (this: unknown, ...args: any[]) {\n const res = await tGet.apply(this, args);\n meterSnap(res, hintFrom(args[0]));\n return res;\n };\n }\n\n // Transaction.getAll(...refs) — one read per ref.\n const tGetAll = Transaction?.prototype?.getAll;\n if (tGetAll) {\n Transaction!.prototype.getAll = async function (this: unknown, ...args: any[]) {\n const res = await tGetAll.apply(this, args);\n meterCount(Array.isArray(res) ? res.length : args.length || 1, hintFrom(args[0]));\n return res;\n };\n }\n\n // Firestore.getAll(...refs) — batched doc reads.\n const fGetAll = Firestore?.prototype?.getAll;\n if (fGetAll) {\n Firestore!.prototype.getAll = async function (this: unknown, ...args: any[]) {\n const res = await fGetAll.apply(this, args);\n meterCount(Array.isArray(res) ? res.length : args.length || 1, hintFrom(args[0]));\n return res;\n };\n }\n\n // AggregateQuery.get — count() / sum() / average(). Firestore bills one read per\n // up to 1000 index entries scanned, and the client never sees that entry count —\n // so this is an HONEST ESTIMATE: for count() we derive it from the result\n // (ceil(count / 1000)), else the billed minimum of 1. Observe-only: it reads the\n // result you already got, adds zero reads. (Closes the aggregation blind spot.)\n const aGet = AggregateQuery?.prototype?.get;\n if (aGet) {\n AggregateQuery!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await aGet.apply(this, args);\n try {\n const data = (snap as { data?: () => { count?: number } } | null)?.data?.();\n const count = typeof data?.count === \"number\" ? data.count : 0;\n meterCount(Math.max(1, Math.ceil(count / 1000)), hintFrom(this));\n } catch {\n /* best-effort */\n }\n return snap;\n };\n }\n\n // Query.onSnapshot / DocumentReference.onSnapshot — server-side realtime\n // listeners. We OBSERVE: wrap onNext and count the docs each fire delivers (a\n // query's changed docs — first fire = all matching; a doc = 1). We attach no\n // listener and issue no read; the meter just sees what the listener is already\n // billed. (Closes the server-listener blind spot.)\n const patchOnSnapshot = (proto: { onSnapshot?: AnyFn } | undefined, perDoc: boolean): void => {\n const real = proto?.onSnapshot;\n if (!real) return;\n proto!.onSnapshot = function (this: unknown, ...args: any[]) {\n const hint = hintFrom(this);\n const i = args.findIndex((a) => typeof a === \"function\");\n if (i >= 0) {\n const onNext = args[i];\n args[i] = function (snap: any) {\n try {\n const n = perDoc\n ? 1\n : typeof snap?.docChanges === \"function\"\n ? snap.docChanges().length\n : typeof snap?.size === \"number\"\n ? snap.size\n : 1;\n if (n > 0) meterCount(n, hint);\n } catch {\n /* best-effort */\n }\n return onNext(snap);\n };\n }\n return real.apply(this, args);\n };\n };\n patchOnSnapshot(Query?.prototype, false);\n patchOnSnapshot(DocumentReference?.prototype, true);\n}\n\n/** Test-only: reset the install guard so a suite can re-patch fresh prototypes. */\nexport function __resetFirestoreMeterForTests(): void {\n installed = false;\n}\n","/**\n * adapters/mongo — the MongoDB read meter (the trap), mirroring the Firestore one.\n *\n * THE RAW UNIT: like Firestore (a query returning N docs = N reads), MongoDB's raw\n * unit is DOCUMENTS READ — the documents each read operation returns, attributed to\n * the feature (bucket) that ran it. This is a real, countable number, NOT a dollar\n * bill. (MongoDB bills by cluster/compute, not per read — so this is the read LOAD by\n * feature: which queries pull the most documents, the thing you index/narrow/cache to\n * run a smaller cluster.) Raw counts only, no money — the two laws hold.\n *\n * THE FIX it brings: per-call-site instrumentation misses paths. Patch the driver's\n * result-returning read methods ONCE, and from install on every read — anywhere, any\n * code path — is counted under the ambient tag with no blind spots.\n *\n * MECHANISM: the wrappers run in the CALLER's own async context (a `find().toArray()`\n * inside `bucket(\"feed\")` resolves in that context), so attribution survives with zero\n * per-call-site work. Observe-only: it counts the result already in hand — it adds NO\n * query (no `explain()`, no profiler scan), so it can never become a read monster.\n *\n * SAFETY CONTRACT (it sits on your production read path): each wrapper calls the REAL\n * method first, counts in a try/catch that can never throw into the caller, and ALWAYS\n * returns the real result untouched. A wrong count is a measurement error, never a\n * correctness or availability one. Idempotent — calling it twice patches once.\n *\n * Pass the classes from your `mongodb` import (an OPTIONAL peer dep — installing this\n * never forces the driver on a Firestore user):\n *\n * import { FindCursor, AggregationCursor, Collection } from \"mongodb\";\n * import { installMongoMeter } from \"@cross-deck/buckets\";\n * installMongoMeter({ FindCursor, AggregationCursor, Collection });\n */\nimport { record, type CostHint } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/** MongoDB's raw read unit — documents returned by a read operation. A count. */\nexport const MONGO_READ_UNIT = \"mongo.docs_read\";\n\n/**\n * The `mongodb` driver classes to patch. Pass them from your `mongodb` import — only\n * the prototypes present are patched, so a driver-version mismatch degrades to\n * \"counts fewer paths\", never a crash.\n */\nexport interface MongoClasses {\n /** find() cursor — `.toArray()` resolves the matched documents. */\n FindCursor?: { prototype: { toArray?: AnyFn } };\n /** aggregate() cursor — `.toArray()` resolves the pipeline output documents. */\n AggregationCursor?: { prototype: { toArray?: AnyFn } };\n /** Collection — `.findOne()` resolves a single document (or null). */\n Collection?: { prototype: { findOne?: AnyFn } };\n}\n\n/** Best-effort `col:<collection>` cascade for an UNtagged read. PURE; never throws. */\nfunction hintFrom(target: any): CostHint | undefined {\n try {\n // FindCursor / AggregationCursor expose `.namespace` (MongoDBNamespace); a\n // Collection exposes `.collectionName`. Either gives us the collection.\n const ns = target?.namespace;\n if (ns && typeof ns.collection === \"string\" && ns.collection) {\n return { collection: ns.collection };\n }\n if (typeof target?.collectionName === \"string\" && target.collectionName) {\n return { collection: target.collectionName };\n }\n } catch {\n /* the meter must never throw */\n }\n return undefined;\n}\n\nfunction meter(n: number, hint?: CostHint): void {\n try {\n if (n > 0) record(MONGO_READ_UNIT, n, hint);\n } catch {\n /* best-effort — never disturb the caller */\n }\n}\n\n/**\n * Install the MongoDB read meter on the driver's read-result methods. Call ONCE at\n * process start, before any reads. Pass the classes from your `mongodb` import.\n */\nexport function installMongoMeter(classes: MongoClasses): void {\n if (installed) return;\n installed = true;\n const { FindCursor, AggregationCursor, Collection } = classes;\n\n // find().toArray() / aggregate().toArray() — the documents the query returned.\n const patchToArray = (proto: { toArray?: AnyFn } | undefined): void => {\n const real = proto?.toArray;\n if (!real) return;\n proto!.toArray = async function (this: unknown, ...args: any[]) {\n const out = await real.apply(this, args);\n meter(Array.isArray(out) ? out.length : 0, hintFrom(this));\n return out;\n };\n };\n patchToArray(FindCursor?.prototype);\n patchToArray(AggregationCursor?.prototype);\n\n // findOne() — one document (or null). A found doc is 1 read; null returned nothing.\n const realFindOne = Collection?.prototype?.findOne;\n if (realFindOne) {\n Collection!.prototype.findOne = async function (this: unknown, ...args: any[]) {\n const out = await realFindOne.apply(this, args);\n meter(out == null ? 0 : 1, hintFrom(this));\n return out;\n };\n }\n}\n\n/** Test-only: reset the install guard so a suite can re-patch fresh prototypes. */\nexport function __resetMongoMeterForTests(): void {\n installed = false;\n}\n","/**\n * adapters/postgres — the Postgres read meter, mirroring the Firestore and Mongo ones.\n * Covers node-postgres (`pg`), and therefore Supabase, Neon, Vercel Postgres, RDS, and\n * plain Postgres — they all speak the same wire protocol through the same driver.\n *\n * THE RAW UNIT: Postgres's raw read unit is ROWS READ — the rows a SELECT returns,\n * attributed to the feature (bucket) that ran it. This is a real, countable number,\n * NOT a dollar bill. (Supabase/Neon/RDS bill by COMPUTE — instance size × hours, not\n * per row — so this is the read LOAD by feature: which queries pull the most rows, the\n * thing you index/narrow/paginate/cache to run a smaller instance.) Raw counts only,\n * no money — the two laws hold.\n *\n * Sourced from the official docs (the playbook's load-bearing stage), not assumed:\n * - Supabase billing: charged purely on compute-hours, explicitly NOT per row/query\n * (https://supabase.com/docs/guides/platform/manage-your-usage/compute) — so the\n * honest unit is the data work, rows read, never a bill.\n * - node-postgres Result: `result.rows` and `result.rowCount` are ALREADY present on\n * the resolved result — reading them costs NO extra round-trip\n * (https://node-postgres.com/apis/result) — so the meter can never be a read monster.\n *\n * THE FIX it brings: per-call-site instrumentation misses paths. Patch the driver's\n * query method ONCE, and from install on every read — anywhere, any code path, through\n * a Pool or a Client — is counted under the ambient tag with no blind spots.\n *\n * MECHANISM: we patch `Client.prototype.query` only. `Pool.query` delegates to a\n * client's `query`, so one patch catches both pool and client usage with NO double\n * count. Attribution survives because the meter runs in the CALLER's async context\n * (a `pool.query()` inside `bucket(\"feed\")` is metered in that context) — for the\n * promise form via a synchronously-attached `.then`, and for the legacy callback form\n * via `AsyncResource.bind`. Observe-only: it counts the rows already in the result it\n * was handed — it adds NO query (no `EXPLAIN`, no `pg_stat_statements` scan), so it can\n * never become a read monster.\n *\n * SAFETY CONTRACT (it sits on your production read path): the wrapper calls the REAL\n * `query` first, counts in a try/catch that can never throw into the caller, and ALWAYS\n * returns the real result untouched. A wrong count is a measurement error, never a\n * correctness or availability one. Idempotent — calling it twice patches once.\n *\n * Pass the `Client` class from your `pg` import (an OPTIONAL peer dep — installing this\n * never forces the driver on a Firestore user):\n *\n * import { Client } from \"pg\";\n * import { installPgMeter, bucket } from \"@cross-deck/buckets\";\n * installPgMeter({ Client }); // once, at startup\n * await bucket(\"billing-page\", () => pool.query(\"SELECT ... \"));\n */\nimport { AsyncResource } from \"node:async_hooks\";\nimport { record } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/** Postgres's raw read unit — rows returned by a SELECT. A count. */\nexport const PG_READ_UNIT = \"postgres.rows_read\";\n\n/**\n * The `pg` driver class to patch. Pass `Client` from your `pg` import; `Pool.query`\n * delegates to it, so this single patch covers pool usage too. Only the prototype\n * present is patched, so a driver-version mismatch degrades to \"counts nothing\",\n * never a crash.\n */\nexport interface PgClasses {\n /** node-postgres Client — `.query()` runs a statement and resolves a Result. */\n Client?: { prototype: { query?: AnyFn } };\n}\n\n/** A pg Result, minimally typed for what we read (already in hand — no round-trip). */\ninterface PgResultLike {\n /** The SQL command tag: \"SELECT\", \"INSERT\", … — tells a read from a write. */\n command?: string;\n /** The rows the statement returned (empty for a write with no RETURNING). */\n rows?: unknown[];\n}\n\n/**\n * Count the ROWS READ from a resolved Result — and ONLY for reads. node-postgres sets\n * `command` to the SQL command tag; a \"SELECT\"'s returned rows are the read load. A\n * write's RETURNING rows (command \"INSERT\"/\"UPDATE\"/\"DELETE\") are NOT reads and are\n * correctly excluded. PURE; never throws.\n */\nfunction meterResult(res: PgResultLike | undefined | null): void {\n try {\n if (res && res.command === \"SELECT\" && Array.isArray(res.rows) && res.rows.length > 0) {\n record(PG_READ_UNIT, res.rows.length);\n }\n } catch {\n /* the meter must never throw into the caller */\n }\n}\n\n/**\n * Install the Postgres read meter on `Client.prototype.query`. Call ONCE at process\n * start, before any reads. Pass the `Client` class from your `pg` import.\n */\nexport function installPgMeter(classes: PgClasses): void {\n if (installed) return;\n installed = true;\n\n const proto = classes.Client?.prototype;\n const real = proto?.query;\n if (!proto || !real) return;\n\n proto.query = function (this: unknown, ...args: any[]): any {\n // The last arg MAY be a Node-style callback (the legacy form). The promise form\n // passes no callback and the driver returns a Promise<Result>.\n const last = args.length > 0 ? args[args.length - 1] : undefined;\n\n if (typeof last === \"function\") {\n // Callback form. pg invokes the callback later, from a socket event whose async\n // context is NOT the caller's — so bind the meter to the caller's context now,\n // so attribution lands under the right bucket.\n const meterHere = AsyncResource.bind((res: PgResultLike) => meterResult(res));\n args[args.length - 1] = function (this: unknown, err: unknown, res: PgResultLike) {\n if (!err) {\n try {\n meterHere(res);\n } catch {\n /* never disturb the caller */\n }\n }\n return last.apply(this, arguments as unknown as any[]);\n };\n return real.apply(this, args);\n }\n\n const ret = real.apply(this, args);\n // Promise form: attach `.then` synchronously, here in the caller's async context,\n // so the meter runs with that context and attribution survives.\n if (ret && typeof ret.then === \"function\") {\n return ret.then((res: PgResultLike) => {\n meterResult(res);\n return res;\n });\n }\n // A submittable (a Query/Cursor object) or anything unexpected — leave untouched.\n return ret;\n };\n}\n\n/** Test-only: reset the install guard so a suite can re-patch a fresh prototype. */\nexport function __resetPgMeterForTests(): void {\n installed = false;\n}\n","/**\n * @cross-deck/buckets — know exactly what every database read costs you, and who\n * caused it. A tiny, never-throws collector for Firestore.\n *\n * The whole footprint a consumer sees:\n * 1. init({ apiKey, firestore }) — configure once, install the trap once\n * 2. bucket(name, fn) — name the read paths that matter\n * 3. (the dashboard shows the rest — and names the ones you haven't yet)\n */\nimport { configureMeter, type MeterConfig } from \"./cost-meter\";\nimport { ReportSink, NullSink, type Sink } from \"./sink\";\nimport { MirrorSink, DEFAULT_MIRROR_DIR } from \"./mirror\";\nimport { installFirestoreMeter, type FirestoreClasses } from \"./adapters/firestore\";\n\nexport interface InitOptions {\n /**\n * The project's `cd_sk_` SECRET key. Server-to-server only — never a browser key.\n * OPTIONAL: with no key, Buckets still meters locally and writes the readout to\n * disk (`.crossdeck/buckets.md`) — the free, no-account wedge. Add a key and it\n * also reports up to Crossdeck so the numbers surface on your dashboard.\n */\n apiKey?: string;\n /**\n * Pass the namespace from `firebase-admin/firestore` to auto-install the read\n * trap (recommended — this is what makes every read count with no per-call work).\n * Omit it if you'd rather call `installFirestoreMeter()` yourself, or you only\n * use the manual `recordReads()` recorders.\n */\n firestore?: FirestoreClasses;\n /** Override the report endpoint (defaults to Crossdeck's ingest). */\n endpoint?: string;\n /** How often to flush coalesced counts (ms). Default 60_000. */\n flushIntervalMs?: number;\n /** Bring your own sink (self-host the rollups). Defaults to reporting up to Crossdeck. */\n sink?: Sink;\n /**\n * Where to write the local readout — the file \"read me my buckets\" reads back.\n * Defaults to `.crossdeck`. Pass `false` to turn the local mirror off entirely.\n */\n mirror?: string | false;\n /** Notified when a flush fails, so a dropped window is never silent. Best-effort. */\n onError?: MeterConfig[\"onError\"];\n}\n\n/**\n * Configure Buckets once, at process start. Always meters locally and writes the\n * readout to disk; if you pass `apiKey` (or your own `sink`) it ALSO reports up to\n * Crossdeck. Pass `firestore` to install the universal read trap so every read counts\n * automatically.\n */\nexport function init(options: InitOptions = {}): void {\n // Upstream: your sink, else a Crossdeck reporter if a key was given, else nothing.\n const upstream: Sink | null =\n options.sink ?? (options.apiKey ? new ReportSink({ apiKey: options.apiKey, endpoint: options.endpoint }) : null);\n // Default behaviour tees every flush to a local readout; `mirror:false` opts out.\n const sink: Sink =\n options.mirror === false\n ? upstream ?? new NullSink()\n : new MirrorSink(upstream, typeof options.mirror === \"string\" ? options.mirror : DEFAULT_MIRROR_DIR);\n configureMeter({ sink, flushIntervalMs: options.flushIntervalMs, onError: options.onError });\n if (options.firestore) installFirestoreMeter(options.firestore);\n}\n\n/** Alias — reads well next to `bucket()` at a call site. */\nexport { init as initBuckets };\n\n// The headline verb + the lower-level tag controls it is sugar over.\nexport {\n bucket,\n runWithCostTag,\n enterCostTag,\n refineCostTag,\n currentCostTag,\n type CostTag,\n} from \"./cost-context\";\n\n// Recorders. `record(resource, quantity)` is the generic adapter primitive — count\n// any resource unit (a future adapter records \"clickhouse.query_ms\"); recordReads/\n// Writes/Deletes are the Firestore conveniences over it.\nexport {\n record,\n recordReads,\n recordWrites,\n recordDeletes,\n flush,\n type CostHint,\n type ResourceUnit,\n type OpType,\n type MeterConfig,\n} from \"./cost-meter\";\n\n// The datastore traps + their class shapes. Re-exported from THIS entry so they\n// share the meter's module-level state — a separate bundle would get its own meter\n// instance and silently drop the counts.\nexport { installFirestoreMeter, type FirestoreClasses } from \"./adapters/firestore\";\nexport { installMongoMeter, type MongoClasses, MONGO_READ_UNIT } from \"./mongo\";\nexport { installPgMeter, type PgClasses, PG_READ_UNIT } from \"./postgres\";\n\n// The sink seam — for self-hosting rollups instead of reporting to Crossdeck.\nexport {\n ReportSink,\n NullSink,\n type Sink,\n type BucketsReport,\n type ResourceCounts,\n type OpCounts,\n type ReportSinkConfig,\n} from \"./sink\";\n\n// The local readout — the file \"read me my buckets\" reads back, and its renderer.\nexport { MirrorSink, DEFAULT_MIRROR_DIR } from \"./mirror\";\nexport { renderReadout, READOUT_FOOTER } from \"./readout\";\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'async_hooks';
|
|
1
|
+
import { AsyncLocalStorage, AsyncResource } from 'async_hooks';
|
|
2
2
|
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
|
|
@@ -428,6 +428,47 @@ function installMongoMeter(classes) {
|
|
|
428
428
|
};
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
|
+
var installed3 = false;
|
|
432
|
+
var PG_READ_UNIT = "postgres.rows_read";
|
|
433
|
+
function meterResult(res) {
|
|
434
|
+
try {
|
|
435
|
+
if (res && res.command === "SELECT" && Array.isArray(res.rows) && res.rows.length > 0) {
|
|
436
|
+
record(PG_READ_UNIT, res.rows.length);
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function installPgMeter(classes) {
|
|
442
|
+
if (installed3) return;
|
|
443
|
+
installed3 = true;
|
|
444
|
+
const proto = classes.Client?.prototype;
|
|
445
|
+
const real = proto?.query;
|
|
446
|
+
if (!proto || !real) return;
|
|
447
|
+
proto.query = function(...args) {
|
|
448
|
+
const last = args.length > 0 ? args[args.length - 1] : void 0;
|
|
449
|
+
if (typeof last === "function") {
|
|
450
|
+
const meterHere = AsyncResource.bind((res) => meterResult(res));
|
|
451
|
+
args[args.length - 1] = function(err, res) {
|
|
452
|
+
if (!err) {
|
|
453
|
+
try {
|
|
454
|
+
meterHere(res);
|
|
455
|
+
} catch {
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return last.apply(this, arguments);
|
|
459
|
+
};
|
|
460
|
+
return real.apply(this, args);
|
|
461
|
+
}
|
|
462
|
+
const ret = real.apply(this, args);
|
|
463
|
+
if (ret && typeof ret.then === "function") {
|
|
464
|
+
return ret.then((res) => {
|
|
465
|
+
meterResult(res);
|
|
466
|
+
return res;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return ret;
|
|
470
|
+
};
|
|
471
|
+
}
|
|
431
472
|
|
|
432
473
|
// src/index.ts
|
|
433
474
|
function init(options = {}) {
|
|
@@ -437,6 +478,6 @@ function init(options = {}) {
|
|
|
437
478
|
if (options.firestore) installFirestoreMeter(options.firestore);
|
|
438
479
|
}
|
|
439
480
|
|
|
440
|
-
export { DEFAULT_MIRROR_DIR, MONGO_READ_UNIT, MirrorSink, NullSink, READOUT_FOOTER, ReportSink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, installMongoMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, renderReadout, runWithCostTag };
|
|
481
|
+
export { DEFAULT_MIRROR_DIR, MONGO_READ_UNIT, MirrorSink, NullSink, PG_READ_UNIT, READOUT_FOOTER, ReportSink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, installMongoMeter, installPgMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, renderReadout, runWithCostTag };
|
|
441
482
|
//# sourceMappingURL=index.mjs.map
|
|
442
483
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cost-context.ts","../src/cost-meter.ts","../src/sink.ts","../src/readout.ts","../src/mirror.ts","../src/adapters/firestore.ts","../src/mongo/index.ts","../src/index.ts"],"names":["installed","hintFrom","sink"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * cost-context — the request-scoped tag every counted operation attributes\n * itself to. Set it ONCE at a boundary (or wrap a path with `bucket()`); it\n * rides Node's AsyncLocalStorage down through every async fan-out, so one\n * handler that triggers 15 reads attributes all 15 to the same bucket — with\n * zero per-call-site work.\n *\n * Generic by design: unlike a hardcoded product taxonomy, the only meaningful\n * field a consumer sets is the free-form `label` (the bucket name). `feature`\n * is an optional coarse grouping if you want one; nothing here is\n * Crossdeck-specific.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nexport interface CostTag {\n /** Optional coarse grouping (a caller-defined surface name). */\n feature?: string;\n /** The bucket name — what `bucket()` sets. Drives the report's `byLabel`. */\n label?: string;\n}\n\nconst DEFAULT_TAG: CostTag = {};\nconst store = new AsyncLocalStorage<CostTag>();\n\n/** Run `fn` with `tag` bound for its entire async subtree. */\nexport function runWithCostTag<T>(tag: CostTag, fn: () => T): T {\n return store.run({ ...tag }, fn);\n}\n\n/** Bind a tag for the remainder of the current async context (no closure to wrap). */\nexport function enterCostTag(tag: CostTag): void {\n store.enterWith({ ...tag });\n}\n\n/** Refine the live tag in place (e.g. stamp a feature after the boundary). */\nexport function refineCostTag(patch: Partial<CostTag>): void {\n const cur = store.getStore();\n if (cur) Object.assign(cur, patch);\n}\n\n/** The current tag, or a safe empty default outside any bound context. */\nexport function currentCostTag(): CostTag {\n return store.getStore() ?? DEFAULT_TAG;\n}\n\n/** Hierarchy separator for bucket paths — Firestore-map-key-safe and distinct from\n * the \"col:\" leaf prefix. `bucket(\"a\", () => bucket(\"b\", …))` → \"a>b\". */\nexport const BUCKET_SEP = \">\";\n\n/**\n * `bucket(name, fn)` — the headline verb, the `track()` of cost. Run `fn` with\n * every operation inside it attributed to the bucket `name`; the attribution rides\n * the async subtree automatically. NESTS: a `bucket()` inside another COMPOSES into\n * a path (`\"analytics\" > \"rollup\"` → `\"analytics>rollup\"`), so the dashboard can\n * drill from the coarse bucket down into its parts. The one verb most developers touch:\n *\n * await bucket(\"analytics\", () =>\n * bucket(\"rollup\", () => db.collection(\"events\").where(...).get())); // → \"analytics>rollup>col:events\"\n */\nexport function bucket<T>(name: string, fn: () => T): T {\n const parent = currentCostTag().label;\n const path = parent ? `${parent}${BUCKET_SEP}${name}` : name;\n return runWithCostTag({ ...currentCostTag(), label: path }, fn);\n}\n","/**\n * cost-meter — counts operations against the ambient tag and flushes them to the\n * configured Sink cheaply.\n *\n * LOW-OVERHEAD CONTRACT (the thing that warns you about reads must not run them\n * up): counts accumulate in an in-memory buffer and flush periodically — NEVER one\n * network call per counted operation. A flush coalesces the whole window into one\n * report per UTC day and hands it to the Sink. At steady state that is ~1 small\n * request a minute, regardless of how many ops you served.\n *\n * BEST-EFFORT CONTRACT: metering must never throw into your code. Every recorder\n * swallows its own errors; a failed flush drops that window's counts (surfaced via\n * `onError` if you pass one) rather than disturbing the app.\n */\nimport { currentCostTag } from \"./cost-context\";\nimport type { Sink, BucketsReport, ResourceCounts } from \"./sink\";\n\n/**\n * A resource unit — what an adapter counts. Firestore emits `read`/`write`/\n * `delete`; other adapters emit their own (`clickhouse.query_ms`, `openai.tokens`).\n * It is a free identifier on purpose, BUT each one is kept entirely distinct: the\n * meter only ever sums quantities WITHIN a single resource, never across two.\n */\nexport type ResourceUnit = string;\n/** @deprecated — Firestore-era name; use {@link ResourceUnit}. */\nexport type OpType = ResourceUnit;\n\n/** Optional read-site hint — the collection touched, derived at the trap from the\n * path. Lets an UNtagged read cascade to `col:<collection>` instead of vanishing. */\nexport interface CostHint {\n collection?: string;\n projectId?: string;\n}\n\n// NUL separator — a bucket/collection name can contain almost anything except\n// this, so the key never collides with a name that has a \"|\" or \":\" in it.\nconst SEP = \"\\u001f\"; // ASCII Unit Separator\n\n/** key = date <NUL> resource <NUL> label → count */\nconst labelBuffer = new Map<string, number>();\n/** key = date <NUL> resource <NUL> hour → count */\nconst hourBuffer = new Map<string, number>();\n/** key = date <NUL> resource <NUL> 5-min-slot (\"HHMM\") → count. The fine grain\n * that lets a developer verify against the provider console's \"last hour\" within\n * minutes of installing — not after a day. Still one maintained doc, zero reads. */\nconst minuteBuffer = new Map<string, number>();\n\nlet sink: Sink | null = null;\nlet flushIntervalMs = 60_000;\nlet onError: ((e: unknown) => void) | null = null;\nlet timer: ReturnType<typeof setInterval> | null = null;\nlet flushing = false;\n/** Safety valve — flush early if a burst fills the buffer between intervals. */\nconst MAX_BUFFER_KEYS = 5_000;\n\nexport interface MeterConfig {\n sink: Sink;\n flushIntervalMs?: number;\n onError?: (e: unknown) => void;\n}\n\n/** Point the meter at a sink. Called by `init()`; pass your own sink to self-host. */\nexport function configureMeter(config: MeterConfig): void {\n sink = config.sink;\n if (config.flushIntervalMs && config.flushIntervalMs > 0) flushIntervalMs = config.flushIntervalMs;\n onError = config.onError ?? null;\n}\n\nconst utcDate = (): string => new Date().toISOString().slice(0, 10);\nconst utcHour = (): string => new Date().toISOString().slice(11, 13);\n/** UTC 5-minute slot of the day as \"HHMM\" — the slot START (e.g. 08:47 → \"0845\"). */\nconst utcFiveMin = (): string => {\n const iso = new Date().toISOString();\n const hh = iso.slice(11, 13);\n const mm = Math.floor(Number(iso.slice(14, 16)) / 5) * 5;\n return hh + String(mm).padStart(2, \"0\");\n};\n\nfunction ensureFlushLoop(): void {\n if (timer) return;\n timer = setInterval(() => void flush(), flushIntervalMs);\n // Don't keep the event loop alive just for metering.\n (timer as { unref?: () => void }).unref?.();\n // Flush the last window on shutdown.\n process.once?.(\"SIGTERM\", () => void flush());\n process.once?.(\"beforeExit\", () => void flush());\n}\n\n/**\n * Count `quantity` of a `resource` against the live tag. THE adapter primitive —\n * a Firestore adapter records \"read\"; a ClickHouse adapter records\n * \"clickhouse.query_ms\"; an OpenAI adapter records \"openai.tokens\". Each resource\n * is bucketed entirely on its own; nothing is ever added across resources. Never\n * throws.\n */\nexport function record(resource: ResourceUnit, quantity: number, hint?: CostHint): void {\n try {\n if (!Number.isFinite(quantity) || quantity <= 0) return;\n const t = currentCostTag();\n const date = utcDate();\n // HIERARCHY — the bucket path is the trunk; the collection is the LEAF kept\n // beneath it (so a tagged bucket still drills down to which collections it\n // read). \"analytics\" + events → \"analytics>col:events\"; untagged → \"col:events\";\n // nothing derivable → \"uncategorized\". A unit is never invisible, and a tagged\n // one never loses where it actually went.\n const coll = hint?.collection ? `col:${hint.collection}` : null;\n const label = t.label\n ? coll\n ? `${t.label}>${coll}`\n : t.label\n : coll ?? \"uncategorized\";\n // Key includes the resource, so each resource accumulates in its OWN slot.\n const lk = date + SEP + resource + SEP + label;\n labelBuffer.set(lk, (labelBuffer.get(lk) ?? 0) + quantity);\n const hk = date + SEP + resource + SEP + utcHour();\n hourBuffer.set(hk, (hourBuffer.get(hk) ?? 0) + quantity);\n const mk = date + SEP + resource + SEP + utcFiveMin();\n minuteBuffer.set(mk, (minuteBuffer.get(mk) ?? 0) + quantity);\n ensureFlushLoop();\n if (labelBuffer.size + hourBuffer.size + minuteBuffer.size > MAX_BUFFER_KEYS) void flush();\n } catch {\n /* metering is best-effort — never disturb the caller */\n }\n}\n\n/** @deprecated — use {@link record}. Firestore-era alias. */\nexport const recordFirestore = record;\n\n/** Firestore bills a minimum of one read even for an empty result, so 0 counts as 1. */\nexport function recordReads(n: number, hint?: CostHint): void {\n record(\"read\", Math.max(n, 1), hint);\n}\nexport function recordWrites(n = 1): void {\n record(\"write\", n);\n}\nexport function recordDeletes(n = 1): void {\n record(\"delete\", n);\n}\n\nfunction add(target: Record<string, ResourceCounts>, key: string, resource: ResourceUnit, n: number): void {\n const bag = (target[key] ??= {});\n // Accumulate WITHIN this resource only — never merge resources.\n bag[resource] = (bag[resource] ?? 0) + n;\n}\n\n/**\n * Coalesce the buffer into one report per UTC day and hand each to the Sink.\n * Snapshots + clears up front so concurrent records land in the next window.\n * Never throws; a sink failure drops that window (surfaced via `onError`).\n */\nexport async function flush(): Promise<void> {\n if (flushing) return;\n // Not configured (init not called) — drop, don't grow unbounded.\n if (!sink) {\n labelBuffer.clear();\n hourBuffer.clear();\n minuteBuffer.clear();\n return;\n }\n if (labelBuffer.size === 0 && hourBuffer.size === 0 && minuteBuffer.size === 0) return;\n flushing = true;\n\n const labels = new Map(labelBuffer);\n const hours = new Map(hourBuffer);\n const minutes = new Map(minuteBuffer);\n labelBuffer.clear();\n hourBuffer.clear();\n minuteBuffer.clear();\n\n try {\n const byDate = new Map<string, BucketsReport>();\n const reportFor = (date: string): BucketsReport => {\n let r = byDate.get(date);\n if (!r) {\n r = { date, byLabel: {}, byHour: {}, byMinute: {} };\n byDate.set(date, r);\n }\n return r;\n };\n for (const [k, n] of labels) {\n const [date, op, label] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byLabel, label, op, n);\n }\n for (const [k, n] of hours) {\n const [date, op, hour] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byHour!, hour, op, n);\n }\n for (const [k, n] of minutes) {\n const [date, op, slot] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byMinute!, slot, op, n);\n }\n for (const report of byDate.values()) {\n await sink.flush(report);\n }\n } catch (e) {\n // Drop this window rather than risk a partial/double report on retry.\n onError?.(e);\n } finally {\n flushing = false;\n }\n}\n","/**\n * sink — where the meter sends a coalesced rollup, and the wire shape it sends.\n *\n * Abstracting the sink is what makes Buckets storage-agnostic: the meter never\n * knows where counts go. The DEFAULT sink (`ReportSink`) reports up to Crossdeck's\n * ingest endpoint so the numbers surface on your Crossdeck dashboard. A team that\n * wants to self-host can implement `Sink` against anything (Postgres, a file, your\n * own API) without touching the meter.\n */\n\n/**\n * Counts for ONE bucket, keyed by RESOURCE UNIT — the raw quantity of each unit,\n * nothing more (no money; you verify cost on your provider's bill).\n *\n * Firestore, the first adapter, emits `read` / `write` / `delete`. Other adapters\n * emit their own units (`clickhouse.query_ms`, `openai.tokens`, …). The rule that\n * keeps this honest: **each resource is its own line. Counts are only ever summed\n * WITHIN a resource, NEVER across one.** A read is not a query-millisecond; the two\n * never land in the same number. (There is deliberately no \"total units\" field.)\n */\nexport interface ResourceCounts {\n read?: number;\n write?: number;\n delete?: number;\n /** Any other resource unit an adapter emits — kept distinct, never merged. */\n [resource: string]: number | undefined;\n}\n\n/** @deprecated name — kept for back-compat; use {@link ResourceCounts}. */\nexport type OpCounts = ResourceCounts;\n\n/**\n * One coalesced report — the wire contract (see docs/ROLLUP_SCHEMA.md). The meter\n * produces one of these per UTC day in a flush window (usually exactly one).\n */\nexport interface BucketsReport {\n /** UTC day \"YYYY-MM-DD\". */\n date: string;\n /** bucket name → counts. The heart of the report. */\n byLabel: Record<string, ResourceCounts>;\n /** UTC hour \"HH\" → counts, for the hourly \"did my fix land this hour?\" view. */\n byHour?: Record<string, ResourceCounts>;\n /** UTC 5-minute slot \"HHMM\" (slot start) → counts. The fine grain for fast\n * verification against a provider console's \"last hour\" view. */\n byMinute?: Record<string, ResourceCounts>;\n}\n\n/**\n * A destination for coalesced rollups. `flush` MAY throw on failure — the meter\n * catches it, drops that one window, and never lets it reach your app.\n */\nexport interface Sink {\n flush(report: BucketsReport): Promise<void>;\n}\n\nexport interface ReportSinkConfig {\n /** The project's `cd_sk_` secret key. Server-to-server only. */\n apiKey: string;\n /** Defaults to https://api.cross-deck.com/v1/buckets/report */\n endpoint?: string;\n /** Request timeout (ms); a slow Crossdeck must never stall your flush. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_ENDPOINT = \"https://api.cross-deck.com/v1/buckets/report\";\n\n/**\n * The default sink: POST one coalesced rollup to Crossdeck's ingest endpoint.\n * The ingest folds it into the day's maintained doc with `increment`, so many\n * reports a minute coalesce safely. This path does ZERO database reads — it sends\n * a summary, it does not read. Throws on a non-202 so the meter can log/drop the\n * window; the meter guarantees it never reaches your app.\n */\nexport class ReportSink implements Sink {\n private readonly endpoint: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n\n constructor(config: ReportSinkConfig) {\n this.endpoint = config.endpoint ?? DEFAULT_ENDPOINT;\n this.apiKey = config.apiKey;\n this.timeoutMs = config.timeoutMs ?? 5000;\n }\n\n async flush(report: BucketsReport): Promise<void> {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n signal: AbortSignal.timeout(this.timeoutMs),\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(report),\n });\n if (res.status !== 202) {\n throw new Error(`Buckets report rejected: HTTP ${res.status}`);\n }\n }\n}\n\n/**\n * A sink that does nothing. Used when the local mirror is turned off AND no upstream\n * was configured — the meter still needs a sink to flush into, but there's nowhere to\n * send. Counts are dropped by the developer's explicit choice.\n */\nexport class NullSink implements Sink {\n async flush(): Promise<void> {\n /* intentionally empty */\n }\n}\n","/**\n * readout — renders the local file a developer (or their AI session) reads back with\n * \"read me my buckets\". PURE string building: no I/O, no database reads. The node\n * mirror (./mirror) writes this to `.crossdeck/buckets.md` on each flush, so the\n * readout works offline, for free, with no account.\n */\nimport type { BucketsReport, ResourceCounts } from \"./sink\";\n\n/**\n * The one line that closes every readout. Plain and factual: what the OSS shows you\n * here, and what signing up adds — for free. No invented numbers, no urgency, no pitch.\n */\nexport const READOUT_FOOTER =\n \"Buckets OSS shows the reads on this surface. Sign up to Crossdeck (free) to see \" +\n \"every surface in one view, drill any bucket down to the exact query, track a fix \" +\n \"before and after, and get paged when reads spike — cross-deck.com\";\n\nfunction fmt(n: number): string {\n if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + \"M\";\n if (n >= 10_000) return Math.round(n / 1000) + \"K\";\n if (n >= 1000) return (n / 1000).toFixed(1) + \"K\";\n return String(Math.round(n));\n}\n\n/** A bucket is untagged when its ROOT segment is a bare collection / catch-all. */\nfunction isUntagged(label: string): boolean {\n const root = label.split(\">\")[0];\n return root.startsWith(\"col:\") || root === \"uncategorized\" || root === \"unknown\";\n}\n\n/** Pretty path: strip the \"col:\" leaf prefix, join the hierarchy with \" › \". */\nfunction displayLabel(label: string): string {\n return label\n .split(\">\")\n .map((s) => (s.startsWith(\"col:\") ? s.slice(4) : s))\n .join(\" › \");\n}\n\n/** Render the day's coalesced report as a human/AI-readable markdown readout. */\nexport function renderReadout(report: BucketsReport): string {\n const entries = Object.entries(report.byLabel ?? {})\n .map(([label, counts]) => ({ label, reads: (counts as ResourceCounts).read ?? 0 }))\n .filter((e) => e.reads > 0)\n .sort((a, b) => b.reads - a.reads);\n\n const total = entries.reduce((s, e) => s + e.reads, 0);\n const out: string[] = [];\n out.push(`# Buckets — reads on this surface`);\n out.push(``);\n out.push(`**${fmt(total)} reads** · ${report.date} (UTC)`);\n out.push(``);\n\n if (entries.length === 0) {\n out.push(`No reads metered yet — install the collector and let your app serve some traffic.`);\n } else {\n out.push(`| bucket | named | reads |`);\n out.push(`| --- | :---: | ---: |`);\n for (const e of entries) {\n out.push(`| ${displayLabel(e.label)} | ${isUntagged(e.label) ? \"—\" : \"✓\"} | ${fmt(e.reads)} |`);\n }\n }\n\n out.push(``);\n out.push(`---`);\n out.push(READOUT_FOOTER);\n out.push(``);\n return out.join(\"\\n\");\n}\n","/**\n * mirror — tees every coalesced report to a local file so \"read me my buckets\" works\n * offline, free, with no account. Writes a human/AI-readable readout\n * (`.crossdeck/buckets.md`) plus the raw report (`.crossdeck/buckets.json`).\n *\n * NODE ONLY — never imported by the browser build (it touches the filesystem).\n *\n * Two contracts it keeps:\n * - NO MONSTER: it only ever WRITES local files (~one small write a minute). It never\n * reads your database; the report is already in hand.\n * - BEST-EFFORT: a write error is swallowed — the local mirror must never disturb a\n * flush or reach your app.\n *\n * The meter hands each flush a DELTA (the window's counts, then clears). To show the\n * day's running total, the mirror accumulates deltas in memory, seeded once from any\n * existing file so a process restart doesn't shrink the readout.\n */\nimport { mkdirSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Sink, BucketsReport, ResourceCounts } from \"./sink\";\nimport { renderReadout } from \"./readout\";\n\nexport const DEFAULT_MIRROR_DIR = \".crossdeck\";\n\nfunction mergeInto(target: Record<string, ResourceCounts>, src?: Record<string, ResourceCounts>): void {\n if (!src) return;\n for (const [key, counts] of Object.entries(src)) {\n const bag = (target[key] ??= {});\n for (const [res, n] of Object.entries(counts)) {\n if (typeof n === \"number\") bag[res] = (bag[res] ?? 0) + n;\n }\n }\n}\n\n/**\n * Wraps an optional upstream sink. On each flush it writes the running day-total\n * locally, THEN (if an upstream sink was given — i.e. a key) reports onward.\n * With no upstream it is a pure local meter: the wedge, working with no account.\n */\nexport class MirrorSink implements Sink {\n private acc: BucketsReport | null = null;\n private announced = false;\n private seeded = false;\n\n constructor(\n private readonly upstream: Sink | null,\n private readonly dir: string = DEFAULT_MIRROR_DIR,\n ) {}\n\n private jsonPath(): string {\n return join(this.dir, \"buckets.json\");\n }\n\n /** Seed the running total once from an existing same-day file (survives restarts). */\n private seed(date: string): void {\n if (this.seeded) return;\n this.seeded = true;\n try {\n const prior = JSON.parse(readFileSync(this.jsonPath(), \"utf8\")) as BucketsReport;\n if (prior?.date === date && prior.byLabel) this.acc = prior;\n } catch {\n /* no prior file (or unreadable) — start fresh */\n }\n }\n\n async flush(report: BucketsReport): Promise<void> {\n // Local first — the part that always works, key or no key.\n try {\n this.seed(report.date);\n if (!this.acc || this.acc.date !== report.date) {\n this.acc = { date: report.date, byLabel: {}, byHour: {}, byMinute: {} };\n }\n mergeInto(this.acc.byLabel, report.byLabel);\n mergeInto((this.acc.byHour ??= {}), report.byHour);\n mergeInto((this.acc.byMinute ??= {}), report.byMinute);\n\n mkdirSync(this.dir, { recursive: true });\n writeFileSync(join(this.dir, \"buckets.md\"), renderReadout(this.acc));\n writeFileSync(this.jsonPath(), JSON.stringify(this.acc, null, 2));\n\n if (!this.announced) {\n this.announced = true;\n // One quiet line, once, so a developer knows where to read it back.\n // eslint-disable-next-line no-console\n console.log(\n `Buckets: readout at ${join(this.dir, \"buckets.md\")} — open it, or ask your AI session to \"read me my buckets\".`,\n );\n }\n } catch {\n /* local mirror is best-effort */\n }\n\n if (this.upstream) await this.upstream.flush(report);\n }\n}\n","/**\n * adapters/firestore — the universal Firestore read meter (the trap).\n *\n * THE LESSON (learned on a real product): per-call-site `recordReads()`\n * instrumentation MISSES paths. You meter the read sites you're looking at and\n * leave the cron / trigger / ingest path uncounted — often the majority of reads,\n * invisible. Humans tag what they see and miss the path that matters.\n *\n * THE FIX: patch the admin SDK's read methods ONCE. From install onward, EVERY\n * read — anywhere, on any code path — is counted under the ambient tag, with zero\n * per-call-site work and no blind spots.\n *\n * SAFETY CONTRACT — this sits on your production read path, so it is defensive by\n * construction. Each wrapper:\n * 1. calls the REAL method first and captures the result,\n * 2. counts in a try/catch that can never throw into the caller,\n * 3. ALWAYS returns the real result, untouched.\n * It cannot break a read, change a result, or add latency beyond one in-memory\n * counter increment. A wrong count is a measurement error, never a correctness or\n * availability one. Idempotent — calling it twice patches once.\n *\n * COUNTING MODEL — a query returning N docs = N reads (an empty result still bills\n * 1, which the meter enforces). A document get = 1. getAll(...) = the ref count.\n * CollectionReference.get IS Query.get (shared prototype method), so patching Query\n * covers collections with no double-count.\n */\nimport { recordReads, type CostHint } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/**\n * The firebase-admin Firestore classes to patch. Pass the module namespace from\n * `firebase-admin/firestore` — only the prototypes present are patched.\n */\nexport interface FirestoreClasses {\n Query?: { prototype: { get?: AnyFn; onSnapshot?: AnyFn } };\n DocumentReference?: { prototype: { get?: AnyFn; onSnapshot?: AnyFn } };\n Transaction?: { prototype: { get?: AnyFn; getAll?: AnyFn } };\n Firestore?: { prototype: { getAll?: AnyFn } };\n /** count() / sum() / average() — aggregation queries bill reads too. */\n AggregateQuery?: { prototype: { get?: AnyFn } };\n}\n\n/** `projects/{id}/…` → the project id, else undefined. Pure string op. */\nfunction projectFromPath(path: string): string | undefined {\n const parts = path.split(\"/\");\n const i = parts.indexOf(\"projects\");\n return i >= 0 && parts[i + 1] ? parts[i + 1] : undefined;\n}\n\n/**\n * Derive { collection, projectId } from the read target's path so an UNtagged read\n * cascades to `col:<collection>` instead of \"uncategorized\". PURE CPU; never reads,\n * never throws. Falls back to firebase-admin's internal `_queryOptions` for filtered\n * queries (which don't expose `.path`).\n */\nfunction hintFrom(target: any): CostHint | undefined {\n try {\n const p = typeof target?.path === \"string\" ? target.path : \"\";\n if (p) {\n const parts = p.split(\"/\").filter(Boolean);\n const collection = parts.length % 2 === 0 ? parts[parts.length - 2] : parts[parts.length - 1];\n return { collection, projectId: projectFromPath(p) };\n }\n const qo = target?._queryOptions;\n if (qo) {\n const collection = typeof qo.collectionId === \"string\" ? qo.collectionId : undefined;\n const parent =\n typeof qo.parentPath?.relativeName === \"string\"\n ? qo.parentPath.relativeName\n : typeof qo.parentPath?.toString === \"function\"\n ? String(qo.parentPath.toString())\n : \"\";\n return { collection, projectId: parent ? projectFromPath(parent) : undefined };\n }\n } catch {\n /* the meter must never throw */\n }\n return undefined;\n}\n\nfunction meterSnap(snap: unknown, hint?: CostHint): void {\n try {\n const size = (snap as { size?: number } | null)?.size;\n recordReads(typeof size === \"number\" ? size : 1, hint);\n } catch {\n /* best-effort */\n }\n}\nfunction meterCount(n: number, hint?: CostHint): void {\n try {\n recordReads(n, hint);\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * Install the universal read meter on the firebase-admin Firestore classes. Call\n * ONCE at process start, before any reads. Pass the namespace from\n * `firebase-admin/firestore` so the exact prototypes the SDK uses are patched:\n *\n * import * as Firestore from \"firebase-admin/firestore\";\n * installFirestoreMeter(Firestore);\n */\nexport function installFirestoreMeter(classes: FirestoreClasses): void {\n if (installed) return;\n installed = true;\n const { Query, DocumentReference, Transaction, Firestore, AggregateQuery } = classes;\n\n // Query.get — covers Query AND CollectionReference (shared prototype method).\n const qGet = Query?.prototype?.get;\n if (qGet) {\n Query!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await qGet.apply(this, args);\n meterSnap(snap, hintFrom(this));\n return snap;\n };\n }\n\n // DocumentReference.get — a single doc = 1 read.\n const dGet = DocumentReference?.prototype?.get;\n if (dGet) {\n DocumentReference!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await dGet.apply(this, args);\n meterCount(1, hintFrom(this));\n return snap;\n };\n }\n\n // Transaction.get — query or doc; size when present, else 1.\n const tGet = Transaction?.prototype?.get;\n if (tGet) {\n Transaction!.prototype.get = async function (this: unknown, ...args: any[]) {\n const res = await tGet.apply(this, args);\n meterSnap(res, hintFrom(args[0]));\n return res;\n };\n }\n\n // Transaction.getAll(...refs) — one read per ref.\n const tGetAll = Transaction?.prototype?.getAll;\n if (tGetAll) {\n Transaction!.prototype.getAll = async function (this: unknown, ...args: any[]) {\n const res = await tGetAll.apply(this, args);\n meterCount(Array.isArray(res) ? res.length : args.length || 1, hintFrom(args[0]));\n return res;\n };\n }\n\n // Firestore.getAll(...refs) — batched doc reads.\n const fGetAll = Firestore?.prototype?.getAll;\n if (fGetAll) {\n Firestore!.prototype.getAll = async function (this: unknown, ...args: any[]) {\n const res = await fGetAll.apply(this, args);\n meterCount(Array.isArray(res) ? res.length : args.length || 1, hintFrom(args[0]));\n return res;\n };\n }\n\n // AggregateQuery.get — count() / sum() / average(). Firestore bills one read per\n // up to 1000 index entries scanned, and the client never sees that entry count —\n // so this is an HONEST ESTIMATE: for count() we derive it from the result\n // (ceil(count / 1000)), else the billed minimum of 1. Observe-only: it reads the\n // result you already got, adds zero reads. (Closes the aggregation blind spot.)\n const aGet = AggregateQuery?.prototype?.get;\n if (aGet) {\n AggregateQuery!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await aGet.apply(this, args);\n try {\n const data = (snap as { data?: () => { count?: number } } | null)?.data?.();\n const count = typeof data?.count === \"number\" ? data.count : 0;\n meterCount(Math.max(1, Math.ceil(count / 1000)), hintFrom(this));\n } catch {\n /* best-effort */\n }\n return snap;\n };\n }\n\n // Query.onSnapshot / DocumentReference.onSnapshot — server-side realtime\n // listeners. We OBSERVE: wrap onNext and count the docs each fire delivers (a\n // query's changed docs — first fire = all matching; a doc = 1). We attach no\n // listener and issue no read; the meter just sees what the listener is already\n // billed. (Closes the server-listener blind spot.)\n const patchOnSnapshot = (proto: { onSnapshot?: AnyFn } | undefined, perDoc: boolean): void => {\n const real = proto?.onSnapshot;\n if (!real) return;\n proto!.onSnapshot = function (this: unknown, ...args: any[]) {\n const hint = hintFrom(this);\n const i = args.findIndex((a) => typeof a === \"function\");\n if (i >= 0) {\n const onNext = args[i];\n args[i] = function (snap: any) {\n try {\n const n = perDoc\n ? 1\n : typeof snap?.docChanges === \"function\"\n ? snap.docChanges().length\n : typeof snap?.size === \"number\"\n ? snap.size\n : 1;\n if (n > 0) meterCount(n, hint);\n } catch {\n /* best-effort */\n }\n return onNext(snap);\n };\n }\n return real.apply(this, args);\n };\n };\n patchOnSnapshot(Query?.prototype, false);\n patchOnSnapshot(DocumentReference?.prototype, true);\n}\n\n/** Test-only: reset the install guard so a suite can re-patch fresh prototypes. */\nexport function __resetFirestoreMeterForTests(): void {\n installed = false;\n}\n","/**\n * adapters/mongo — the MongoDB read meter (the trap), mirroring the Firestore one.\n *\n * THE RAW UNIT: like Firestore (a query returning N docs = N reads), MongoDB's raw\n * unit is DOCUMENTS READ — the documents each read operation returns, attributed to\n * the feature (bucket) that ran it. This is a real, countable number, NOT a dollar\n * bill. (MongoDB bills by cluster/compute, not per read — so this is the read LOAD by\n * feature: which queries pull the most documents, the thing you index/narrow/cache to\n * run a smaller cluster.) Raw counts only, no money — the two laws hold.\n *\n * THE FIX it brings: per-call-site instrumentation misses paths. Patch the driver's\n * result-returning read methods ONCE, and from install on every read — anywhere, any\n * code path — is counted under the ambient tag with no blind spots.\n *\n * MECHANISM: the wrappers run in the CALLER's own async context (a `find().toArray()`\n * inside `bucket(\"feed\")` resolves in that context), so attribution survives with zero\n * per-call-site work. Observe-only: it counts the result already in hand — it adds NO\n * query (no `explain()`, no profiler scan), so it can never become a read monster.\n *\n * SAFETY CONTRACT (it sits on your production read path): each wrapper calls the REAL\n * method first, counts in a try/catch that can never throw into the caller, and ALWAYS\n * returns the real result untouched. A wrong count is a measurement error, never a\n * correctness or availability one. Idempotent — calling it twice patches once.\n *\n * Pass the classes from your `mongodb` import (an OPTIONAL peer dep — installing this\n * never forces the driver on a Firestore user):\n *\n * import { FindCursor, AggregationCursor, Collection } from \"mongodb\";\n * import { installMongoMeter } from \"@cross-deck/buckets/mongo\";\n * installMongoMeter({ FindCursor, AggregationCursor, Collection });\n */\nimport { record, type CostHint } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/** MongoDB's raw read unit — documents returned by a read operation. A count. */\nexport const MONGO_READ_UNIT = \"mongo.docs_read\";\n\n/**\n * The `mongodb` driver classes to patch. Pass them from your `mongodb` import — only\n * the prototypes present are patched, so a driver-version mismatch degrades to\n * \"counts fewer paths\", never a crash.\n */\nexport interface MongoClasses {\n /** find() cursor — `.toArray()` resolves the matched documents. */\n FindCursor?: { prototype: { toArray?: AnyFn } };\n /** aggregate() cursor — `.toArray()` resolves the pipeline output documents. */\n AggregationCursor?: { prototype: { toArray?: AnyFn } };\n /** Collection — `.findOne()` resolves a single document (or null). */\n Collection?: { prototype: { findOne?: AnyFn } };\n}\n\n/** Best-effort `col:<collection>` cascade for an UNtagged read. PURE; never throws. */\nfunction hintFrom(target: any): CostHint | undefined {\n try {\n // FindCursor / AggregationCursor expose `.namespace` (MongoDBNamespace); a\n // Collection exposes `.collectionName`. Either gives us the collection.\n const ns = target?.namespace;\n if (ns && typeof ns.collection === \"string\" && ns.collection) {\n return { collection: ns.collection };\n }\n if (typeof target?.collectionName === \"string\" && target.collectionName) {\n return { collection: target.collectionName };\n }\n } catch {\n /* the meter must never throw */\n }\n return undefined;\n}\n\nfunction meter(n: number, hint?: CostHint): void {\n try {\n if (n > 0) record(MONGO_READ_UNIT, n, hint);\n } catch {\n /* best-effort — never disturb the caller */\n }\n}\n\n/**\n * Install the MongoDB read meter on the driver's read-result methods. Call ONCE at\n * process start, before any reads. Pass the classes from your `mongodb` import.\n */\nexport function installMongoMeter(classes: MongoClasses): void {\n if (installed) return;\n installed = true;\n const { FindCursor, AggregationCursor, Collection } = classes;\n\n // find().toArray() / aggregate().toArray() — the documents the query returned.\n const patchToArray = (proto: { toArray?: AnyFn } | undefined): void => {\n const real = proto?.toArray;\n if (!real) return;\n proto!.toArray = async function (this: unknown, ...args: any[]) {\n const out = await real.apply(this, args);\n meter(Array.isArray(out) ? out.length : 0, hintFrom(this));\n return out;\n };\n };\n patchToArray(FindCursor?.prototype);\n patchToArray(AggregationCursor?.prototype);\n\n // findOne() — one document (or null). A found doc is 1 read; null returned nothing.\n const realFindOne = Collection?.prototype?.findOne;\n if (realFindOne) {\n Collection!.prototype.findOne = async function (this: unknown, ...args: any[]) {\n const out = await realFindOne.apply(this, args);\n meter(out == null ? 0 : 1, hintFrom(this));\n return out;\n };\n }\n}\n\n/** Test-only: reset the install guard so a suite can re-patch fresh prototypes. */\nexport function __resetMongoMeterForTests(): void {\n installed = false;\n}\n","/**\n * @cross-deck/buckets — know exactly what every database read costs you, and who\n * caused it. A tiny, never-throws collector for Firestore.\n *\n * The whole footprint a consumer sees:\n * 1. init({ apiKey, firestore }) — configure once, install the trap once\n * 2. bucket(name, fn) — name the read paths that matter\n * 3. (the dashboard shows the rest — and names the ones you haven't yet)\n */\nimport { configureMeter, type MeterConfig } from \"./cost-meter\";\nimport { ReportSink, NullSink, type Sink } from \"./sink\";\nimport { MirrorSink, DEFAULT_MIRROR_DIR } from \"./mirror\";\nimport { installFirestoreMeter, type FirestoreClasses } from \"./adapters/firestore\";\n\nexport interface InitOptions {\n /**\n * The project's `cd_sk_` SECRET key. Server-to-server only — never a browser key.\n * OPTIONAL: with no key, Buckets still meters locally and writes the readout to\n * disk (`.crossdeck/buckets.md`) — the free, no-account wedge. Add a key and it\n * also reports up to Crossdeck so the numbers surface on your dashboard.\n */\n apiKey?: string;\n /**\n * Pass the namespace from `firebase-admin/firestore` to auto-install the read\n * trap (recommended — this is what makes every read count with no per-call work).\n * Omit it if you'd rather call `installFirestoreMeter()` yourself, or you only\n * use the manual `recordReads()` recorders.\n */\n firestore?: FirestoreClasses;\n /** Override the report endpoint (defaults to Crossdeck's ingest). */\n endpoint?: string;\n /** How often to flush coalesced counts (ms). Default 60_000. */\n flushIntervalMs?: number;\n /** Bring your own sink (self-host the rollups). Defaults to reporting up to Crossdeck. */\n sink?: Sink;\n /**\n * Where to write the local readout — the file \"read me my buckets\" reads back.\n * Defaults to `.crossdeck`. Pass `false` to turn the local mirror off entirely.\n */\n mirror?: string | false;\n /** Notified when a flush fails, so a dropped window is never silent. Best-effort. */\n onError?: MeterConfig[\"onError\"];\n}\n\n/**\n * Configure Buckets once, at process start. Always meters locally and writes the\n * readout to disk; if you pass `apiKey` (or your own `sink`) it ALSO reports up to\n * Crossdeck. Pass `firestore` to install the universal read trap so every read counts\n * automatically.\n */\nexport function init(options: InitOptions = {}): void {\n // Upstream: your sink, else a Crossdeck reporter if a key was given, else nothing.\n const upstream: Sink | null =\n options.sink ?? (options.apiKey ? new ReportSink({ apiKey: options.apiKey, endpoint: options.endpoint }) : null);\n // Default behaviour tees every flush to a local readout; `mirror:false` opts out.\n const sink: Sink =\n options.mirror === false\n ? upstream ?? new NullSink()\n : new MirrorSink(upstream, typeof options.mirror === \"string\" ? options.mirror : DEFAULT_MIRROR_DIR);\n configureMeter({ sink, flushIntervalMs: options.flushIntervalMs, onError: options.onError });\n if (options.firestore) installFirestoreMeter(options.firestore);\n}\n\n/** Alias — reads well next to `bucket()` at a call site. */\nexport { init as initBuckets };\n\n// The headline verb + the lower-level tag controls it is sugar over.\nexport {\n bucket,\n runWithCostTag,\n enterCostTag,\n refineCostTag,\n currentCostTag,\n type CostTag,\n} from \"./cost-context\";\n\n// Recorders. `record(resource, quantity)` is the generic adapter primitive — count\n// any resource unit (a future adapter records \"clickhouse.query_ms\"); recordReads/\n// Writes/Deletes are the Firestore conveniences over it.\nexport {\n record,\n recordReads,\n recordWrites,\n recordDeletes,\n flush,\n type CostHint,\n type ResourceUnit,\n type OpType,\n type MeterConfig,\n} from \"./cost-meter\";\n\n// The datastore traps + their class shapes. Re-exported from THIS entry so they\n// share the meter's module-level state — a separate bundle would get its own meter\n// instance and silently drop the counts.\nexport { installFirestoreMeter, type FirestoreClasses } from \"./adapters/firestore\";\nexport { installMongoMeter, type MongoClasses, MONGO_READ_UNIT } from \"./mongo\";\n\n// The sink seam — for self-hosting rollups instead of reporting to Crossdeck.\nexport {\n ReportSink,\n NullSink,\n type Sink,\n type BucketsReport,\n type ResourceCounts,\n type OpCounts,\n type ReportSinkConfig,\n} from \"./sink\";\n\n// The local readout — the file \"read me my buckets\" reads back, and its renderer.\nexport { MirrorSink, DEFAULT_MIRROR_DIR } from \"./mirror\";\nexport { renderReadout, READOUT_FOOTER } from \"./readout\";\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/cost-context.ts","../src/cost-meter.ts","../src/sink.ts","../src/readout.ts","../src/mirror.ts","../src/adapters/firestore.ts","../src/mongo/index.ts","../src/postgres/index.ts","../src/index.ts"],"names":["installed","hintFrom","sink"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * cost-context — the request-scoped tag every counted operation attributes\n * itself to. Set it ONCE at a boundary (or wrap a path with `bucket()`); it\n * rides Node's AsyncLocalStorage down through every async fan-out, so one\n * handler that triggers 15 reads attributes all 15 to the same bucket — with\n * zero per-call-site work.\n *\n * Generic by design: unlike a hardcoded product taxonomy, the only meaningful\n * field a consumer sets is the free-form `label` (the bucket name). `feature`\n * is an optional coarse grouping if you want one; nothing here is\n * Crossdeck-specific.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nexport interface CostTag {\n /** Optional coarse grouping (a caller-defined surface name). */\n feature?: string;\n /** The bucket name — what `bucket()` sets. Drives the report's `byLabel`. */\n label?: string;\n}\n\nconst DEFAULT_TAG: CostTag = {};\nconst store = new AsyncLocalStorage<CostTag>();\n\n/** Run `fn` with `tag` bound for its entire async subtree. */\nexport function runWithCostTag<T>(tag: CostTag, fn: () => T): T {\n return store.run({ ...tag }, fn);\n}\n\n/** Bind a tag for the remainder of the current async context (no closure to wrap). */\nexport function enterCostTag(tag: CostTag): void {\n store.enterWith({ ...tag });\n}\n\n/** Refine the live tag in place (e.g. stamp a feature after the boundary). */\nexport function refineCostTag(patch: Partial<CostTag>): void {\n const cur = store.getStore();\n if (cur) Object.assign(cur, patch);\n}\n\n/** The current tag, or a safe empty default outside any bound context. */\nexport function currentCostTag(): CostTag {\n return store.getStore() ?? DEFAULT_TAG;\n}\n\n/** Hierarchy separator for bucket paths — Firestore-map-key-safe and distinct from\n * the \"col:\" leaf prefix. `bucket(\"a\", () => bucket(\"b\", …))` → \"a>b\". */\nexport const BUCKET_SEP = \">\";\n\n/**\n * `bucket(name, fn)` — the headline verb, the `track()` of cost. Run `fn` with\n * every operation inside it attributed to the bucket `name`; the attribution rides\n * the async subtree automatically. NESTS: a `bucket()` inside another COMPOSES into\n * a path (`\"analytics\" > \"rollup\"` → `\"analytics>rollup\"`), so the dashboard can\n * drill from the coarse bucket down into its parts. The one verb most developers touch:\n *\n * await bucket(\"analytics\", () =>\n * bucket(\"rollup\", () => db.collection(\"events\").where(...).get())); // → \"analytics>rollup>col:events\"\n */\nexport function bucket<T>(name: string, fn: () => T): T {\n const parent = currentCostTag().label;\n const path = parent ? `${parent}${BUCKET_SEP}${name}` : name;\n return runWithCostTag({ ...currentCostTag(), label: path }, fn);\n}\n","/**\n * cost-meter — counts operations against the ambient tag and flushes them to the\n * configured Sink cheaply.\n *\n * LOW-OVERHEAD CONTRACT (the thing that warns you about reads must not run them\n * up): counts accumulate in an in-memory buffer and flush periodically — NEVER one\n * network call per counted operation. A flush coalesces the whole window into one\n * report per UTC day and hands it to the Sink. At steady state that is ~1 small\n * request a minute, regardless of how many ops you served.\n *\n * BEST-EFFORT CONTRACT: metering must never throw into your code. Every recorder\n * swallows its own errors; a failed flush drops that window's counts (surfaced via\n * `onError` if you pass one) rather than disturbing the app.\n */\nimport { currentCostTag } from \"./cost-context\";\nimport type { Sink, BucketsReport, ResourceCounts } from \"./sink\";\n\n/**\n * A resource unit — what an adapter counts. Firestore emits `read`/`write`/\n * `delete`; other adapters emit their own (`clickhouse.query_ms`, `openai.tokens`).\n * It is a free identifier on purpose, BUT each one is kept entirely distinct: the\n * meter only ever sums quantities WITHIN a single resource, never across two.\n */\nexport type ResourceUnit = string;\n/** @deprecated — Firestore-era name; use {@link ResourceUnit}. */\nexport type OpType = ResourceUnit;\n\n/** Optional read-site hint — the collection touched, derived at the trap from the\n * path. Lets an UNtagged read cascade to `col:<collection>` instead of vanishing. */\nexport interface CostHint {\n collection?: string;\n projectId?: string;\n}\n\n// NUL separator — a bucket/collection name can contain almost anything except\n// this, so the key never collides with a name that has a \"|\" or \":\" in it.\nconst SEP = \"\\u001f\"; // ASCII Unit Separator\n\n/** key = date <NUL> resource <NUL> label → count */\nconst labelBuffer = new Map<string, number>();\n/** key = date <NUL> resource <NUL> hour → count */\nconst hourBuffer = new Map<string, number>();\n/** key = date <NUL> resource <NUL> 5-min-slot (\"HHMM\") → count. The fine grain\n * that lets a developer verify against the provider console's \"last hour\" within\n * minutes of installing — not after a day. Still one maintained doc, zero reads. */\nconst minuteBuffer = new Map<string, number>();\n\nlet sink: Sink | null = null;\nlet flushIntervalMs = 60_000;\nlet onError: ((e: unknown) => void) | null = null;\nlet timer: ReturnType<typeof setInterval> | null = null;\nlet flushing = false;\n/** Safety valve — flush early if a burst fills the buffer between intervals. */\nconst MAX_BUFFER_KEYS = 5_000;\n\nexport interface MeterConfig {\n sink: Sink;\n flushIntervalMs?: number;\n onError?: (e: unknown) => void;\n}\n\n/** Point the meter at a sink. Called by `init()`; pass your own sink to self-host. */\nexport function configureMeter(config: MeterConfig): void {\n sink = config.sink;\n if (config.flushIntervalMs && config.flushIntervalMs > 0) flushIntervalMs = config.flushIntervalMs;\n onError = config.onError ?? null;\n}\n\nconst utcDate = (): string => new Date().toISOString().slice(0, 10);\nconst utcHour = (): string => new Date().toISOString().slice(11, 13);\n/** UTC 5-minute slot of the day as \"HHMM\" — the slot START (e.g. 08:47 → \"0845\"). */\nconst utcFiveMin = (): string => {\n const iso = new Date().toISOString();\n const hh = iso.slice(11, 13);\n const mm = Math.floor(Number(iso.slice(14, 16)) / 5) * 5;\n return hh + String(mm).padStart(2, \"0\");\n};\n\nfunction ensureFlushLoop(): void {\n if (timer) return;\n timer = setInterval(() => void flush(), flushIntervalMs);\n // Don't keep the event loop alive just for metering.\n (timer as { unref?: () => void }).unref?.();\n // Flush the last window on shutdown.\n process.once?.(\"SIGTERM\", () => void flush());\n process.once?.(\"beforeExit\", () => void flush());\n}\n\n/**\n * Count `quantity` of a `resource` against the live tag. THE adapter primitive —\n * a Firestore adapter records \"read\"; a ClickHouse adapter records\n * \"clickhouse.query_ms\"; an OpenAI adapter records \"openai.tokens\". Each resource\n * is bucketed entirely on its own; nothing is ever added across resources. Never\n * throws.\n */\nexport function record(resource: ResourceUnit, quantity: number, hint?: CostHint): void {\n try {\n if (!Number.isFinite(quantity) || quantity <= 0) return;\n const t = currentCostTag();\n const date = utcDate();\n // HIERARCHY — the bucket path is the trunk; the collection is the LEAF kept\n // beneath it (so a tagged bucket still drills down to which collections it\n // read). \"analytics\" + events → \"analytics>col:events\"; untagged → \"col:events\";\n // nothing derivable → \"uncategorized\". A unit is never invisible, and a tagged\n // one never loses where it actually went.\n const coll = hint?.collection ? `col:${hint.collection}` : null;\n const label = t.label\n ? coll\n ? `${t.label}>${coll}`\n : t.label\n : coll ?? \"uncategorized\";\n // Key includes the resource, so each resource accumulates in its OWN slot.\n const lk = date + SEP + resource + SEP + label;\n labelBuffer.set(lk, (labelBuffer.get(lk) ?? 0) + quantity);\n const hk = date + SEP + resource + SEP + utcHour();\n hourBuffer.set(hk, (hourBuffer.get(hk) ?? 0) + quantity);\n const mk = date + SEP + resource + SEP + utcFiveMin();\n minuteBuffer.set(mk, (minuteBuffer.get(mk) ?? 0) + quantity);\n ensureFlushLoop();\n if (labelBuffer.size + hourBuffer.size + minuteBuffer.size > MAX_BUFFER_KEYS) void flush();\n } catch {\n /* metering is best-effort — never disturb the caller */\n }\n}\n\n/** @deprecated — use {@link record}. Firestore-era alias. */\nexport const recordFirestore = record;\n\n/** Firestore bills a minimum of one read even for an empty result, so 0 counts as 1. */\nexport function recordReads(n: number, hint?: CostHint): void {\n record(\"read\", Math.max(n, 1), hint);\n}\nexport function recordWrites(n = 1): void {\n record(\"write\", n);\n}\nexport function recordDeletes(n = 1): void {\n record(\"delete\", n);\n}\n\nfunction add(target: Record<string, ResourceCounts>, key: string, resource: ResourceUnit, n: number): void {\n const bag = (target[key] ??= {});\n // Accumulate WITHIN this resource only — never merge resources.\n bag[resource] = (bag[resource] ?? 0) + n;\n}\n\n/**\n * Coalesce the buffer into one report per UTC day and hand each to the Sink.\n * Snapshots + clears up front so concurrent records land in the next window.\n * Never throws; a sink failure drops that window (surfaced via `onError`).\n */\nexport async function flush(): Promise<void> {\n if (flushing) return;\n // Not configured (init not called) — drop, don't grow unbounded.\n if (!sink) {\n labelBuffer.clear();\n hourBuffer.clear();\n minuteBuffer.clear();\n return;\n }\n if (labelBuffer.size === 0 && hourBuffer.size === 0 && minuteBuffer.size === 0) return;\n flushing = true;\n\n const labels = new Map(labelBuffer);\n const hours = new Map(hourBuffer);\n const minutes = new Map(minuteBuffer);\n labelBuffer.clear();\n hourBuffer.clear();\n minuteBuffer.clear();\n\n try {\n const byDate = new Map<string, BucketsReport>();\n const reportFor = (date: string): BucketsReport => {\n let r = byDate.get(date);\n if (!r) {\n r = { date, byLabel: {}, byHour: {}, byMinute: {} };\n byDate.set(date, r);\n }\n return r;\n };\n for (const [k, n] of labels) {\n const [date, op, label] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byLabel, label, op, n);\n }\n for (const [k, n] of hours) {\n const [date, op, hour] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byHour!, hour, op, n);\n }\n for (const [k, n] of minutes) {\n const [date, op, slot] = k.split(SEP) as [string, OpType, string];\n add(reportFor(date).byMinute!, slot, op, n);\n }\n for (const report of byDate.values()) {\n await sink.flush(report);\n }\n } catch (e) {\n // Drop this window rather than risk a partial/double report on retry.\n onError?.(e);\n } finally {\n flushing = false;\n }\n}\n","/**\n * sink — where the meter sends a coalesced rollup, and the wire shape it sends.\n *\n * Abstracting the sink is what makes Buckets storage-agnostic: the meter never\n * knows where counts go. The DEFAULT sink (`ReportSink`) reports up to Crossdeck's\n * ingest endpoint so the numbers surface on your Crossdeck dashboard. A team that\n * wants to self-host can implement `Sink` against anything (Postgres, a file, your\n * own API) without touching the meter.\n */\n\n/**\n * Counts for ONE bucket, keyed by RESOURCE UNIT — the raw quantity of each unit,\n * nothing more (no money; you verify cost on your provider's bill).\n *\n * Firestore, the first adapter, emits `read` / `write` / `delete`. Other adapters\n * emit their own units (`clickhouse.query_ms`, `openai.tokens`, …). The rule that\n * keeps this honest: **each resource is its own line. Counts are only ever summed\n * WITHIN a resource, NEVER across one.** A read is not a query-millisecond; the two\n * never land in the same number. (There is deliberately no \"total units\" field.)\n */\nexport interface ResourceCounts {\n read?: number;\n write?: number;\n delete?: number;\n /** Any other resource unit an adapter emits — kept distinct, never merged. */\n [resource: string]: number | undefined;\n}\n\n/** @deprecated name — kept for back-compat; use {@link ResourceCounts}. */\nexport type OpCounts = ResourceCounts;\n\n/**\n * One coalesced report — the wire contract (see docs/ROLLUP_SCHEMA.md). The meter\n * produces one of these per UTC day in a flush window (usually exactly one).\n */\nexport interface BucketsReport {\n /** UTC day \"YYYY-MM-DD\". */\n date: string;\n /** bucket name → counts. The heart of the report. */\n byLabel: Record<string, ResourceCounts>;\n /** UTC hour \"HH\" → counts, for the hourly \"did my fix land this hour?\" view. */\n byHour?: Record<string, ResourceCounts>;\n /** UTC 5-minute slot \"HHMM\" (slot start) → counts. The fine grain for fast\n * verification against a provider console's \"last hour\" view. */\n byMinute?: Record<string, ResourceCounts>;\n}\n\n/**\n * A destination for coalesced rollups. `flush` MAY throw on failure — the meter\n * catches it, drops that one window, and never lets it reach your app.\n */\nexport interface Sink {\n flush(report: BucketsReport): Promise<void>;\n}\n\nexport interface ReportSinkConfig {\n /** The project's `cd_sk_` secret key. Server-to-server only. */\n apiKey: string;\n /** Defaults to https://api.cross-deck.com/v1/buckets/report */\n endpoint?: string;\n /** Request timeout (ms); a slow Crossdeck must never stall your flush. */\n timeoutMs?: number;\n}\n\nconst DEFAULT_ENDPOINT = \"https://api.cross-deck.com/v1/buckets/report\";\n\n/**\n * The default sink: POST one coalesced rollup to Crossdeck's ingest endpoint.\n * The ingest folds it into the day's maintained doc with `increment`, so many\n * reports a minute coalesce safely. This path does ZERO database reads — it sends\n * a summary, it does not read. Throws on a non-202 so the meter can log/drop the\n * window; the meter guarantees it never reaches your app.\n */\nexport class ReportSink implements Sink {\n private readonly endpoint: string;\n private readonly apiKey: string;\n private readonly timeoutMs: number;\n\n constructor(config: ReportSinkConfig) {\n this.endpoint = config.endpoint ?? DEFAULT_ENDPOINT;\n this.apiKey = config.apiKey;\n this.timeoutMs = config.timeoutMs ?? 5000;\n }\n\n async flush(report: BucketsReport): Promise<void> {\n const res = await fetch(this.endpoint, {\n method: \"POST\",\n signal: AbortSignal.timeout(this.timeoutMs),\n headers: {\n \"content-type\": \"application/json\",\n authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify(report),\n });\n if (res.status !== 202) {\n throw new Error(`Buckets report rejected: HTTP ${res.status}`);\n }\n }\n}\n\n/**\n * A sink that does nothing. Used when the local mirror is turned off AND no upstream\n * was configured — the meter still needs a sink to flush into, but there's nowhere to\n * send. Counts are dropped by the developer's explicit choice.\n */\nexport class NullSink implements Sink {\n async flush(): Promise<void> {\n /* intentionally empty */\n }\n}\n","/**\n * readout — renders the local file a developer (or their AI session) reads back with\n * \"read me my buckets\". PURE string building: no I/O, no database reads. The node\n * mirror (./mirror) writes this to `.crossdeck/buckets.md` on each flush, so the\n * readout works offline, for free, with no account.\n */\nimport type { BucketsReport, ResourceCounts } from \"./sink\";\n\n/**\n * The one line that closes every readout. Plain and factual: what the OSS shows you\n * here, and what signing up adds — for free. No invented numbers, no urgency, no pitch.\n */\nexport const READOUT_FOOTER =\n \"Buckets OSS shows the reads on this surface. Sign up to Crossdeck (free) to see \" +\n \"every surface in one view, drill any bucket down to the exact query, track a fix \" +\n \"before and after, and get paged when reads spike — cross-deck.com\";\n\nfunction fmt(n: number): string {\n if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + \"M\";\n if (n >= 10_000) return Math.round(n / 1000) + \"K\";\n if (n >= 1000) return (n / 1000).toFixed(1) + \"K\";\n return String(Math.round(n));\n}\n\n/** A bucket is untagged when its ROOT segment is a bare collection / catch-all. */\nfunction isUntagged(label: string): boolean {\n const root = label.split(\">\")[0];\n return root.startsWith(\"col:\") || root === \"uncategorized\" || root === \"unknown\";\n}\n\n/** Pretty path: strip the \"col:\" leaf prefix, join the hierarchy with \" › \". */\nfunction displayLabel(label: string): string {\n return label\n .split(\">\")\n .map((s) => (s.startsWith(\"col:\") ? s.slice(4) : s))\n .join(\" › \");\n}\n\n/** Render the day's coalesced report as a human/AI-readable markdown readout. */\nexport function renderReadout(report: BucketsReport): string {\n const entries = Object.entries(report.byLabel ?? {})\n .map(([label, counts]) => ({ label, reads: (counts as ResourceCounts).read ?? 0 }))\n .filter((e) => e.reads > 0)\n .sort((a, b) => b.reads - a.reads);\n\n const total = entries.reduce((s, e) => s + e.reads, 0);\n const out: string[] = [];\n out.push(`# Buckets — reads on this surface`);\n out.push(``);\n out.push(`**${fmt(total)} reads** · ${report.date} (UTC)`);\n out.push(``);\n\n if (entries.length === 0) {\n out.push(`No reads metered yet — install the collector and let your app serve some traffic.`);\n } else {\n out.push(`| bucket | named | reads |`);\n out.push(`| --- | :---: | ---: |`);\n for (const e of entries) {\n out.push(`| ${displayLabel(e.label)} | ${isUntagged(e.label) ? \"—\" : \"✓\"} | ${fmt(e.reads)} |`);\n }\n }\n\n out.push(``);\n out.push(`---`);\n out.push(READOUT_FOOTER);\n out.push(``);\n return out.join(\"\\n\");\n}\n","/**\n * mirror — tees every coalesced report to a local file so \"read me my buckets\" works\n * offline, free, with no account. Writes a human/AI-readable readout\n * (`.crossdeck/buckets.md`) plus the raw report (`.crossdeck/buckets.json`).\n *\n * NODE ONLY — never imported by the browser build (it touches the filesystem).\n *\n * Two contracts it keeps:\n * - NO MONSTER: it only ever WRITES local files (~one small write a minute). It never\n * reads your database; the report is already in hand.\n * - BEST-EFFORT: a write error is swallowed — the local mirror must never disturb a\n * flush or reach your app.\n *\n * The meter hands each flush a DELTA (the window's counts, then clears). To show the\n * day's running total, the mirror accumulates deltas in memory, seeded once from any\n * existing file so a process restart doesn't shrink the readout.\n */\nimport { mkdirSync, writeFileSync, readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Sink, BucketsReport, ResourceCounts } from \"./sink\";\nimport { renderReadout } from \"./readout\";\n\nexport const DEFAULT_MIRROR_DIR = \".crossdeck\";\n\nfunction mergeInto(target: Record<string, ResourceCounts>, src?: Record<string, ResourceCounts>): void {\n if (!src) return;\n for (const [key, counts] of Object.entries(src)) {\n const bag = (target[key] ??= {});\n for (const [res, n] of Object.entries(counts)) {\n if (typeof n === \"number\") bag[res] = (bag[res] ?? 0) + n;\n }\n }\n}\n\n/**\n * Wraps an optional upstream sink. On each flush it writes the running day-total\n * locally, THEN (if an upstream sink was given — i.e. a key) reports onward.\n * With no upstream it is a pure local meter: the wedge, working with no account.\n */\nexport class MirrorSink implements Sink {\n private acc: BucketsReport | null = null;\n private announced = false;\n private seeded = false;\n\n constructor(\n private readonly upstream: Sink | null,\n private readonly dir: string = DEFAULT_MIRROR_DIR,\n ) {}\n\n private jsonPath(): string {\n return join(this.dir, \"buckets.json\");\n }\n\n /** Seed the running total once from an existing same-day file (survives restarts). */\n private seed(date: string): void {\n if (this.seeded) return;\n this.seeded = true;\n try {\n const prior = JSON.parse(readFileSync(this.jsonPath(), \"utf8\")) as BucketsReport;\n if (prior?.date === date && prior.byLabel) this.acc = prior;\n } catch {\n /* no prior file (or unreadable) — start fresh */\n }\n }\n\n async flush(report: BucketsReport): Promise<void> {\n // Local first — the part that always works, key or no key.\n try {\n this.seed(report.date);\n if (!this.acc || this.acc.date !== report.date) {\n this.acc = { date: report.date, byLabel: {}, byHour: {}, byMinute: {} };\n }\n mergeInto(this.acc.byLabel, report.byLabel);\n mergeInto((this.acc.byHour ??= {}), report.byHour);\n mergeInto((this.acc.byMinute ??= {}), report.byMinute);\n\n mkdirSync(this.dir, { recursive: true });\n writeFileSync(join(this.dir, \"buckets.md\"), renderReadout(this.acc));\n writeFileSync(this.jsonPath(), JSON.stringify(this.acc, null, 2));\n\n if (!this.announced) {\n this.announced = true;\n // One quiet line, once, so a developer knows where to read it back.\n // eslint-disable-next-line no-console\n console.log(\n `Buckets: readout at ${join(this.dir, \"buckets.md\")} — open it, or ask your AI session to \"read me my buckets\".`,\n );\n }\n } catch {\n /* local mirror is best-effort */\n }\n\n if (this.upstream) await this.upstream.flush(report);\n }\n}\n","/**\n * adapters/firestore — the universal Firestore read meter (the trap).\n *\n * THE LESSON (learned on a real product): per-call-site `recordReads()`\n * instrumentation MISSES paths. You meter the read sites you're looking at and\n * leave the cron / trigger / ingest path uncounted — often the majority of reads,\n * invisible. Humans tag what they see and miss the path that matters.\n *\n * THE FIX: patch the admin SDK's read methods ONCE. From install onward, EVERY\n * read — anywhere, on any code path — is counted under the ambient tag, with zero\n * per-call-site work and no blind spots.\n *\n * SAFETY CONTRACT — this sits on your production read path, so it is defensive by\n * construction. Each wrapper:\n * 1. calls the REAL method first and captures the result,\n * 2. counts in a try/catch that can never throw into the caller,\n * 3. ALWAYS returns the real result, untouched.\n * It cannot break a read, change a result, or add latency beyond one in-memory\n * counter increment. A wrong count is a measurement error, never a correctness or\n * availability one. Idempotent — calling it twice patches once.\n *\n * COUNTING MODEL — a query returning N docs = N reads (an empty result still bills\n * 1, which the meter enforces). A document get = 1. getAll(...) = the ref count.\n * CollectionReference.get IS Query.get (shared prototype method), so patching Query\n * covers collections with no double-count.\n */\nimport { recordReads, type CostHint } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/**\n * The firebase-admin Firestore classes to patch. Pass the module namespace from\n * `firebase-admin/firestore` — only the prototypes present are patched.\n */\nexport interface FirestoreClasses {\n Query?: { prototype: { get?: AnyFn; onSnapshot?: AnyFn } };\n DocumentReference?: { prototype: { get?: AnyFn; onSnapshot?: AnyFn } };\n Transaction?: { prototype: { get?: AnyFn; getAll?: AnyFn } };\n Firestore?: { prototype: { getAll?: AnyFn } };\n /** count() / sum() / average() — aggregation queries bill reads too. */\n AggregateQuery?: { prototype: { get?: AnyFn } };\n}\n\n/** `projects/{id}/…` → the project id, else undefined. Pure string op. */\nfunction projectFromPath(path: string): string | undefined {\n const parts = path.split(\"/\");\n const i = parts.indexOf(\"projects\");\n return i >= 0 && parts[i + 1] ? parts[i + 1] : undefined;\n}\n\n/**\n * Derive { collection, projectId } from the read target's path so an UNtagged read\n * cascades to `col:<collection>` instead of \"uncategorized\". PURE CPU; never reads,\n * never throws. Falls back to firebase-admin's internal `_queryOptions` for filtered\n * queries (which don't expose `.path`).\n */\nfunction hintFrom(target: any): CostHint | undefined {\n try {\n const p = typeof target?.path === \"string\" ? target.path : \"\";\n if (p) {\n const parts = p.split(\"/\").filter(Boolean);\n const collection = parts.length % 2 === 0 ? parts[parts.length - 2] : parts[parts.length - 1];\n return { collection, projectId: projectFromPath(p) };\n }\n const qo = target?._queryOptions;\n if (qo) {\n const collection = typeof qo.collectionId === \"string\" ? qo.collectionId : undefined;\n const parent =\n typeof qo.parentPath?.relativeName === \"string\"\n ? qo.parentPath.relativeName\n : typeof qo.parentPath?.toString === \"function\"\n ? String(qo.parentPath.toString())\n : \"\";\n return { collection, projectId: parent ? projectFromPath(parent) : undefined };\n }\n } catch {\n /* the meter must never throw */\n }\n return undefined;\n}\n\nfunction meterSnap(snap: unknown, hint?: CostHint): void {\n try {\n const size = (snap as { size?: number } | null)?.size;\n recordReads(typeof size === \"number\" ? size : 1, hint);\n } catch {\n /* best-effort */\n }\n}\nfunction meterCount(n: number, hint?: CostHint): void {\n try {\n recordReads(n, hint);\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * Install the universal read meter on the firebase-admin Firestore classes. Call\n * ONCE at process start, before any reads. Pass the namespace from\n * `firebase-admin/firestore` so the exact prototypes the SDK uses are patched:\n *\n * import * as Firestore from \"firebase-admin/firestore\";\n * installFirestoreMeter(Firestore);\n */\nexport function installFirestoreMeter(classes: FirestoreClasses): void {\n if (installed) return;\n installed = true;\n const { Query, DocumentReference, Transaction, Firestore, AggregateQuery } = classes;\n\n // Query.get — covers Query AND CollectionReference (shared prototype method).\n const qGet = Query?.prototype?.get;\n if (qGet) {\n Query!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await qGet.apply(this, args);\n meterSnap(snap, hintFrom(this));\n return snap;\n };\n }\n\n // DocumentReference.get — a single doc = 1 read.\n const dGet = DocumentReference?.prototype?.get;\n if (dGet) {\n DocumentReference!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await dGet.apply(this, args);\n meterCount(1, hintFrom(this));\n return snap;\n };\n }\n\n // Transaction.get — query or doc; size when present, else 1.\n const tGet = Transaction?.prototype?.get;\n if (tGet) {\n Transaction!.prototype.get = async function (this: unknown, ...args: any[]) {\n const res = await tGet.apply(this, args);\n meterSnap(res, hintFrom(args[0]));\n return res;\n };\n }\n\n // Transaction.getAll(...refs) — one read per ref.\n const tGetAll = Transaction?.prototype?.getAll;\n if (tGetAll) {\n Transaction!.prototype.getAll = async function (this: unknown, ...args: any[]) {\n const res = await tGetAll.apply(this, args);\n meterCount(Array.isArray(res) ? res.length : args.length || 1, hintFrom(args[0]));\n return res;\n };\n }\n\n // Firestore.getAll(...refs) — batched doc reads.\n const fGetAll = Firestore?.prototype?.getAll;\n if (fGetAll) {\n Firestore!.prototype.getAll = async function (this: unknown, ...args: any[]) {\n const res = await fGetAll.apply(this, args);\n meterCount(Array.isArray(res) ? res.length : args.length || 1, hintFrom(args[0]));\n return res;\n };\n }\n\n // AggregateQuery.get — count() / sum() / average(). Firestore bills one read per\n // up to 1000 index entries scanned, and the client never sees that entry count —\n // so this is an HONEST ESTIMATE: for count() we derive it from the result\n // (ceil(count / 1000)), else the billed minimum of 1. Observe-only: it reads the\n // result you already got, adds zero reads. (Closes the aggregation blind spot.)\n const aGet = AggregateQuery?.prototype?.get;\n if (aGet) {\n AggregateQuery!.prototype.get = async function (this: unknown, ...args: any[]) {\n const snap = await aGet.apply(this, args);\n try {\n const data = (snap as { data?: () => { count?: number } } | null)?.data?.();\n const count = typeof data?.count === \"number\" ? data.count : 0;\n meterCount(Math.max(1, Math.ceil(count / 1000)), hintFrom(this));\n } catch {\n /* best-effort */\n }\n return snap;\n };\n }\n\n // Query.onSnapshot / DocumentReference.onSnapshot — server-side realtime\n // listeners. We OBSERVE: wrap onNext and count the docs each fire delivers (a\n // query's changed docs — first fire = all matching; a doc = 1). We attach no\n // listener and issue no read; the meter just sees what the listener is already\n // billed. (Closes the server-listener blind spot.)\n const patchOnSnapshot = (proto: { onSnapshot?: AnyFn } | undefined, perDoc: boolean): void => {\n const real = proto?.onSnapshot;\n if (!real) return;\n proto!.onSnapshot = function (this: unknown, ...args: any[]) {\n const hint = hintFrom(this);\n const i = args.findIndex((a) => typeof a === \"function\");\n if (i >= 0) {\n const onNext = args[i];\n args[i] = function (snap: any) {\n try {\n const n = perDoc\n ? 1\n : typeof snap?.docChanges === \"function\"\n ? snap.docChanges().length\n : typeof snap?.size === \"number\"\n ? snap.size\n : 1;\n if (n > 0) meterCount(n, hint);\n } catch {\n /* best-effort */\n }\n return onNext(snap);\n };\n }\n return real.apply(this, args);\n };\n };\n patchOnSnapshot(Query?.prototype, false);\n patchOnSnapshot(DocumentReference?.prototype, true);\n}\n\n/** Test-only: reset the install guard so a suite can re-patch fresh prototypes. */\nexport function __resetFirestoreMeterForTests(): void {\n installed = false;\n}\n","/**\n * adapters/mongo — the MongoDB read meter (the trap), mirroring the Firestore one.\n *\n * THE RAW UNIT: like Firestore (a query returning N docs = N reads), MongoDB's raw\n * unit is DOCUMENTS READ — the documents each read operation returns, attributed to\n * the feature (bucket) that ran it. This is a real, countable number, NOT a dollar\n * bill. (MongoDB bills by cluster/compute, not per read — so this is the read LOAD by\n * feature: which queries pull the most documents, the thing you index/narrow/cache to\n * run a smaller cluster.) Raw counts only, no money — the two laws hold.\n *\n * THE FIX it brings: per-call-site instrumentation misses paths. Patch the driver's\n * result-returning read methods ONCE, and from install on every read — anywhere, any\n * code path — is counted under the ambient tag with no blind spots.\n *\n * MECHANISM: the wrappers run in the CALLER's own async context (a `find().toArray()`\n * inside `bucket(\"feed\")` resolves in that context), so attribution survives with zero\n * per-call-site work. Observe-only: it counts the result already in hand — it adds NO\n * query (no `explain()`, no profiler scan), so it can never become a read monster.\n *\n * SAFETY CONTRACT (it sits on your production read path): each wrapper calls the REAL\n * method first, counts in a try/catch that can never throw into the caller, and ALWAYS\n * returns the real result untouched. A wrong count is a measurement error, never a\n * correctness or availability one. Idempotent — calling it twice patches once.\n *\n * Pass the classes from your `mongodb` import (an OPTIONAL peer dep — installing this\n * never forces the driver on a Firestore user):\n *\n * import { FindCursor, AggregationCursor, Collection } from \"mongodb\";\n * import { installMongoMeter } from \"@cross-deck/buckets\";\n * installMongoMeter({ FindCursor, AggregationCursor, Collection });\n */\nimport { record, type CostHint } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/** MongoDB's raw read unit — documents returned by a read operation. A count. */\nexport const MONGO_READ_UNIT = \"mongo.docs_read\";\n\n/**\n * The `mongodb` driver classes to patch. Pass them from your `mongodb` import — only\n * the prototypes present are patched, so a driver-version mismatch degrades to\n * \"counts fewer paths\", never a crash.\n */\nexport interface MongoClasses {\n /** find() cursor — `.toArray()` resolves the matched documents. */\n FindCursor?: { prototype: { toArray?: AnyFn } };\n /** aggregate() cursor — `.toArray()` resolves the pipeline output documents. */\n AggregationCursor?: { prototype: { toArray?: AnyFn } };\n /** Collection — `.findOne()` resolves a single document (or null). */\n Collection?: { prototype: { findOne?: AnyFn } };\n}\n\n/** Best-effort `col:<collection>` cascade for an UNtagged read. PURE; never throws. */\nfunction hintFrom(target: any): CostHint | undefined {\n try {\n // FindCursor / AggregationCursor expose `.namespace` (MongoDBNamespace); a\n // Collection exposes `.collectionName`. Either gives us the collection.\n const ns = target?.namespace;\n if (ns && typeof ns.collection === \"string\" && ns.collection) {\n return { collection: ns.collection };\n }\n if (typeof target?.collectionName === \"string\" && target.collectionName) {\n return { collection: target.collectionName };\n }\n } catch {\n /* the meter must never throw */\n }\n return undefined;\n}\n\nfunction meter(n: number, hint?: CostHint): void {\n try {\n if (n > 0) record(MONGO_READ_UNIT, n, hint);\n } catch {\n /* best-effort — never disturb the caller */\n }\n}\n\n/**\n * Install the MongoDB read meter on the driver's read-result methods. Call ONCE at\n * process start, before any reads. Pass the classes from your `mongodb` import.\n */\nexport function installMongoMeter(classes: MongoClasses): void {\n if (installed) return;\n installed = true;\n const { FindCursor, AggregationCursor, Collection } = classes;\n\n // find().toArray() / aggregate().toArray() — the documents the query returned.\n const patchToArray = (proto: { toArray?: AnyFn } | undefined): void => {\n const real = proto?.toArray;\n if (!real) return;\n proto!.toArray = async function (this: unknown, ...args: any[]) {\n const out = await real.apply(this, args);\n meter(Array.isArray(out) ? out.length : 0, hintFrom(this));\n return out;\n };\n };\n patchToArray(FindCursor?.prototype);\n patchToArray(AggregationCursor?.prototype);\n\n // findOne() — one document (or null). A found doc is 1 read; null returned nothing.\n const realFindOne = Collection?.prototype?.findOne;\n if (realFindOne) {\n Collection!.prototype.findOne = async function (this: unknown, ...args: any[]) {\n const out = await realFindOne.apply(this, args);\n meter(out == null ? 0 : 1, hintFrom(this));\n return out;\n };\n }\n}\n\n/** Test-only: reset the install guard so a suite can re-patch fresh prototypes. */\nexport function __resetMongoMeterForTests(): void {\n installed = false;\n}\n","/**\n * adapters/postgres — the Postgres read meter, mirroring the Firestore and Mongo ones.\n * Covers node-postgres (`pg`), and therefore Supabase, Neon, Vercel Postgres, RDS, and\n * plain Postgres — they all speak the same wire protocol through the same driver.\n *\n * THE RAW UNIT: Postgres's raw read unit is ROWS READ — the rows a SELECT returns,\n * attributed to the feature (bucket) that ran it. This is a real, countable number,\n * NOT a dollar bill. (Supabase/Neon/RDS bill by COMPUTE — instance size × hours, not\n * per row — so this is the read LOAD by feature: which queries pull the most rows, the\n * thing you index/narrow/paginate/cache to run a smaller instance.) Raw counts only,\n * no money — the two laws hold.\n *\n * Sourced from the official docs (the playbook's load-bearing stage), not assumed:\n * - Supabase billing: charged purely on compute-hours, explicitly NOT per row/query\n * (https://supabase.com/docs/guides/platform/manage-your-usage/compute) — so the\n * honest unit is the data work, rows read, never a bill.\n * - node-postgres Result: `result.rows` and `result.rowCount` are ALREADY present on\n * the resolved result — reading them costs NO extra round-trip\n * (https://node-postgres.com/apis/result) — so the meter can never be a read monster.\n *\n * THE FIX it brings: per-call-site instrumentation misses paths. Patch the driver's\n * query method ONCE, and from install on every read — anywhere, any code path, through\n * a Pool or a Client — is counted under the ambient tag with no blind spots.\n *\n * MECHANISM: we patch `Client.prototype.query` only. `Pool.query` delegates to a\n * client's `query`, so one patch catches both pool and client usage with NO double\n * count. Attribution survives because the meter runs in the CALLER's async context\n * (a `pool.query()` inside `bucket(\"feed\")` is metered in that context) — for the\n * promise form via a synchronously-attached `.then`, and for the legacy callback form\n * via `AsyncResource.bind`. Observe-only: it counts the rows already in the result it\n * was handed — it adds NO query (no `EXPLAIN`, no `pg_stat_statements` scan), so it can\n * never become a read monster.\n *\n * SAFETY CONTRACT (it sits on your production read path): the wrapper calls the REAL\n * `query` first, counts in a try/catch that can never throw into the caller, and ALWAYS\n * returns the real result untouched. A wrong count is a measurement error, never a\n * correctness or availability one. Idempotent — calling it twice patches once.\n *\n * Pass the `Client` class from your `pg` import (an OPTIONAL peer dep — installing this\n * never forces the driver on a Firestore user):\n *\n * import { Client } from \"pg\";\n * import { installPgMeter, bucket } from \"@cross-deck/buckets\";\n * installPgMeter({ Client }); // once, at startup\n * await bucket(\"billing-page\", () => pool.query(\"SELECT ... \"));\n */\nimport { AsyncResource } from \"node:async_hooks\";\nimport { record } from \"../cost-meter\";\n\nlet installed = false;\n\n/* eslint-disable @typescript-eslint/no-explicit-any */\ntype AnyFn = (...args: any[]) => any;\n\n/** Postgres's raw read unit — rows returned by a SELECT. A count. */\nexport const PG_READ_UNIT = \"postgres.rows_read\";\n\n/**\n * The `pg` driver class to patch. Pass `Client` from your `pg` import; `Pool.query`\n * delegates to it, so this single patch covers pool usage too. Only the prototype\n * present is patched, so a driver-version mismatch degrades to \"counts nothing\",\n * never a crash.\n */\nexport interface PgClasses {\n /** node-postgres Client — `.query()` runs a statement and resolves a Result. */\n Client?: { prototype: { query?: AnyFn } };\n}\n\n/** A pg Result, minimally typed for what we read (already in hand — no round-trip). */\ninterface PgResultLike {\n /** The SQL command tag: \"SELECT\", \"INSERT\", … — tells a read from a write. */\n command?: string;\n /** The rows the statement returned (empty for a write with no RETURNING). */\n rows?: unknown[];\n}\n\n/**\n * Count the ROWS READ from a resolved Result — and ONLY for reads. node-postgres sets\n * `command` to the SQL command tag; a \"SELECT\"'s returned rows are the read load. A\n * write's RETURNING rows (command \"INSERT\"/\"UPDATE\"/\"DELETE\") are NOT reads and are\n * correctly excluded. PURE; never throws.\n */\nfunction meterResult(res: PgResultLike | undefined | null): void {\n try {\n if (res && res.command === \"SELECT\" && Array.isArray(res.rows) && res.rows.length > 0) {\n record(PG_READ_UNIT, res.rows.length);\n }\n } catch {\n /* the meter must never throw into the caller */\n }\n}\n\n/**\n * Install the Postgres read meter on `Client.prototype.query`. Call ONCE at process\n * start, before any reads. Pass the `Client` class from your `pg` import.\n */\nexport function installPgMeter(classes: PgClasses): void {\n if (installed) return;\n installed = true;\n\n const proto = classes.Client?.prototype;\n const real = proto?.query;\n if (!proto || !real) return;\n\n proto.query = function (this: unknown, ...args: any[]): any {\n // The last arg MAY be a Node-style callback (the legacy form). The promise form\n // passes no callback and the driver returns a Promise<Result>.\n const last = args.length > 0 ? args[args.length - 1] : undefined;\n\n if (typeof last === \"function\") {\n // Callback form. pg invokes the callback later, from a socket event whose async\n // context is NOT the caller's — so bind the meter to the caller's context now,\n // so attribution lands under the right bucket.\n const meterHere = AsyncResource.bind((res: PgResultLike) => meterResult(res));\n args[args.length - 1] = function (this: unknown, err: unknown, res: PgResultLike) {\n if (!err) {\n try {\n meterHere(res);\n } catch {\n /* never disturb the caller */\n }\n }\n return last.apply(this, arguments as unknown as any[]);\n };\n return real.apply(this, args);\n }\n\n const ret = real.apply(this, args);\n // Promise form: attach `.then` synchronously, here in the caller's async context,\n // so the meter runs with that context and attribution survives.\n if (ret && typeof ret.then === \"function\") {\n return ret.then((res: PgResultLike) => {\n meterResult(res);\n return res;\n });\n }\n // A submittable (a Query/Cursor object) or anything unexpected — leave untouched.\n return ret;\n };\n}\n\n/** Test-only: reset the install guard so a suite can re-patch a fresh prototype. */\nexport function __resetPgMeterForTests(): void {\n installed = false;\n}\n","/**\n * @cross-deck/buckets — know exactly what every database read costs you, and who\n * caused it. A tiny, never-throws collector for Firestore.\n *\n * The whole footprint a consumer sees:\n * 1. init({ apiKey, firestore }) — configure once, install the trap once\n * 2. bucket(name, fn) — name the read paths that matter\n * 3. (the dashboard shows the rest — and names the ones you haven't yet)\n */\nimport { configureMeter, type MeterConfig } from \"./cost-meter\";\nimport { ReportSink, NullSink, type Sink } from \"./sink\";\nimport { MirrorSink, DEFAULT_MIRROR_DIR } from \"./mirror\";\nimport { installFirestoreMeter, type FirestoreClasses } from \"./adapters/firestore\";\n\nexport interface InitOptions {\n /**\n * The project's `cd_sk_` SECRET key. Server-to-server only — never a browser key.\n * OPTIONAL: with no key, Buckets still meters locally and writes the readout to\n * disk (`.crossdeck/buckets.md`) — the free, no-account wedge. Add a key and it\n * also reports up to Crossdeck so the numbers surface on your dashboard.\n */\n apiKey?: string;\n /**\n * Pass the namespace from `firebase-admin/firestore` to auto-install the read\n * trap (recommended — this is what makes every read count with no per-call work).\n * Omit it if you'd rather call `installFirestoreMeter()` yourself, or you only\n * use the manual `recordReads()` recorders.\n */\n firestore?: FirestoreClasses;\n /** Override the report endpoint (defaults to Crossdeck's ingest). */\n endpoint?: string;\n /** How often to flush coalesced counts (ms). Default 60_000. */\n flushIntervalMs?: number;\n /** Bring your own sink (self-host the rollups). Defaults to reporting up to Crossdeck. */\n sink?: Sink;\n /**\n * Where to write the local readout — the file \"read me my buckets\" reads back.\n * Defaults to `.crossdeck`. Pass `false` to turn the local mirror off entirely.\n */\n mirror?: string | false;\n /** Notified when a flush fails, so a dropped window is never silent. Best-effort. */\n onError?: MeterConfig[\"onError\"];\n}\n\n/**\n * Configure Buckets once, at process start. Always meters locally and writes the\n * readout to disk; if you pass `apiKey` (or your own `sink`) it ALSO reports up to\n * Crossdeck. Pass `firestore` to install the universal read trap so every read counts\n * automatically.\n */\nexport function init(options: InitOptions = {}): void {\n // Upstream: your sink, else a Crossdeck reporter if a key was given, else nothing.\n const upstream: Sink | null =\n options.sink ?? (options.apiKey ? new ReportSink({ apiKey: options.apiKey, endpoint: options.endpoint }) : null);\n // Default behaviour tees every flush to a local readout; `mirror:false` opts out.\n const sink: Sink =\n options.mirror === false\n ? upstream ?? new NullSink()\n : new MirrorSink(upstream, typeof options.mirror === \"string\" ? options.mirror : DEFAULT_MIRROR_DIR);\n configureMeter({ sink, flushIntervalMs: options.flushIntervalMs, onError: options.onError });\n if (options.firestore) installFirestoreMeter(options.firestore);\n}\n\n/** Alias — reads well next to `bucket()` at a call site. */\nexport { init as initBuckets };\n\n// The headline verb + the lower-level tag controls it is sugar over.\nexport {\n bucket,\n runWithCostTag,\n enterCostTag,\n refineCostTag,\n currentCostTag,\n type CostTag,\n} from \"./cost-context\";\n\n// Recorders. `record(resource, quantity)` is the generic adapter primitive — count\n// any resource unit (a future adapter records \"clickhouse.query_ms\"); recordReads/\n// Writes/Deletes are the Firestore conveniences over it.\nexport {\n record,\n recordReads,\n recordWrites,\n recordDeletes,\n flush,\n type CostHint,\n type ResourceUnit,\n type OpType,\n type MeterConfig,\n} from \"./cost-meter\";\n\n// The datastore traps + their class shapes. Re-exported from THIS entry so they\n// share the meter's module-level state — a separate bundle would get its own meter\n// instance and silently drop the counts.\nexport { installFirestoreMeter, type FirestoreClasses } from \"./adapters/firestore\";\nexport { installMongoMeter, type MongoClasses, MONGO_READ_UNIT } from \"./mongo\";\nexport { installPgMeter, type PgClasses, PG_READ_UNIT } from \"./postgres\";\n\n// The sink seam — for self-hosting rollups instead of reporting to Crossdeck.\nexport {\n ReportSink,\n NullSink,\n type Sink,\n type BucketsReport,\n type ResourceCounts,\n type OpCounts,\n type ReportSinkConfig,\n} from \"./sink\";\n\n// The local readout — the file \"read me my buckets\" reads back, and its renderer.\nexport { MirrorSink, DEFAULT_MIRROR_DIR } from \"./mirror\";\nexport { renderReadout, READOUT_FOOTER } from \"./readout\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cross-deck/buckets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Know exactly what every database read costs you — and who caused it. A tiny, never-throws read-cost collector for Firestore, server AND browser.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Crossdeck",
|