@cross-deck/buckets 0.6.0 → 0.8.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 CHANGED
@@ -74,9 +74,24 @@ await bucket("pulse-map", async () => {
74
74
  });
75
75
  ```
76
76
 
77
- **3. That's it.** Open your Crossdeck dashboard and your operations are there
78
- `pulse-map`, every other bucket you named, and an `unknown` row for anything you
79
- haven't named yet. Buckets counted them at the database driver, not in your code.
77
+ **3. Read it back right where you code.** Buckets writes a live readout to
78
+ `.crossdeck/buckets.md` (biggest bucket first). Open it, or just ask your AI session
79
+ **"read me my buckets"** it's a plain file on disk, no dashboard required:
80
+
81
+ ```
82
+ # Buckets — reads on this surface
83
+ **32K reads** · 2026-06-19 (UTC)
84
+
85
+ | bucket | named | reads |
86
+ | ----------------- | :---: | ----: |
87
+ | headline-counters | ✓ | 31K |
88
+ | subscriptions | — | 1.1K |
89
+ ```
90
+
91
+ **No Crossdeck key needed for any of that.** `init()` with no `apiKey` meters locally
92
+ and writes the readout — the free, no-account wedge. Add a key and it *also* reports
93
+ up so the same numbers surface on your dashboard, with the drill-down, before/after,
94
+ and read-spike alerts. (Set `mirror: false` to turn the local file off.)
80
95
 
81
96
  ---
82
97
 
@@ -296,6 +311,20 @@ about. Two grains:
296
311
  - **Tag a bucket** (coarse) — a whole handler or job: `bucket("pulse-map", handler)`
297
312
  - **Tag a single read** (fine) — one query: `bucket("owner-lookup", () => db.doc(id).get())`
298
313
 
314
+ **Nest to drill down.** A `bucket()` inside another **composes into a path** —
315
+ the dashboard reads that path as a tree, so you tag the biggest bucket, see what's
316
+ under it, tag *that*, and the next-biggest surfaces. A tagged bucket also keeps the
317
+ collection it read as the leaf, so you never lose where the units actually went:
318
+
319
+ ```ts
320
+ await bucket("analytics", () =>
321
+ bucket("rollup", () => db.collection("events").where(/*…*/).get()));
322
+ // → "analytics > rollup > col:events"
323
+ ```
324
+
325
+ That waterfall — tag, drill, tag again — is how you walk a bill down from "where's
326
+ it all going?" to the exact line, one ship at a time.
327
+
299
328
  `unknown` is never a dead end. It's a to-do with a one-line fix — exactly like a
300
329
  custom analytics event you haven't named yet. You tag until you've found your
301
330
  source; Buckets just shows the names resolving in.
@@ -318,6 +347,31 @@ primitive that the whole ecosystem can build on — so we open-sourced it, and w
318
347
  publish the rollup schema so you can consume it with any tool you like, including
319
348
  your own. **The best place to read Buckets data should be earned, never locked.**
320
349
 
350
+ ### Free with the collector — and what Crossdeck adds
351
+
352
+ The collector is yours, free, forever: it meters every read on the surface you put
353
+ it on, never costs you reads to *run*, and you can point it at your own sink and read
354
+ the raw numbers yourself. That's a real tool on its own.
355
+
356
+ Two honest limits come with going it alone — and they're exactly what Crossdeck is for:
357
+
358
+ - **You see the surface you installed on.** Drop it in your server and you see server
359
+ reads — often the *minority*. Most apps read from the browser too (a separate
360
+ install), and the bill is the sum of both. **Crossdeck stitches server + browser +
361
+ every surface into one number**, so you stop reasoning from a slice.
362
+ - **Reading the numbers back yourself costs a few reads** — querying your own stored
363
+ rollups is still a read. **Crossdeck maintains the summary and serves it to you free,
364
+ live, any time** — the cost tool never costs you to look.
365
+
366
+ And because it's already wired in, Crossdeck turns the raw meter into the thing you
367
+ *act* on: the **drill-down** (tag → see the next-biggest → tag again, down to the
368
+ line), the **before/after fix verdict**, a **7-day baseline**, and **Slack alerts that
369
+ page you before the invoice**. The numbers are identical on both sides — same counts,
370
+ same source — Crossdeck just makes them whole, free to read, and impossible to miss.
371
+
372
+ Getting there is one step you were taking anyway: **onboard, install the SDK once, and
373
+ read-cost comes with it** — no second setup.
374
+
321
375
  ### Cost should page you like an exception — not surprise you like an invoice
322
376
 
323
377
  You already know what your code *should* do. You wrote the analytics pipeline; you
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as Sink } from './sink-DDQmzqm5.mjs';
2
- export { B as BucketsReport, O as OpCounts, R as ReportSink, a as ReportSinkConfig, b as ResourceCounts } from './sink-DDQmzqm5.mjs';
1
+ import { S as Sink, B as BucketsReport } from './sink-B-i4E1fY.mjs';
2
+ export { N as NullSink, O as OpCounts, R as ReportSink, a as ReportSinkConfig, b as ResourceCounts } from './sink-B-i4E1fY.mjs';
3
3
 
4
4
  /**
5
5
  * A resource unit — what an adapter counts. Firestore emits `read`/`write`/
@@ -102,15 +102,50 @@ declare function refineCostTag(patch: Partial<CostTag>): void;
102
102
  declare function currentCostTag(): CostTag;
103
103
  /**
104
104
  * `bucket(name, fn)` — the headline verb, the `track()` of cost. Run `fn` with
105
- * every operation inside it attributed to the bucket `name`; the attribution
106
- * rides the async subtree automatically. The one verb most developers ever touch:
105
+ * every operation inside it attributed to the bucket `name`; the attribution rides
106
+ * the async subtree automatically. NESTS: a `bucket()` inside another COMPOSES into
107
+ * a path (`"analytics" > "rollup"` → `"analytics>rollup"`), so the dashboard can
108
+ * drill from the coarse bucket down into its parts. The one verb most developers touch:
107
109
  *
108
- * await bucket("nightly-export", async () => {
109
- * const rows = await db.collection("events").where(...).get(); // → "nightly-export"
110
- * });
110
+ * await bucket("analytics", () =>
111
+ * bucket("rollup", () => db.collection("events").where(...).get())); // → "analytics>rollup>col:events"
111
112
  */
112
113
  declare function bucket<T>(name: string, fn: () => T): T;
113
114
 
115
+ declare const DEFAULT_MIRROR_DIR = ".crossdeck";
116
+ /**
117
+ * Wraps an optional upstream sink. On each flush it writes the running day-total
118
+ * locally, THEN (if an upstream sink was given — i.e. a key) reports onward.
119
+ * With no upstream it is a pure local meter: the wedge, working with no account.
120
+ */
121
+ declare class MirrorSink implements Sink {
122
+ private readonly upstream;
123
+ private readonly dir;
124
+ private acc;
125
+ private announced;
126
+ private seeded;
127
+ constructor(upstream: Sink | null, dir?: string);
128
+ private jsonPath;
129
+ /** Seed the running total once from an existing same-day file (survives restarts). */
130
+ private seed;
131
+ flush(report: BucketsReport): Promise<void>;
132
+ }
133
+
134
+ /**
135
+ * readout — renders the local file a developer (or their AI session) reads back with
136
+ * "read me my buckets". PURE string building: no I/O, no database reads. The node
137
+ * mirror (./mirror) writes this to `.crossdeck/buckets.md` on each flush, so the
138
+ * readout works offline, for free, with no account.
139
+ */
140
+
141
+ /**
142
+ * The one line that closes every readout. Plain and factual: what the OSS shows you
143
+ * here, and what signing up adds — for free. No invented numbers, no urgency, no pitch.
144
+ */
145
+ declare const READOUT_FOOTER: string;
146
+ /** Render the day's coalesced report as a human/AI-readable markdown readout. */
147
+ declare function renderReadout(report: BucketsReport): string;
148
+
114
149
  /**
115
150
  * @cross-deck/buckets — know exactly what every database read costs you, and who
116
151
  * caused it. A tiny, never-throws collector for Firestore.
@@ -122,8 +157,13 @@ declare function bucket<T>(name: string, fn: () => T): T;
122
157
  */
123
158
 
124
159
  interface InitOptions {
125
- /** The project's `cd_sk_` SECRET key. Server-to-server only — never a browser key. */
126
- apiKey: string;
160
+ /**
161
+ * The project's `cd_sk_` SECRET key. Server-to-server only — never a browser key.
162
+ * OPTIONAL: with no key, Buckets still meters locally and writes the readout to
163
+ * disk (`.crossdeck/buckets.md`) — the free, no-account wedge. Add a key and it
164
+ * also reports up to Crossdeck so the numbers surface on your dashboard.
165
+ */
166
+ apiKey?: string;
127
167
  /**
128
168
  * Pass the namespace from `firebase-admin/firestore` to auto-install the read
129
169
  * trap (recommended — this is what makes every read count with no per-call work).
@@ -137,14 +177,20 @@ interface InitOptions {
137
177
  flushIntervalMs?: number;
138
178
  /** Bring your own sink (self-host the rollups). Defaults to reporting up to Crossdeck. */
139
179
  sink?: Sink;
180
+ /**
181
+ * Where to write the local readout — the file "read me my buckets" reads back.
182
+ * Defaults to `.crossdeck`. Pass `false` to turn the local mirror off entirely.
183
+ */
184
+ mirror?: string | false;
140
185
  /** Notified when a flush fails, so a dropped window is never silent. Best-effort. */
141
186
  onError?: MeterConfig["onError"];
142
187
  }
143
188
  /**
144
- * Configure Buckets once, at process start. Points the meter at a sink (Crossdeck's
145
- * ingest by default) and — if you pass `firestore` installs the universal read
146
- * trap so every read counts automatically.
189
+ * Configure Buckets once, at process start. Always meters locally and writes the
190
+ * readout to disk; if you pass `apiKey` (or your own `sink`) it ALSO reports up to
191
+ * Crossdeck. Pass `firestore` to install the universal read trap so every read counts
192
+ * automatically.
147
193
  */
148
- declare function init(options: InitOptions): void;
194
+ declare function init(options?: InitOptions): void;
149
195
 
150
- export { type CostHint, type CostTag, type FirestoreClasses, type InitOptions, type MeterConfig, type OpType, type ResourceUnit, Sink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, runWithCostTag };
196
+ export { BucketsReport, type CostHint, type CostTag, DEFAULT_MIRROR_DIR, type FirestoreClasses, type InitOptions, type MeterConfig, MirrorSink, type OpType, READOUT_FOOTER, type ResourceUnit, Sink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, renderReadout, runWithCostTag };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as Sink } from './sink-DDQmzqm5.js';
2
- export { B as BucketsReport, O as OpCounts, R as ReportSink, a as ReportSinkConfig, b as ResourceCounts } from './sink-DDQmzqm5.js';
1
+ import { S as Sink, B as BucketsReport } from './sink-B-i4E1fY.js';
2
+ export { N as NullSink, O as OpCounts, R as ReportSink, a as ReportSinkConfig, b as ResourceCounts } from './sink-B-i4E1fY.js';
3
3
 
4
4
  /**
5
5
  * A resource unit — what an adapter counts. Firestore emits `read`/`write`/
@@ -102,15 +102,50 @@ declare function refineCostTag(patch: Partial<CostTag>): void;
102
102
  declare function currentCostTag(): CostTag;
103
103
  /**
104
104
  * `bucket(name, fn)` — the headline verb, the `track()` of cost. Run `fn` with
105
- * every operation inside it attributed to the bucket `name`; the attribution
106
- * rides the async subtree automatically. The one verb most developers ever touch:
105
+ * every operation inside it attributed to the bucket `name`; the attribution rides
106
+ * the async subtree automatically. NESTS: a `bucket()` inside another COMPOSES into
107
+ * a path (`"analytics" > "rollup"` → `"analytics>rollup"`), so the dashboard can
108
+ * drill from the coarse bucket down into its parts. The one verb most developers touch:
107
109
  *
108
- * await bucket("nightly-export", async () => {
109
- * const rows = await db.collection("events").where(...).get(); // → "nightly-export"
110
- * });
110
+ * await bucket("analytics", () =>
111
+ * bucket("rollup", () => db.collection("events").where(...).get())); // → "analytics>rollup>col:events"
111
112
  */
112
113
  declare function bucket<T>(name: string, fn: () => T): T;
113
114
 
115
+ declare const DEFAULT_MIRROR_DIR = ".crossdeck";
116
+ /**
117
+ * Wraps an optional upstream sink. On each flush it writes the running day-total
118
+ * locally, THEN (if an upstream sink was given — i.e. a key) reports onward.
119
+ * With no upstream it is a pure local meter: the wedge, working with no account.
120
+ */
121
+ declare class MirrorSink implements Sink {
122
+ private readonly upstream;
123
+ private readonly dir;
124
+ private acc;
125
+ private announced;
126
+ private seeded;
127
+ constructor(upstream: Sink | null, dir?: string);
128
+ private jsonPath;
129
+ /** Seed the running total once from an existing same-day file (survives restarts). */
130
+ private seed;
131
+ flush(report: BucketsReport): Promise<void>;
132
+ }
133
+
134
+ /**
135
+ * readout — renders the local file a developer (or their AI session) reads back with
136
+ * "read me my buckets". PURE string building: no I/O, no database reads. The node
137
+ * mirror (./mirror) writes this to `.crossdeck/buckets.md` on each flush, so the
138
+ * readout works offline, for free, with no account.
139
+ */
140
+
141
+ /**
142
+ * The one line that closes every readout. Plain and factual: what the OSS shows you
143
+ * here, and what signing up adds — for free. No invented numbers, no urgency, no pitch.
144
+ */
145
+ declare const READOUT_FOOTER: string;
146
+ /** Render the day's coalesced report as a human/AI-readable markdown readout. */
147
+ declare function renderReadout(report: BucketsReport): string;
148
+
114
149
  /**
115
150
  * @cross-deck/buckets — know exactly what every database read costs you, and who
116
151
  * caused it. A tiny, never-throws collector for Firestore.
@@ -122,8 +157,13 @@ declare function bucket<T>(name: string, fn: () => T): T;
122
157
  */
123
158
 
124
159
  interface InitOptions {
125
- /** The project's `cd_sk_` SECRET key. Server-to-server only — never a browser key. */
126
- apiKey: string;
160
+ /**
161
+ * The project's `cd_sk_` SECRET key. Server-to-server only — never a browser key.
162
+ * OPTIONAL: with no key, Buckets still meters locally and writes the readout to
163
+ * disk (`.crossdeck/buckets.md`) — the free, no-account wedge. Add a key and it
164
+ * also reports up to Crossdeck so the numbers surface on your dashboard.
165
+ */
166
+ apiKey?: string;
127
167
  /**
128
168
  * Pass the namespace from `firebase-admin/firestore` to auto-install the read
129
169
  * trap (recommended — this is what makes every read count with no per-call work).
@@ -137,14 +177,20 @@ interface InitOptions {
137
177
  flushIntervalMs?: number;
138
178
  /** Bring your own sink (self-host the rollups). Defaults to reporting up to Crossdeck. */
139
179
  sink?: Sink;
180
+ /**
181
+ * Where to write the local readout — the file "read me my buckets" reads back.
182
+ * Defaults to `.crossdeck`. Pass `false` to turn the local mirror off entirely.
183
+ */
184
+ mirror?: string | false;
140
185
  /** Notified when a flush fails, so a dropped window is never silent. Best-effort. */
141
186
  onError?: MeterConfig["onError"];
142
187
  }
143
188
  /**
144
- * Configure Buckets once, at process start. Points the meter at a sink (Crossdeck's
145
- * ingest by default) and — if you pass `firestore` installs the universal read
146
- * trap so every read counts automatically.
189
+ * Configure Buckets once, at process start. Always meters locally and writes the
190
+ * readout to disk; if you pass `apiKey` (or your own `sink`) it ALSO reports up to
191
+ * Crossdeck. Pass `firestore` to install the universal read trap so every read counts
192
+ * automatically.
147
193
  */
148
- declare function init(options: InitOptions): void;
194
+ declare function init(options?: InitOptions): void;
149
195
 
150
- export { type CostHint, type CostTag, type FirestoreClasses, type InitOptions, type MeterConfig, type OpType, type ResourceUnit, Sink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, runWithCostTag };
196
+ export { BucketsReport, type CostHint, type CostTag, DEFAULT_MIRROR_DIR, type FirestoreClasses, type InitOptions, type MeterConfig, MirrorSink, type OpType, READOUT_FOOTER, type ResourceUnit, Sink, bucket, currentCostTag, enterCostTag, flush, init, init as initBuckets, installFirestoreMeter, record, recordDeletes, recordReads, recordWrites, refineCostTag, renderReadout, runWithCostTag };
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var async_hooks = require('async_hooks');
4
+ var fs = require('fs');
5
+ var path = require('path');
4
6
 
5
7
  // src/cost-context.ts
6
8
  var DEFAULT_TAG = {};
@@ -18,8 +20,11 @@ function refineCostTag(patch) {
18
20
  function currentCostTag() {
19
21
  return store.getStore() ?? DEFAULT_TAG;
20
22
  }
23
+ var BUCKET_SEP = ">";
21
24
  function bucket(name, fn) {
22
- return runWithCostTag({ ...currentCostTag(), label: name }, fn);
25
+ const parent = currentCostTag().label;
26
+ const path = parent ? `${parent}${BUCKET_SEP}${name}` : name;
27
+ return runWithCostTag({ ...currentCostTag(), label: path }, fn);
23
28
  }
24
29
 
25
30
  // src/cost-meter.ts
@@ -58,7 +63,8 @@ function record(resource, quantity, hint) {
58
63
  if (!Number.isFinite(quantity) || quantity <= 0) return;
59
64
  const t = currentCostTag();
60
65
  const date = utcDate();
61
- const label = t.label || (hint?.collection ? `col:${hint.collection}` : "uncategorized");
66
+ const coll = hint?.collection ? `col:${hint.collection}` : null;
67
+ const label = t.label ? coll ? `${t.label}>${coll}` : t.label : coll ?? "uncategorized";
62
68
  const lk = date + SEP + resource + SEP + label;
63
69
  labelBuffer.set(lk, (labelBuffer.get(lk) ?? 0) + quantity);
64
70
  const hk = date + SEP + resource + SEP + utcHour();
@@ -157,6 +163,107 @@ var ReportSink = class {
157
163
  }
158
164
  }
159
165
  };
166
+ var NullSink = class {
167
+ async flush() {
168
+ }
169
+ };
170
+
171
+ // src/readout.ts
172
+ var READOUT_FOOTER = "Buckets OSS shows the reads on this surface. Sign up to Crossdeck (free) to see every surface in one view, drill any bucket down to the exact query, track a fix before and after, and get paged when reads spike \u2014 cross-deck.com";
173
+ function fmt(n) {
174
+ if (n >= 1e6) return (n / 1e6).toFixed(2) + "M";
175
+ if (n >= 1e4) return Math.round(n / 1e3) + "K";
176
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
177
+ return String(Math.round(n));
178
+ }
179
+ function isUntagged(label) {
180
+ const root = label.split(">")[0];
181
+ return root.startsWith("col:") || root === "uncategorized" || root === "unknown";
182
+ }
183
+ function displayLabel(label) {
184
+ return label.split(">").map((s) => s.startsWith("col:") ? s.slice(4) : s).join(" \u203A ");
185
+ }
186
+ function renderReadout(report) {
187
+ const entries = Object.entries(report.byLabel ?? {}).map(([label, counts]) => ({ label, reads: counts.read ?? 0 })).filter((e) => e.reads > 0).sort((a, b) => b.reads - a.reads);
188
+ const total = entries.reduce((s, e) => s + e.reads, 0);
189
+ const out = [];
190
+ out.push(`# Buckets \u2014 reads on this surface`);
191
+ out.push(``);
192
+ out.push(`**${fmt(total)} reads** \xB7 ${report.date} (UTC)`);
193
+ out.push(``);
194
+ if (entries.length === 0) {
195
+ out.push(`No reads metered yet \u2014 install the collector and let your app serve some traffic.`);
196
+ } else {
197
+ out.push(`| bucket | named | reads |`);
198
+ out.push(`| --- | :---: | ---: |`);
199
+ for (const e of entries) {
200
+ out.push(`| ${displayLabel(e.label)} | ${isUntagged(e.label) ? "\u2014" : "\u2713"} | ${fmt(e.reads)} |`);
201
+ }
202
+ }
203
+ out.push(``);
204
+ out.push(`---`);
205
+ out.push(READOUT_FOOTER);
206
+ out.push(``);
207
+ return out.join("\n");
208
+ }
209
+
210
+ // src/mirror.ts
211
+ var DEFAULT_MIRROR_DIR = ".crossdeck";
212
+ function mergeInto(target, src) {
213
+ if (!src) return;
214
+ for (const [key, counts] of Object.entries(src)) {
215
+ const bag = target[key] ??= {};
216
+ for (const [res, n] of Object.entries(counts)) {
217
+ if (typeof n === "number") bag[res] = (bag[res] ?? 0) + n;
218
+ }
219
+ }
220
+ }
221
+ var MirrorSink = class {
222
+ constructor(upstream, dir = DEFAULT_MIRROR_DIR) {
223
+ this.upstream = upstream;
224
+ this.dir = dir;
225
+ }
226
+ upstream;
227
+ dir;
228
+ acc = null;
229
+ announced = false;
230
+ seeded = false;
231
+ jsonPath() {
232
+ return path.join(this.dir, "buckets.json");
233
+ }
234
+ /** Seed the running total once from an existing same-day file (survives restarts). */
235
+ seed(date) {
236
+ if (this.seeded) return;
237
+ this.seeded = true;
238
+ try {
239
+ const prior = JSON.parse(fs.readFileSync(this.jsonPath(), "utf8"));
240
+ if (prior?.date === date && prior.byLabel) this.acc = prior;
241
+ } catch {
242
+ }
243
+ }
244
+ async flush(report) {
245
+ try {
246
+ this.seed(report.date);
247
+ if (!this.acc || this.acc.date !== report.date) {
248
+ this.acc = { date: report.date, byLabel: {}, byHour: {}, byMinute: {} };
249
+ }
250
+ mergeInto(this.acc.byLabel, report.byLabel);
251
+ mergeInto(this.acc.byHour ??= {}, report.byHour);
252
+ mergeInto(this.acc.byMinute ??= {}, report.byMinute);
253
+ fs.mkdirSync(this.dir, { recursive: true });
254
+ fs.writeFileSync(path.join(this.dir, "buckets.md"), renderReadout(this.acc));
255
+ fs.writeFileSync(this.jsonPath(), JSON.stringify(this.acc, null, 2));
256
+ if (!this.announced) {
257
+ this.announced = true;
258
+ console.log(
259
+ `Buckets: readout at ${path.join(this.dir, "buckets.md")} \u2014 open it, or ask your AI session to "read me my buckets".`
260
+ );
261
+ }
262
+ } catch {
263
+ }
264
+ if (this.upstream) await this.upstream.flush(report);
265
+ }
266
+ };
160
267
 
161
268
  // src/adapters/firestore.ts
162
269
  var installed = false;
@@ -278,12 +385,17 @@ function installFirestoreMeter(classes) {
278
385
  }
279
386
 
280
387
  // src/index.ts
281
- function init(options) {
282
- const sink2 = options.sink ?? new ReportSink({ apiKey: options.apiKey, endpoint: options.endpoint });
388
+ function init(options = {}) {
389
+ const upstream = options.sink ?? (options.apiKey ? new ReportSink({ apiKey: options.apiKey, endpoint: options.endpoint }) : null);
390
+ const sink2 = options.mirror === false ? upstream ?? new NullSink() : new MirrorSink(upstream, typeof options.mirror === "string" ? options.mirror : DEFAULT_MIRROR_DIR);
283
391
  configureMeter({ sink: sink2, flushIntervalMs: options.flushIntervalMs, onError: options.onError });
284
392
  if (options.firestore) installFirestoreMeter(options.firestore);
285
393
  }
286
394
 
395
+ exports.DEFAULT_MIRROR_DIR = DEFAULT_MIRROR_DIR;
396
+ exports.MirrorSink = MirrorSink;
397
+ exports.NullSink = NullSink;
398
+ exports.READOUT_FOOTER = READOUT_FOOTER;
287
399
  exports.ReportSink = ReportSink;
288
400
  exports.bucket = bucket;
289
401
  exports.currentCostTag = currentCostTag;
@@ -297,6 +409,7 @@ exports.recordDeletes = recordDeletes;
297
409
  exports.recordReads = recordReads;
298
410
  exports.recordWrites = recordWrites;
299
411
  exports.refineCostTag = refineCostTag;
412
+ exports.renderReadout = renderReadout;
300
413
  exports.runWithCostTag = runWithCostTag;
301
414
  //# sourceMappingURL=index.js.map
302
415
  //# sourceMappingURL=index.js.map