@drakkar.software/sunglasses-core 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/dist/index.d.mts +21 -77
- package/dist/index.d.ts +21 -77
- package/dist/index.js +50 -110
- package/dist/index.mjs +50 -110
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -11,7 +11,7 @@ interface IStorageAdapter {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Output destination that receives batches of sanitized, consented events.
|
|
14
|
-
* Implementations
|
|
14
|
+
* Implementations include `HttpStorageAdapter` and any custom adapter.
|
|
15
15
|
*/
|
|
16
16
|
interface IAnalyticsAdapter {
|
|
17
17
|
/**
|
|
@@ -27,7 +27,7 @@ interface IAnalyticsAdapter {
|
|
|
27
27
|
/**
|
|
28
28
|
* Called after a successful flush with the events that were delivered.
|
|
29
29
|
* Use this to archive or remove old events from the remote store.
|
|
30
|
-
* Implement
|
|
30
|
+
* Implement in adapters that accumulate data and need post-flush pruning.
|
|
31
31
|
*/
|
|
32
32
|
cleanupAfterFlush?(delivered: ReadonlyArray<SunglassesEvent>, config: CleanupConfig): Promise<void>;
|
|
33
33
|
}
|
|
@@ -366,54 +366,6 @@ interface HttpAdapterConfig {
|
|
|
366
366
|
/** Request timeout in ms. Default: 10_000. */
|
|
367
367
|
timeout?: number;
|
|
368
368
|
}
|
|
369
|
-
interface StarfishAdapterConfig {
|
|
370
|
-
/** Base URL of the Starfish sync server, e.g. https://sync.example.com */
|
|
371
|
-
serverUrl: string;
|
|
372
|
-
/**
|
|
373
|
-
* Path template for the event document.
|
|
374
|
-
* Use `{identity}` as a placeholder — it is replaced with `distinctId ?? anonymousId`.
|
|
375
|
-
* Example: "analytics/{identity}/events"
|
|
376
|
-
*/
|
|
377
|
-
storagePath: string;
|
|
378
|
-
/** Bearer token for Authorization header. */
|
|
379
|
-
authToken?: string;
|
|
380
|
-
/** Max retries on 409 Conflict (optimistic locking). Default: 3. */
|
|
381
|
-
maxRetries?: number;
|
|
382
|
-
/**
|
|
383
|
-
* When true, each successful push creates a **new** Starfish document using
|
|
384
|
-
* a rotating path suffix (e.g. `events-0001`, `events-0002`…).
|
|
385
|
-
*
|
|
386
|
-
* Benefits:
|
|
387
|
-
* - No pull step needed — each push is always a fresh document
|
|
388
|
-
* - No growing single document — each file stays small
|
|
389
|
-
* - Old documents accumulate on Starfish (combine with `cleanupAfterFlush` to prune)
|
|
390
|
-
*
|
|
391
|
-
* Requires `pathStorage` to persist the current path generation counter.
|
|
392
|
-
* Works best with `enableLocalArchive: true` in `SunglassesConfig` so the
|
|
393
|
-
* complete event history is kept locally even across many push generations.
|
|
394
|
-
*/
|
|
395
|
-
rotatePathOnSuccess?: boolean;
|
|
396
|
-
/**
|
|
397
|
-
* Storage adapter used to persist the current path generation counter.
|
|
398
|
-
* Required when `rotatePathOnSuccess: true`.
|
|
399
|
-
* Can be the same adapter as `SunglassesConfig.storage`.
|
|
400
|
-
*/
|
|
401
|
-
pathStorage?: IStorageAdapter;
|
|
402
|
-
/**
|
|
403
|
-
* When `true`, events are pushed directly without a prior pull.
|
|
404
|
-
* No merge, no optimistic locking, no conflict detection.
|
|
405
|
-
*
|
|
406
|
-
* Use this for Starfish collections configured with `queueOnly: true` —
|
|
407
|
-
* the server ignores `baseHash` and returns no stored data on pull,
|
|
408
|
-
* so a pull round-trip is always wasted.
|
|
409
|
-
*
|
|
410
|
-
* On push failure the adapter **throws**, allowing SunglassesCore to keep
|
|
411
|
-
* events in the local queue and retry on the next flush interval.
|
|
412
|
-
*
|
|
413
|
-
* Cannot be combined with `rotatePathOnSuccess`.
|
|
414
|
-
*/
|
|
415
|
-
pushOnly?: boolean;
|
|
416
|
-
}
|
|
417
369
|
/**
|
|
418
370
|
* In-memory + persisted session state.
|
|
419
371
|
* Session IDs are random UUIDs — they never contain PII.
|
|
@@ -667,15 +619,11 @@ declare class MiddlewarePipeline {
|
|
|
667
619
|
/**
|
|
668
620
|
* Persists per-event counts bucketed by time period (daily, weekly, monthly, all-time).
|
|
669
621
|
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
* where `bucket` is:
|
|
674
|
-
* - daily: "2024-01-15"
|
|
675
|
-
* - weekly: "2024-W03" (ISO week)
|
|
676
|
-
* - monthly: "2024-01"
|
|
677
|
-
* - all-time: "all"
|
|
622
|
+
* All counts are stored in a single `sg:counts` JSON blob, keyed internally by
|
|
623
|
+
* `{period}:{bucket}:{eventName}` within the object:
|
|
624
|
+
* `{ "daily:2024-01-15:click": 3, "weekly:2024-W03:click": 7, ... }`
|
|
678
625
|
*
|
|
626
|
+
* One storage key regardless of how many event types are tracked.
|
|
679
627
|
* Counts survive app restarts (persisted to IStorageAdapter).
|
|
680
628
|
* Designed to be written from the enqueue hot path — must not throw.
|
|
681
629
|
*/
|
|
@@ -683,17 +631,12 @@ declare class EventCounter implements IEventCounter {
|
|
|
683
631
|
private readonly storage;
|
|
684
632
|
private readonly logger;
|
|
685
633
|
/**
|
|
686
|
-
* In-memory cache of counts keyed by
|
|
687
|
-
* Updated synchronously on increment so getCount() can return immediately
|
|
688
|
-
* without waiting for storage writes to complete.
|
|
634
|
+
* In-memory cache of counts keyed by sub-key (`{period}:{bucket}:{eventName}`).
|
|
635
|
+
* Updated synchronously on increment so getCount() can return immediately.
|
|
689
636
|
*/
|
|
690
637
|
private readonly cache;
|
|
691
|
-
/**
|
|
692
|
-
|
|
693
|
-
* Used to reliably clear an event's timed buckets on reset(),
|
|
694
|
-
* even when they fall outside the 90-day sweep window.
|
|
695
|
-
*/
|
|
696
|
-
private readonly writtenKeys;
|
|
638
|
+
/** Whether the counts blob has been loaded from storage into cache. */
|
|
639
|
+
private loaded;
|
|
697
640
|
constructor(storage: IStorageAdapter, logger: Logger);
|
|
698
641
|
/**
|
|
699
642
|
* Increment the count for `eventName` across all four periods for `date`.
|
|
@@ -702,20 +645,21 @@ declare class EventCounter implements IEventCounter {
|
|
|
702
645
|
getCount(eventName: string, period: EventCountPeriod, date?: Date): Promise<number>;
|
|
703
646
|
/**
|
|
704
647
|
* Reset counts for a specific event (all periods), or all events if omitted.
|
|
705
|
-
*
|
|
706
|
-
* Note: resetting all events requires listing storage keys. This is a best-effort
|
|
707
|
-
* operation — adapters that don't support key enumeration will only clear
|
|
708
|
-
* the provided event name's keys.
|
|
709
648
|
*/
|
|
710
649
|
reset(eventName?: string): Promise<void>;
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
650
|
+
/**
|
|
651
|
+
* Load the counts blob from storage into the in-memory cache.
|
|
652
|
+
* Only runs once; subsequent calls are no-ops.
|
|
653
|
+
*/
|
|
654
|
+
private loadIfNeeded;
|
|
655
|
+
/** Serialize the full in-memory cache to `sg:counts`. */
|
|
656
|
+
private persist;
|
|
657
|
+
/** Build the sub-key within the counts blob for a given event, period, and date. */
|
|
658
|
+
private subKey;
|
|
715
659
|
/**
|
|
716
660
|
* Return the time-bucket string for a given period and date.
|
|
717
661
|
* - daily: "2024-01-15"
|
|
718
|
-
* - weekly: "2024-W03"
|
|
662
|
+
* - weekly: "2024-W03" (ISO week)
|
|
719
663
|
* - monthly: "2024-01"
|
|
720
664
|
* - all-time: "all"
|
|
721
665
|
*/
|
|
@@ -1173,4 +1117,4 @@ declare function sha256Hex(input: string): Promise<string>;
|
|
|
1173
1117
|
*/
|
|
1174
1118
|
declare function nowISO(): string;
|
|
1175
1119
|
|
|
1176
|
-
export { type CleanupConfig, type ConsentHistoryEntry, ConsentManager, type ConsentState, type ConsentStatus, type ErrorEventProperties, type EventContext, type EventCountPeriod, EventCounter, type EventMap, EventQueue, type EventType, FrequencyMiddleware, type FrequencyMiddlewareOptions, type HttpAdapterConfig, type IAnalyticsAdapter, type IEventCounter, type IMiddleware, type IStorageAdapter, type ISunglassesClient, type ISunglassesTypedClient, IdentityManager, type IdentityState, LocalEventArchive, type Logger, type MiddlewareNext, MiddlewarePipeline, PiiSanitizer, SamplingMiddleware, type SamplingMiddlewareOptions, type ScreenTrackingOptions, SessionManager, type SessionState, type
|
|
1120
|
+
export { type CleanupConfig, type ConsentHistoryEntry, ConsentManager, type ConsentState, type ConsentStatus, type ErrorEventProperties, type EventContext, type EventCountPeriod, EventCounter, type EventMap, EventQueue, type EventType, FrequencyMiddleware, type FrequencyMiddlewareOptions, type HttpAdapterConfig, type IAnalyticsAdapter, type IEventCounter, type IMiddleware, type IStorageAdapter, type ISunglassesClient, type ISunglassesTypedClient, IdentityManager, type IdentityState, LocalEventArchive, type Logger, type MiddlewareNext, MiddlewarePipeline, PiiSanitizer, SamplingMiddleware, type SamplingMiddlewareOptions, type ScreenTrackingOptions, SessionManager, type SessionState, type SunglassesConfig, SunglassesCore, type SunglassesEvent, TraitManager, type UserDataExport, asTyped, createLazyClient, createLogger, generateUUID, nowISO, sha256Hex };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ interface IStorageAdapter {
|
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
13
|
* Output destination that receives batches of sanitized, consented events.
|
|
14
|
-
* Implementations
|
|
14
|
+
* Implementations include `HttpStorageAdapter` and any custom adapter.
|
|
15
15
|
*/
|
|
16
16
|
interface IAnalyticsAdapter {
|
|
17
17
|
/**
|
|
@@ -27,7 +27,7 @@ interface IAnalyticsAdapter {
|
|
|
27
27
|
/**
|
|
28
28
|
* Called after a successful flush with the events that were delivered.
|
|
29
29
|
* Use this to archive or remove old events from the remote store.
|
|
30
|
-
* Implement
|
|
30
|
+
* Implement in adapters that accumulate data and need post-flush pruning.
|
|
31
31
|
*/
|
|
32
32
|
cleanupAfterFlush?(delivered: ReadonlyArray<SunglassesEvent>, config: CleanupConfig): Promise<void>;
|
|
33
33
|
}
|
|
@@ -366,54 +366,6 @@ interface HttpAdapterConfig {
|
|
|
366
366
|
/** Request timeout in ms. Default: 10_000. */
|
|
367
367
|
timeout?: number;
|
|
368
368
|
}
|
|
369
|
-
interface StarfishAdapterConfig {
|
|
370
|
-
/** Base URL of the Starfish sync server, e.g. https://sync.example.com */
|
|
371
|
-
serverUrl: string;
|
|
372
|
-
/**
|
|
373
|
-
* Path template for the event document.
|
|
374
|
-
* Use `{identity}` as a placeholder — it is replaced with `distinctId ?? anonymousId`.
|
|
375
|
-
* Example: "analytics/{identity}/events"
|
|
376
|
-
*/
|
|
377
|
-
storagePath: string;
|
|
378
|
-
/** Bearer token for Authorization header. */
|
|
379
|
-
authToken?: string;
|
|
380
|
-
/** Max retries on 409 Conflict (optimistic locking). Default: 3. */
|
|
381
|
-
maxRetries?: number;
|
|
382
|
-
/**
|
|
383
|
-
* When true, each successful push creates a **new** Starfish document using
|
|
384
|
-
* a rotating path suffix (e.g. `events-0001`, `events-0002`…).
|
|
385
|
-
*
|
|
386
|
-
* Benefits:
|
|
387
|
-
* - No pull step needed — each push is always a fresh document
|
|
388
|
-
* - No growing single document — each file stays small
|
|
389
|
-
* - Old documents accumulate on Starfish (combine with `cleanupAfterFlush` to prune)
|
|
390
|
-
*
|
|
391
|
-
* Requires `pathStorage` to persist the current path generation counter.
|
|
392
|
-
* Works best with `enableLocalArchive: true` in `SunglassesConfig` so the
|
|
393
|
-
* complete event history is kept locally even across many push generations.
|
|
394
|
-
*/
|
|
395
|
-
rotatePathOnSuccess?: boolean;
|
|
396
|
-
/**
|
|
397
|
-
* Storage adapter used to persist the current path generation counter.
|
|
398
|
-
* Required when `rotatePathOnSuccess: true`.
|
|
399
|
-
* Can be the same adapter as `SunglassesConfig.storage`.
|
|
400
|
-
*/
|
|
401
|
-
pathStorage?: IStorageAdapter;
|
|
402
|
-
/**
|
|
403
|
-
* When `true`, events are pushed directly without a prior pull.
|
|
404
|
-
* No merge, no optimistic locking, no conflict detection.
|
|
405
|
-
*
|
|
406
|
-
* Use this for Starfish collections configured with `queueOnly: true` —
|
|
407
|
-
* the server ignores `baseHash` and returns no stored data on pull,
|
|
408
|
-
* so a pull round-trip is always wasted.
|
|
409
|
-
*
|
|
410
|
-
* On push failure the adapter **throws**, allowing SunglassesCore to keep
|
|
411
|
-
* events in the local queue and retry on the next flush interval.
|
|
412
|
-
*
|
|
413
|
-
* Cannot be combined with `rotatePathOnSuccess`.
|
|
414
|
-
*/
|
|
415
|
-
pushOnly?: boolean;
|
|
416
|
-
}
|
|
417
369
|
/**
|
|
418
370
|
* In-memory + persisted session state.
|
|
419
371
|
* Session IDs are random UUIDs — they never contain PII.
|
|
@@ -667,15 +619,11 @@ declare class MiddlewarePipeline {
|
|
|
667
619
|
/**
|
|
668
620
|
* Persists per-event counts bucketed by time period (daily, weekly, monthly, all-time).
|
|
669
621
|
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
* where `bucket` is:
|
|
674
|
-
* - daily: "2024-01-15"
|
|
675
|
-
* - weekly: "2024-W03" (ISO week)
|
|
676
|
-
* - monthly: "2024-01"
|
|
677
|
-
* - all-time: "all"
|
|
622
|
+
* All counts are stored in a single `sg:counts` JSON blob, keyed internally by
|
|
623
|
+
* `{period}:{bucket}:{eventName}` within the object:
|
|
624
|
+
* `{ "daily:2024-01-15:click": 3, "weekly:2024-W03:click": 7, ... }`
|
|
678
625
|
*
|
|
626
|
+
* One storage key regardless of how many event types are tracked.
|
|
679
627
|
* Counts survive app restarts (persisted to IStorageAdapter).
|
|
680
628
|
* Designed to be written from the enqueue hot path — must not throw.
|
|
681
629
|
*/
|
|
@@ -683,17 +631,12 @@ declare class EventCounter implements IEventCounter {
|
|
|
683
631
|
private readonly storage;
|
|
684
632
|
private readonly logger;
|
|
685
633
|
/**
|
|
686
|
-
* In-memory cache of counts keyed by
|
|
687
|
-
* Updated synchronously on increment so getCount() can return immediately
|
|
688
|
-
* without waiting for storage writes to complete.
|
|
634
|
+
* In-memory cache of counts keyed by sub-key (`{period}:{bucket}:{eventName}`).
|
|
635
|
+
* Updated synchronously on increment so getCount() can return immediately.
|
|
689
636
|
*/
|
|
690
637
|
private readonly cache;
|
|
691
|
-
/**
|
|
692
|
-
|
|
693
|
-
* Used to reliably clear an event's timed buckets on reset(),
|
|
694
|
-
* even when they fall outside the 90-day sweep window.
|
|
695
|
-
*/
|
|
696
|
-
private readonly writtenKeys;
|
|
638
|
+
/** Whether the counts blob has been loaded from storage into cache. */
|
|
639
|
+
private loaded;
|
|
697
640
|
constructor(storage: IStorageAdapter, logger: Logger);
|
|
698
641
|
/**
|
|
699
642
|
* Increment the count for `eventName` across all four periods for `date`.
|
|
@@ -702,20 +645,21 @@ declare class EventCounter implements IEventCounter {
|
|
|
702
645
|
getCount(eventName: string, period: EventCountPeriod, date?: Date): Promise<number>;
|
|
703
646
|
/**
|
|
704
647
|
* Reset counts for a specific event (all periods), or all events if omitted.
|
|
705
|
-
*
|
|
706
|
-
* Note: resetting all events requires listing storage keys. This is a best-effort
|
|
707
|
-
* operation — adapters that don't support key enumeration will only clear
|
|
708
|
-
* the provided event name's keys.
|
|
709
648
|
*/
|
|
710
649
|
reset(eventName?: string): Promise<void>;
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
650
|
+
/**
|
|
651
|
+
* Load the counts blob from storage into the in-memory cache.
|
|
652
|
+
* Only runs once; subsequent calls are no-ops.
|
|
653
|
+
*/
|
|
654
|
+
private loadIfNeeded;
|
|
655
|
+
/** Serialize the full in-memory cache to `sg:counts`. */
|
|
656
|
+
private persist;
|
|
657
|
+
/** Build the sub-key within the counts blob for a given event, period, and date. */
|
|
658
|
+
private subKey;
|
|
715
659
|
/**
|
|
716
660
|
* Return the time-bucket string for a given period and date.
|
|
717
661
|
* - daily: "2024-01-15"
|
|
718
|
-
* - weekly: "2024-W03"
|
|
662
|
+
* - weekly: "2024-W03" (ISO week)
|
|
719
663
|
* - monthly: "2024-01"
|
|
720
664
|
* - all-time: "all"
|
|
721
665
|
*/
|
|
@@ -1173,4 +1117,4 @@ declare function sha256Hex(input: string): Promise<string>;
|
|
|
1173
1117
|
*/
|
|
1174
1118
|
declare function nowISO(): string;
|
|
1175
1119
|
|
|
1176
|
-
export { type CleanupConfig, type ConsentHistoryEntry, ConsentManager, type ConsentState, type ConsentStatus, type ErrorEventProperties, type EventContext, type EventCountPeriod, EventCounter, type EventMap, EventQueue, type EventType, FrequencyMiddleware, type FrequencyMiddlewareOptions, type HttpAdapterConfig, type IAnalyticsAdapter, type IEventCounter, type IMiddleware, type IStorageAdapter, type ISunglassesClient, type ISunglassesTypedClient, IdentityManager, type IdentityState, LocalEventArchive, type Logger, type MiddlewareNext, MiddlewarePipeline, PiiSanitizer, SamplingMiddleware, type SamplingMiddlewareOptions, type ScreenTrackingOptions, SessionManager, type SessionState, type
|
|
1120
|
+
export { type CleanupConfig, type ConsentHistoryEntry, ConsentManager, type ConsentState, type ConsentStatus, type ErrorEventProperties, type EventContext, type EventCountPeriod, EventCounter, type EventMap, EventQueue, type EventType, FrequencyMiddleware, type FrequencyMiddlewareOptions, type HttpAdapterConfig, type IAnalyticsAdapter, type IEventCounter, type IMiddleware, type IStorageAdapter, type ISunglassesClient, type ISunglassesTypedClient, IdentityManager, type IdentityState, LocalEventArchive, type Logger, type MiddlewareNext, MiddlewarePipeline, PiiSanitizer, SamplingMiddleware, type SamplingMiddlewareOptions, type ScreenTrackingOptions, SessionManager, type SessionState, type SunglassesConfig, SunglassesCore, type SunglassesEvent, TraitManager, type UserDataExport, asTyped, createLazyClient, createLogger, generateUUID, nowISO, sha256Hex };
|
package/dist/index.js
CHANGED
|
@@ -205,150 +205,91 @@ var ConsentManager = class {
|
|
|
205
205
|
};
|
|
206
206
|
|
|
207
207
|
// src/EventCounter.ts
|
|
208
|
-
var
|
|
208
|
+
var COUNTS_KEY = "sg:counts";
|
|
209
209
|
var EventCounter = class {
|
|
210
210
|
constructor(storage, logger) {
|
|
211
211
|
this.storage = storage;
|
|
212
212
|
this.logger = logger;
|
|
213
213
|
/**
|
|
214
|
-
* In-memory cache of counts keyed by
|
|
215
|
-
* Updated synchronously on increment so getCount() can return immediately
|
|
216
|
-
* without waiting for storage writes to complete.
|
|
214
|
+
* In-memory cache of counts keyed by sub-key (`{period}:{bucket}:{eventName}`).
|
|
215
|
+
* Updated synchronously on increment so getCount() can return immediately.
|
|
217
216
|
*/
|
|
218
217
|
this.cache = /* @__PURE__ */ new Map();
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
* Used to reliably clear an event's timed buckets on reset(),
|
|
222
|
-
* even when they fall outside the 90-day sweep window.
|
|
223
|
-
*/
|
|
224
|
-
this.writtenKeys = /* @__PURE__ */ new Set();
|
|
218
|
+
/** Whether the counts blob has been loaded from storage into cache. */
|
|
219
|
+
this.loaded = false;
|
|
225
220
|
}
|
|
226
221
|
/**
|
|
227
222
|
* Increment the count for `eventName` across all four periods for `date`.
|
|
228
223
|
*/
|
|
229
224
|
async increment(eventName, date = /* @__PURE__ */ new Date()) {
|
|
225
|
+
await this.loadIfNeeded();
|
|
230
226
|
const periods = ["daily", "weekly", "monthly", "all-time"];
|
|
231
|
-
|
|
227
|
+
for (const period of periods) {
|
|
228
|
+
const key = this.subKey(eventName, period, date);
|
|
229
|
+
this.cache.set(key, (this.cache.get(key) ?? 0) + 1);
|
|
230
|
+
}
|
|
231
|
+
await this.persist();
|
|
232
232
|
}
|
|
233
233
|
async getCount(eventName, period, date = /* @__PURE__ */ new Date()) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (cached !== void 0) return cached;
|
|
237
|
-
try {
|
|
238
|
-
const raw = await this.storage.read(key);
|
|
239
|
-
if (raw === null) return 0;
|
|
240
|
-
const n = parseInt(raw, 10);
|
|
241
|
-
const count = Number.isNaN(n) ? 0 : n;
|
|
242
|
-
this.cache.set(key, count);
|
|
243
|
-
return count;
|
|
244
|
-
} catch (err) {
|
|
245
|
-
this.logger.warn("EventCounter: failed to read count", err);
|
|
246
|
-
return 0;
|
|
247
|
-
}
|
|
234
|
+
await this.loadIfNeeded();
|
|
235
|
+
return this.cache.get(this.subKey(eventName, period, date)) ?? 0;
|
|
248
236
|
}
|
|
249
237
|
/**
|
|
250
238
|
* Reset counts for a specific event (all periods), or all events if omitted.
|
|
251
|
-
*
|
|
252
|
-
* Note: resetting all events requires listing storage keys. This is a best-effort
|
|
253
|
-
* operation — adapters that don't support key enumeration will only clear
|
|
254
|
-
* the provided event name's keys.
|
|
255
239
|
*/
|
|
256
240
|
async reset(eventName) {
|
|
241
|
+
await this.loadIfNeeded();
|
|
257
242
|
if (eventName) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
await this.storage.delete(key);
|
|
264
|
-
} catch {
|
|
265
|
-
}
|
|
266
|
-
this.writtenKeys.delete(key);
|
|
267
|
-
this.cache.delete(key);
|
|
268
|
-
}
|
|
269
|
-
const now = /* @__PURE__ */ new Date();
|
|
270
|
-
const seenKeys = new Set(allKeys);
|
|
271
|
-
const periods = ["daily", "weekly", "monthly", "all-time"];
|
|
272
|
-
const eventNames = new Set(
|
|
273
|
-
allKeys.map((k) => k.split(":").pop()).filter(Boolean)
|
|
274
|
-
);
|
|
275
|
-
for (let i = 0; i < 90; i++) {
|
|
276
|
-
const d = new Date(now);
|
|
277
|
-
d.setDate(d.getDate() - i);
|
|
278
|
-
for (const name of eventNames) {
|
|
279
|
-
for (const period of periods) {
|
|
280
|
-
const key = this.storageKey(name, period, d);
|
|
281
|
-
if (!seenKeys.has(key)) {
|
|
282
|
-
seenKeys.add(key);
|
|
283
|
-
try {
|
|
284
|
-
await this.storage.delete(key);
|
|
285
|
-
} catch {
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
243
|
+
const safeEvent = eventName.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
244
|
+
for (const key of this.cache.keys()) {
|
|
245
|
+
if (key.endsWith(`:${safeEvent}`)) {
|
|
246
|
+
this.cache.delete(key);
|
|
289
247
|
}
|
|
290
248
|
}
|
|
291
|
-
|
|
249
|
+
} else {
|
|
250
|
+
this.cache.clear();
|
|
292
251
|
}
|
|
252
|
+
await this.persist();
|
|
253
|
+
this.logger.debug("EventCounter.reset(): cleared event counters");
|
|
293
254
|
}
|
|
294
255
|
// ── Private ──────────────────────────────────────────────────────────────
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
this.
|
|
256
|
+
/**
|
|
257
|
+
* Load the counts blob from storage into the in-memory cache.
|
|
258
|
+
* Only runs once; subsequent calls are no-ops.
|
|
259
|
+
*/
|
|
260
|
+
async loadIfNeeded() {
|
|
261
|
+
if (this.loaded) return;
|
|
262
|
+
this.loaded = true;
|
|
301
263
|
try {
|
|
302
|
-
await this.storage.
|
|
264
|
+
const raw = await this.storage.read(COUNTS_KEY);
|
|
265
|
+
if (!raw) return;
|
|
266
|
+
const parsed = JSON.parse(raw);
|
|
267
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
268
|
+
if (typeof v === "number") this.cache.set(k, v);
|
|
269
|
+
}
|
|
303
270
|
} catch (err) {
|
|
304
|
-
this.logger.warn("EventCounter: failed to
|
|
271
|
+
this.logger.warn("EventCounter: failed to load counts blob", err);
|
|
305
272
|
}
|
|
306
273
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
);
|
|
312
|
-
for (const key of sessionKeys) {
|
|
313
|
-
try {
|
|
314
|
-
await this.storage.delete(key);
|
|
315
|
-
} catch {
|
|
316
|
-
}
|
|
317
|
-
this.writtenKeys.delete(key);
|
|
318
|
-
this.cache.delete(key);
|
|
319
|
-
}
|
|
320
|
-
const now = /* @__PURE__ */ new Date();
|
|
321
|
-
const seenBuckets = new Set(sessionKeys);
|
|
322
|
-
for (let i = 0; i < 90; i++) {
|
|
323
|
-
const d = new Date(now);
|
|
324
|
-
d.setDate(d.getDate() - i);
|
|
325
|
-
const periods = ["daily", "weekly", "monthly"];
|
|
326
|
-
for (const period of periods) {
|
|
327
|
-
const key = this.storageKey(eventName, period, d);
|
|
328
|
-
if (!seenBuckets.has(key)) {
|
|
329
|
-
seenBuckets.add(key);
|
|
330
|
-
try {
|
|
331
|
-
await this.storage.delete(key);
|
|
332
|
-
} catch {
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
274
|
+
/** Serialize the full in-memory cache to `sg:counts`. */
|
|
275
|
+
async persist() {
|
|
276
|
+
const obj = {};
|
|
277
|
+
for (const [k, v] of this.cache) obj[k] = v;
|
|
337
278
|
try {
|
|
338
|
-
await this.storage.
|
|
339
|
-
} catch {
|
|
279
|
+
await this.storage.write(COUNTS_KEY, JSON.stringify(obj));
|
|
280
|
+
} catch (err) {
|
|
281
|
+
this.logger.warn("EventCounter: failed to persist counts blob", err);
|
|
340
282
|
}
|
|
341
283
|
}
|
|
342
|
-
/** Build the
|
|
343
|
-
|
|
344
|
-
const bucket = this.bucketFor(period, date);
|
|
284
|
+
/** Build the sub-key within the counts blob for a given event, period, and date. */
|
|
285
|
+
subKey(eventName, period, date) {
|
|
345
286
|
const safeEvent = eventName.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
346
|
-
return `${
|
|
287
|
+
return `${period}:${this.bucketFor(period, date)}:${safeEvent}`;
|
|
347
288
|
}
|
|
348
289
|
/**
|
|
349
290
|
* Return the time-bucket string for a given period and date.
|
|
350
291
|
* - daily: "2024-01-15"
|
|
351
|
-
* - weekly: "2024-W03"
|
|
292
|
+
* - weekly: "2024-W03" (ISO week)
|
|
352
293
|
* - monthly: "2024-01"
|
|
353
294
|
* - all-time: "all"
|
|
354
295
|
*/
|
|
@@ -357,10 +298,9 @@ var EventCounter = class {
|
|
|
357
298
|
case "daily":
|
|
358
299
|
return date.toISOString().slice(0, 10);
|
|
359
300
|
// "YYYY-MM-DD"
|
|
360
|
-
case "weekly":
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
301
|
+
case "weekly":
|
|
302
|
+
return toISOWeek(date);
|
|
303
|
+
// "YYYY-Www"
|
|
364
304
|
case "monthly":
|
|
365
305
|
return date.toISOString().slice(0, 7);
|
|
366
306
|
// "YYYY-MM"
|
package/dist/index.mjs
CHANGED
|
@@ -162,150 +162,91 @@ var ConsentManager = class {
|
|
|
162
162
|
};
|
|
163
163
|
|
|
164
164
|
// src/EventCounter.ts
|
|
165
|
-
var
|
|
165
|
+
var COUNTS_KEY = "sg:counts";
|
|
166
166
|
var EventCounter = class {
|
|
167
167
|
constructor(storage, logger) {
|
|
168
168
|
this.storage = storage;
|
|
169
169
|
this.logger = logger;
|
|
170
170
|
/**
|
|
171
|
-
* In-memory cache of counts keyed by
|
|
172
|
-
* Updated synchronously on increment so getCount() can return immediately
|
|
173
|
-
* without waiting for storage writes to complete.
|
|
171
|
+
* In-memory cache of counts keyed by sub-key (`{period}:{bucket}:{eventName}`).
|
|
172
|
+
* Updated synchronously on increment so getCount() can return immediately.
|
|
174
173
|
*/
|
|
175
174
|
this.cache = /* @__PURE__ */ new Map();
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
* Used to reliably clear an event's timed buckets on reset(),
|
|
179
|
-
* even when they fall outside the 90-day sweep window.
|
|
180
|
-
*/
|
|
181
|
-
this.writtenKeys = /* @__PURE__ */ new Set();
|
|
175
|
+
/** Whether the counts blob has been loaded from storage into cache. */
|
|
176
|
+
this.loaded = false;
|
|
182
177
|
}
|
|
183
178
|
/**
|
|
184
179
|
* Increment the count for `eventName` across all four periods for `date`.
|
|
185
180
|
*/
|
|
186
181
|
async increment(eventName, date = /* @__PURE__ */ new Date()) {
|
|
182
|
+
await this.loadIfNeeded();
|
|
187
183
|
const periods = ["daily", "weekly", "monthly", "all-time"];
|
|
188
|
-
|
|
184
|
+
for (const period of periods) {
|
|
185
|
+
const key = this.subKey(eventName, period, date);
|
|
186
|
+
this.cache.set(key, (this.cache.get(key) ?? 0) + 1);
|
|
187
|
+
}
|
|
188
|
+
await this.persist();
|
|
189
189
|
}
|
|
190
190
|
async getCount(eventName, period, date = /* @__PURE__ */ new Date()) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (cached !== void 0) return cached;
|
|
194
|
-
try {
|
|
195
|
-
const raw = await this.storage.read(key);
|
|
196
|
-
if (raw === null) return 0;
|
|
197
|
-
const n = parseInt(raw, 10);
|
|
198
|
-
const count = Number.isNaN(n) ? 0 : n;
|
|
199
|
-
this.cache.set(key, count);
|
|
200
|
-
return count;
|
|
201
|
-
} catch (err) {
|
|
202
|
-
this.logger.warn("EventCounter: failed to read count", err);
|
|
203
|
-
return 0;
|
|
204
|
-
}
|
|
191
|
+
await this.loadIfNeeded();
|
|
192
|
+
return this.cache.get(this.subKey(eventName, period, date)) ?? 0;
|
|
205
193
|
}
|
|
206
194
|
/**
|
|
207
195
|
* Reset counts for a specific event (all periods), or all events if omitted.
|
|
208
|
-
*
|
|
209
|
-
* Note: resetting all events requires listing storage keys. This is a best-effort
|
|
210
|
-
* operation — adapters that don't support key enumeration will only clear
|
|
211
|
-
* the provided event name's keys.
|
|
212
196
|
*/
|
|
213
197
|
async reset(eventName) {
|
|
198
|
+
await this.loadIfNeeded();
|
|
214
199
|
if (eventName) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
await this.storage.delete(key);
|
|
221
|
-
} catch {
|
|
222
|
-
}
|
|
223
|
-
this.writtenKeys.delete(key);
|
|
224
|
-
this.cache.delete(key);
|
|
225
|
-
}
|
|
226
|
-
const now = /* @__PURE__ */ new Date();
|
|
227
|
-
const seenKeys = new Set(allKeys);
|
|
228
|
-
const periods = ["daily", "weekly", "monthly", "all-time"];
|
|
229
|
-
const eventNames = new Set(
|
|
230
|
-
allKeys.map((k) => k.split(":").pop()).filter(Boolean)
|
|
231
|
-
);
|
|
232
|
-
for (let i = 0; i < 90; i++) {
|
|
233
|
-
const d = new Date(now);
|
|
234
|
-
d.setDate(d.getDate() - i);
|
|
235
|
-
for (const name of eventNames) {
|
|
236
|
-
for (const period of periods) {
|
|
237
|
-
const key = this.storageKey(name, period, d);
|
|
238
|
-
if (!seenKeys.has(key)) {
|
|
239
|
-
seenKeys.add(key);
|
|
240
|
-
try {
|
|
241
|
-
await this.storage.delete(key);
|
|
242
|
-
} catch {
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
200
|
+
const safeEvent = eventName.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
201
|
+
for (const key of this.cache.keys()) {
|
|
202
|
+
if (key.endsWith(`:${safeEvent}`)) {
|
|
203
|
+
this.cache.delete(key);
|
|
246
204
|
}
|
|
247
205
|
}
|
|
248
|
-
|
|
206
|
+
} else {
|
|
207
|
+
this.cache.clear();
|
|
249
208
|
}
|
|
209
|
+
await this.persist();
|
|
210
|
+
this.logger.debug("EventCounter.reset(): cleared event counters");
|
|
250
211
|
}
|
|
251
212
|
// ── Private ──────────────────────────────────────────────────────────────
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
this.
|
|
213
|
+
/**
|
|
214
|
+
* Load the counts blob from storage into the in-memory cache.
|
|
215
|
+
* Only runs once; subsequent calls are no-ops.
|
|
216
|
+
*/
|
|
217
|
+
async loadIfNeeded() {
|
|
218
|
+
if (this.loaded) return;
|
|
219
|
+
this.loaded = true;
|
|
258
220
|
try {
|
|
259
|
-
await this.storage.
|
|
221
|
+
const raw = await this.storage.read(COUNTS_KEY);
|
|
222
|
+
if (!raw) return;
|
|
223
|
+
const parsed = JSON.parse(raw);
|
|
224
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
225
|
+
if (typeof v === "number") this.cache.set(k, v);
|
|
226
|
+
}
|
|
260
227
|
} catch (err) {
|
|
261
|
-
this.logger.warn("EventCounter: failed to
|
|
228
|
+
this.logger.warn("EventCounter: failed to load counts blob", err);
|
|
262
229
|
}
|
|
263
230
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
);
|
|
269
|
-
for (const key of sessionKeys) {
|
|
270
|
-
try {
|
|
271
|
-
await this.storage.delete(key);
|
|
272
|
-
} catch {
|
|
273
|
-
}
|
|
274
|
-
this.writtenKeys.delete(key);
|
|
275
|
-
this.cache.delete(key);
|
|
276
|
-
}
|
|
277
|
-
const now = /* @__PURE__ */ new Date();
|
|
278
|
-
const seenBuckets = new Set(sessionKeys);
|
|
279
|
-
for (let i = 0; i < 90; i++) {
|
|
280
|
-
const d = new Date(now);
|
|
281
|
-
d.setDate(d.getDate() - i);
|
|
282
|
-
const periods = ["daily", "weekly", "monthly"];
|
|
283
|
-
for (const period of periods) {
|
|
284
|
-
const key = this.storageKey(eventName, period, d);
|
|
285
|
-
if (!seenBuckets.has(key)) {
|
|
286
|
-
seenBuckets.add(key);
|
|
287
|
-
try {
|
|
288
|
-
await this.storage.delete(key);
|
|
289
|
-
} catch {
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
231
|
+
/** Serialize the full in-memory cache to `sg:counts`. */
|
|
232
|
+
async persist() {
|
|
233
|
+
const obj = {};
|
|
234
|
+
for (const [k, v] of this.cache) obj[k] = v;
|
|
294
235
|
try {
|
|
295
|
-
await this.storage.
|
|
296
|
-
} catch {
|
|
236
|
+
await this.storage.write(COUNTS_KEY, JSON.stringify(obj));
|
|
237
|
+
} catch (err) {
|
|
238
|
+
this.logger.warn("EventCounter: failed to persist counts blob", err);
|
|
297
239
|
}
|
|
298
240
|
}
|
|
299
|
-
/** Build the
|
|
300
|
-
|
|
301
|
-
const bucket = this.bucketFor(period, date);
|
|
241
|
+
/** Build the sub-key within the counts blob for a given event, period, and date. */
|
|
242
|
+
subKey(eventName, period, date) {
|
|
302
243
|
const safeEvent = eventName.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
303
|
-
return `${
|
|
244
|
+
return `${period}:${this.bucketFor(period, date)}:${safeEvent}`;
|
|
304
245
|
}
|
|
305
246
|
/**
|
|
306
247
|
* Return the time-bucket string for a given period and date.
|
|
307
248
|
* - daily: "2024-01-15"
|
|
308
|
-
* - weekly: "2024-W03"
|
|
249
|
+
* - weekly: "2024-W03" (ISO week)
|
|
309
250
|
* - monthly: "2024-01"
|
|
310
251
|
* - all-time: "all"
|
|
311
252
|
*/
|
|
@@ -314,10 +255,9 @@ var EventCounter = class {
|
|
|
314
255
|
case "daily":
|
|
315
256
|
return date.toISOString().slice(0, 10);
|
|
316
257
|
// "YYYY-MM-DD"
|
|
317
|
-
case "weekly":
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
258
|
+
case "weekly":
|
|
259
|
+
return toISOWeek(date);
|
|
260
|
+
// "YYYY-Www"
|
|
321
261
|
case "monthly":
|
|
322
262
|
return date.toISOString().slice(0, 7);
|
|
323
263
|
// "YYYY-MM"
|