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.
- package/dist/concerns/plugin-storage.js +20 -17
- package/dist/concerns/plugin-storage.js.map +1 -1
- package/dist/s3db.cjs +22 -19
- package/dist/s3db.cjs.map +1 -1
- package/dist/s3db.es.js +22 -19
- package/dist/s3db.es.js.map +1 -1
- package/dist/types/concerns/plugin-storage.d.ts +9 -1
- package/dist/types/concerns/plugin-storage.d.ts.map +1 -1
- package/mcp/data/embeddings-core.json +23 -23
- package/mcp/data/embeddings-plugins.json +71 -71
- package/package.json +4 -5
- package/src/concerns/plugin-storage.ts +28 -17
|
@@ -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 =
|
|
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 (
|
|
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
|
|
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 -
|
|
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 &&
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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:
|
|
864
|
-
_expiresAt:
|
|
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 &&
|
|
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 &&
|
|
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) ||
|
|
926
|
-
updatedAt:
|
|
927
|
-
resetAt:
|
|
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;
|