s3db.js 19.0.1 → 19.1.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.
@@ -136,13 +136,22 @@ interface SequenceLockOptions {
136
136
  timeout?: number;
137
137
  }
138
138
 
139
+ export interface PluginStorageOptions {
140
+ /**
141
+ * Custom time function for testing. Defaults to Date.now.
142
+ * Inject a mock function to enable time-travel in tests.
143
+ */
144
+ now?: () => number;
145
+ }
146
+
139
147
  export class PluginStorage {
140
148
  client: PluginClient;
141
149
  pluginSlug: string;
142
150
  private _lock: DistributedLock;
143
151
  private _sequence: DistributedSequence;
152
+ private _now: () => number;
144
153
 
145
- constructor(client: PluginClient, pluginSlug: string) {
154
+ constructor(client: PluginClient, pluginSlug: string, options: PluginStorageOptions = {}) {
146
155
  if (!client) {
147
156
  throw new PluginStorageError('PluginStorage requires a client instance', {
148
157
  operation: 'constructor',
@@ -159,6 +168,8 @@ export class PluginStorage {
159
168
 
160
169
  this.client = client;
161
170
  this.pluginSlug = pluginSlug;
171
+ // Use arrow function to capture Date.now dynamically (enables FakeTimers mocking)
172
+ this._now = options.now ?? (() => Date.now());
162
173
 
163
174
  this._lock = new DistributedLock(this as unknown as StorageAdapter, {
164
175
  keyGenerator: (name: string) => this.getPluginKey(null, 'locks', name)
@@ -198,7 +209,7 @@ export class PluginStorage {
198
209
  const dataToSave: Record<string, unknown> = { ...data };
199
210
 
200
211
  if (ttl && typeof ttl === 'number' && ttl > 0) {
201
- dataToSave._expiresAt = Date.now() + (ttl * 1000);
212
+ dataToSave._expiresAt = this._now() + (ttl * 1000);
202
213
  }
203
214
 
204
215
  const { metadata, body } = this._applyBehavior(dataToSave, behavior);
@@ -299,7 +310,7 @@ export class PluginStorage {
299
310
 
300
311
  const expiresAt = (data._expiresat || data._expiresAt) as number | undefined;
301
312
  if (expiresAt) {
302
- if (Date.now() > expiresAt) {
313
+ if (this._now() > expiresAt) {
303
314
  await this.delete(key);
304
315
  return null;
305
316
  }
@@ -463,7 +474,7 @@ export class PluginStorage {
463
474
  return false;
464
475
  }
465
476
 
466
- return Date.now() > expiresAt;
477
+ return this._now() > expiresAt;
467
478
  }
468
479
 
469
480
  async getTTL(key: string): Promise<number | null> {
@@ -500,7 +511,7 @@ export class PluginStorage {
500
511
  return null;
501
512
  }
502
513
 
503
- const remaining = Math.max(0, expiresAt - Date.now());
514
+ const remaining = Math.max(0, expiresAt - this._now());
504
515
  return Math.floor(remaining / 1000);
505
516
  }
506
517
 
@@ -676,7 +687,7 @@ export class PluginStorage {
676
687
 
677
688
  // Check expiration
678
689
  const expiresAt = (data._expiresat || data._expiresAt) as number | undefined;
679
- if (expiresAt && Date.now() > expiresAt) {
690
+ if (expiresAt && this._now() > expiresAt) {
680
691
  await this.delete(key);
681
692
  return { data: null, version: null };
682
693
  }
@@ -770,7 +781,7 @@ export class PluginStorage {
770
781
  parsedMetadata.value = newValue;
771
782
 
772
783
  if (options.ttl) {
773
- parsedMetadata._expiresAt = Date.now() + (options.ttl * 1000);
784
+ parsedMetadata._expiresAt = this._now() + (options.ttl * 1000);
774
785
  }
775
786
 
776
787
  const encodedMetadata: Record<string, string> = {};
@@ -822,7 +833,7 @@ export class PluginStorage {
822
833
  value: initialValue + increment,
823
834
  name,
824
835
  resourceName,
825
- createdAt: Date.now()
836
+ createdAt: this._now()
826
837
  }, { behavior: 'body-only' });
827
838
  return initialValue;
828
839
  }
@@ -831,7 +842,7 @@ export class PluginStorage {
831
842
  await this.set(valueKey, {
832
843
  ...data,
833
844
  value: currentValue + increment,
834
- updatedAt: Date.now()
845
+ updatedAt: this._now()
835
846
  }, { behavior: 'body-only' });
836
847
 
837
848
  return currentValue;
@@ -854,14 +865,14 @@ export class PluginStorage {
854
865
  private async _withSequenceLock<T>(lockKey: string, options: SequenceLockOptions, callback: () => Promise<T>): Promise<T | null> {
855
866
  const { ttl = 30, timeout = 5000 } = options;
856
867
  const token = idGenerator();
857
- const startTime = Date.now();
868
+ const startTime = this._now();
858
869
  let attempt = 0;
859
870
 
860
871
  while (true) {
861
872
  const payload = {
862
873
  token,
863
- acquiredAt: Date.now(),
864
- _expiresAt: Date.now() + (ttl * 1000)
874
+ acquiredAt: this._now(),
875
+ _expiresAt: this._now() + (ttl * 1000)
865
876
  };
866
877
 
867
878
  const [ok, err] = await tryFn(() => this.set(lockKey, payload, {
@@ -884,14 +895,14 @@ export class PluginStorage {
884
895
  throw err;
885
896
  }
886
897
 
887
- if (timeout !== undefined && Date.now() - startTime >= timeout) {
898
+ if (timeout !== undefined && this._now() - startTime >= timeout) {
888
899
  return null;
889
900
  }
890
901
 
891
902
  const current = await this.get(lockKey);
892
903
  if (!current) continue;
893
904
 
894
- if (current._expiresAt && Date.now() > (current._expiresAt as number)) {
905
+ if (current._expiresAt && this._now() > (current._expiresAt as number)) {
895
906
  await tryFn(() => this.delete(lockKey));
896
907
  continue;
897
908
  }
@@ -922,9 +933,9 @@ export class PluginStorage {
922
933
  value,
923
934
  name,
924
935
  resourceName,
925
- createdAt: (data?.createdAt as number) || Date.now(),
926
- updatedAt: Date.now(),
927
- resetAt: Date.now()
936
+ createdAt: (data?.createdAt as number) || this._now(),
937
+ updatedAt: this._now(),
938
+ resetAt: this._now()
928
939
  }, { behavior: 'body-only' });
929
940
 
930
941
  return true;