@getstrata/core 0.5.15 → 0.5.17
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/bootstrap/http/securedRouteModelBinding.d.ts +11 -0
- package/dist/bootstrap/membershipService.d.ts +3 -0
- package/dist/bootstrap/queue/defaultJobs.d.ts +2 -0
- package/dist/core/auth/membershipService.d.ts +1 -2
- package/dist/core/http/securedRouteModelBinding.d.ts +2 -11
- package/dist/core/queue/createAppQueue.d.ts +3 -3
- package/dist/entries/cache/createCacheStore.js +376 -0
- package/dist/entries/queue/createAppQueue.js +484 -484
- package/dist/entries/queue/queueMetrics.js +881 -881
- package/dist/framework/public-api.d.ts +1 -0
- package/dist/index.js +394 -20
- package/package.json +7 -2
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Policy } from "../../core/auth/policy";
|
|
2
|
+
import type { RouteRequest } from "../../core/http/route";
|
|
3
|
+
interface RouteModelAuthorization {
|
|
4
|
+
resource: string;
|
|
5
|
+
action: keyof Policy;
|
|
6
|
+
requireIfMatch?: boolean;
|
|
7
|
+
}
|
|
8
|
+
declare function securedBindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
|
|
9
|
+
declare function securedBindRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
|
|
10
|
+
export type { RouteModelAuthorization };
|
|
11
|
+
export { securedBindRouteModel, securedBindRouteModelByKey };
|
|
@@ -18,7 +18,6 @@ declare class MembershipService {
|
|
|
18
18
|
}): Promise<import("../../modules/organization/memberTypes").OrganizationMemberRecord>;
|
|
19
19
|
removeMember(organizationId: number, userId: number): Promise<boolean>;
|
|
20
20
|
}
|
|
21
|
-
declare function resolveMembershipService(): MembershipService;
|
|
22
21
|
export type { MembershipRepositoryLike };
|
|
23
22
|
export default MembershipService;
|
|
24
|
-
export { resolveMembershipService };
|
|
23
|
+
export { resolveMembershipService } from "../../bootstrap/membershipService.ts";
|
|
@@ -1,11 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
interface RouteModelAuthorization {
|
|
4
|
-
resource: string;
|
|
5
|
-
action: keyof Policy;
|
|
6
|
-
requireIfMatch?: boolean;
|
|
7
|
-
}
|
|
8
|
-
declare function securedBindRouteModel<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (id: number, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
|
|
9
|
-
declare function securedBindRouteModelByKey<TParams extends Record<string, string>, TModel, TParam extends keyof TParams & string>(param: TParam, resolver: (key: string, request: RouteRequest<TParams>) => Promise<TModel>, authorization: RouteModelAuthorization, handler: (request: RouteRequest<TParams>, model: TModel) => Response | Promise<Response>): (request: RouteRequest<TParams>) => Promise<Response>;
|
|
10
|
-
export type { RouteModelAuthorization };
|
|
11
|
-
export { securedBindRouteModel, securedBindRouteModelByKey };
|
|
1
|
+
export type { RouteModelAuthorization } from "../../bootstrap/http/securedRouteModelBinding.ts";
|
|
2
|
+
export { securedBindRouteModel, securedBindRouteModelByKey, } from "../../bootstrap/http/securedRouteModelBinding.ts";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Queue } from "./index";
|
|
2
2
|
import { createFailedJobService, createQueueWorker, createTrackedJob, type FailedJobService } from "./publicQueue";
|
|
3
3
|
declare const FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
|
|
4
|
-
declare function
|
|
5
|
-
|
|
6
|
-
export { createAppQueue, createFailedJobService, createQueueWorker, createTrackedJob, FAILED_JOB_SERVICE_TOKEN,
|
|
4
|
+
declare function createAppQueue(driver: "sync" | "async" | "redis", redisUrl?: string, failedJobs?: FailedJobService, registerJobs?: () => void): Queue;
|
|
5
|
+
export { registerDefaultJobs } from "../../bootstrap/queue/defaultJobs.ts";
|
|
6
|
+
export { createAppQueue, createFailedJobService, createQueueWorker, createTrackedJob, FAILED_JOB_SERVICE_TOKEN, };
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/core/cache/redisCacheStore.ts
|
|
3
|
+
var {RedisClient } = globalThis.Bun;
|
|
4
|
+
var KEY_PREFIX = "workhub:cache:";
|
|
5
|
+
var TAG_PREFIX = "workhub:cache:tag:";
|
|
6
|
+
|
|
7
|
+
class RedisCacheStore {
|
|
8
|
+
ttlMs;
|
|
9
|
+
maxEntries;
|
|
10
|
+
client;
|
|
11
|
+
inflight = new Map;
|
|
12
|
+
keyTags = new Map;
|
|
13
|
+
constructor(redisUrl, ttlMs, maxEntries) {
|
|
14
|
+
this.ttlMs = ttlMs;
|
|
15
|
+
this.maxEntries = maxEntries;
|
|
16
|
+
this.client = new RedisClient(redisUrl);
|
|
17
|
+
}
|
|
18
|
+
async get(key) {
|
|
19
|
+
const raw = await this.client.get(this.storageKey(key));
|
|
20
|
+
if (raw === null) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
return JSON.parse(raw);
|
|
24
|
+
}
|
|
25
|
+
async set(key, value, ttlMs) {
|
|
26
|
+
const resolvedTtlMs = ttlMs ?? this.ttlMs;
|
|
27
|
+
const payload = JSON.stringify(value);
|
|
28
|
+
if (resolvedTtlMs > 0) {
|
|
29
|
+
await this.client.psetex(this.storageKey(key), resolvedTtlMs, payload);
|
|
30
|
+
} else {
|
|
31
|
+
await this.client.set(this.storageKey(key), payload);
|
|
32
|
+
}
|
|
33
|
+
await this.enforceMaxEntries();
|
|
34
|
+
}
|
|
35
|
+
async getOrSet(key, loader, ttlMs) {
|
|
36
|
+
const cached = await this.get(key);
|
|
37
|
+
if (cached !== undefined) {
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
const inflightRequest = this.inflight.get(key);
|
|
41
|
+
if (inflightRequest) {
|
|
42
|
+
return inflightRequest;
|
|
43
|
+
}
|
|
44
|
+
const pendingRequest = loader().then(async (value) => {
|
|
45
|
+
await this.set(key, value, ttlMs);
|
|
46
|
+
return value;
|
|
47
|
+
}).finally(() => {
|
|
48
|
+
this.inflight.delete(key);
|
|
49
|
+
});
|
|
50
|
+
this.inflight.set(key, pendingRequest);
|
|
51
|
+
return pendingRequest;
|
|
52
|
+
}
|
|
53
|
+
async attachTags(key, tags) {
|
|
54
|
+
if (tags.length === 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let tagsForKey = this.keyTags.get(key);
|
|
58
|
+
if (!tagsForKey) {
|
|
59
|
+
tagsForKey = new Set;
|
|
60
|
+
this.keyTags.set(key, tagsForKey);
|
|
61
|
+
}
|
|
62
|
+
for (const tag of tags) {
|
|
63
|
+
tagsForKey.add(tag);
|
|
64
|
+
await this.client.sadd(this.tagKey(tag), key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async flushTags(tags) {
|
|
68
|
+
const keysToRemove = new Set;
|
|
69
|
+
for (const tag of tags) {
|
|
70
|
+
const members = await this.client.smembers(this.tagKey(tag));
|
|
71
|
+
for (const member of members) {
|
|
72
|
+
keysToRemove.add(member);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let removed = 0;
|
|
76
|
+
for (const key of keysToRemove) {
|
|
77
|
+
if (await this.invalidate(key)) {
|
|
78
|
+
removed += 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const tag of tags) {
|
|
82
|
+
await this.client.del(this.tagKey(tag));
|
|
83
|
+
}
|
|
84
|
+
return removed;
|
|
85
|
+
}
|
|
86
|
+
async invalidate(key) {
|
|
87
|
+
const deleted = await this.client.del(this.storageKey(key));
|
|
88
|
+
await this.detachKeyFromTags(key);
|
|
89
|
+
return deleted > 0;
|
|
90
|
+
}
|
|
91
|
+
async invalidateByPrefix(prefix) {
|
|
92
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
93
|
+
let removed = 0;
|
|
94
|
+
for (const storageKey of keys) {
|
|
95
|
+
const key = storageKey.slice(KEY_PREFIX.length);
|
|
96
|
+
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
97
|
+
if (await this.invalidate(key)) {
|
|
98
|
+
removed += 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return removed;
|
|
103
|
+
}
|
|
104
|
+
async clear() {
|
|
105
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
106
|
+
if (keys.length > 0) {
|
|
107
|
+
await this.client.del(...keys);
|
|
108
|
+
}
|
|
109
|
+
const tagKeys = await this.client.keys(`${TAG_PREFIX}*`);
|
|
110
|
+
if (tagKeys.length > 0) {
|
|
111
|
+
await this.client.del(...tagKeys);
|
|
112
|
+
}
|
|
113
|
+
this.inflight.clear();
|
|
114
|
+
this.keyTags.clear();
|
|
115
|
+
}
|
|
116
|
+
async size() {
|
|
117
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
118
|
+
return keys.length;
|
|
119
|
+
}
|
|
120
|
+
storageKey(key) {
|
|
121
|
+
return `${KEY_PREFIX}${key}`;
|
|
122
|
+
}
|
|
123
|
+
tagKey(tag) {
|
|
124
|
+
return `${TAG_PREFIX}${tag}`;
|
|
125
|
+
}
|
|
126
|
+
async detachKeyFromTags(key) {
|
|
127
|
+
const tags = this.keyTags.get(key);
|
|
128
|
+
if (!tags) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const tag of tags) {
|
|
132
|
+
await this.client.srem(this.tagKey(tag), key);
|
|
133
|
+
}
|
|
134
|
+
this.keyTags.delete(key);
|
|
135
|
+
}
|
|
136
|
+
async enforceMaxEntries() {
|
|
137
|
+
const keys = await this.client.keys(`${KEY_PREFIX}*`);
|
|
138
|
+
if (keys.length <= this.maxEntries) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const overflow = keys.length - this.maxEntries;
|
|
142
|
+
const keysToRemove = keys.slice(0, overflow);
|
|
143
|
+
if (keysToRemove.length > 0) {
|
|
144
|
+
await this.client.del(...keysToRemove);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
var redisCacheStore_default = RedisCacheStore;
|
|
149
|
+
|
|
150
|
+
// ../../src/core/cache/simpleCache.ts
|
|
151
|
+
class SimpleCache {
|
|
152
|
+
ttlMs;
|
|
153
|
+
maxEntries;
|
|
154
|
+
cache = new Map;
|
|
155
|
+
inflight = new Map;
|
|
156
|
+
tagIndex = new Map;
|
|
157
|
+
keyTags = new Map;
|
|
158
|
+
constructor(ttlMs = 3600000, maxEntries = 100) {
|
|
159
|
+
this.ttlMs = ttlMs;
|
|
160
|
+
this.maxEntries = maxEntries;
|
|
161
|
+
if (!Number.isFinite(ttlMs) || ttlMs < 0) {
|
|
162
|
+
throw new RangeError("ttlMs must be a non-negative number.");
|
|
163
|
+
}
|
|
164
|
+
if (!Number.isInteger(maxEntries) || maxEntries < 1) {
|
|
165
|
+
throw new RangeError("maxEntries must be a positive integer.");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
get(key) {
|
|
169
|
+
return this.getFreshEntry(key)?.value;
|
|
170
|
+
}
|
|
171
|
+
set(key, value, ttlMs) {
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const resolvedTtlMs = ttlMs ?? this.ttlMs;
|
|
174
|
+
this.cache.set(key, {
|
|
175
|
+
value,
|
|
176
|
+
expiresAt: now + resolvedTtlMs,
|
|
177
|
+
lastAccessedAt: now
|
|
178
|
+
});
|
|
179
|
+
this.evictOverflow();
|
|
180
|
+
}
|
|
181
|
+
async getOrSet(key, loader, ttlMs) {
|
|
182
|
+
this.pruneExpired();
|
|
183
|
+
const cachedEntry = this.getFreshEntry(key);
|
|
184
|
+
if (cachedEntry) {
|
|
185
|
+
return cachedEntry.value;
|
|
186
|
+
}
|
|
187
|
+
const inflightRequest = this.inflight.get(key);
|
|
188
|
+
if (inflightRequest) {
|
|
189
|
+
return inflightRequest;
|
|
190
|
+
}
|
|
191
|
+
const pendingRequest = loader().then((value) => {
|
|
192
|
+
this.set(key, value, ttlMs);
|
|
193
|
+
return value;
|
|
194
|
+
}).finally(() => {
|
|
195
|
+
this.inflight.delete(key);
|
|
196
|
+
});
|
|
197
|
+
this.inflight.set(key, pendingRequest);
|
|
198
|
+
return pendingRequest;
|
|
199
|
+
}
|
|
200
|
+
attachTags(key, tags) {
|
|
201
|
+
if (tags.length === 0) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
let tagsForKey = this.keyTags.get(key);
|
|
205
|
+
if (!tagsForKey) {
|
|
206
|
+
tagsForKey = new Set;
|
|
207
|
+
this.keyTags.set(key, tagsForKey);
|
|
208
|
+
}
|
|
209
|
+
for (const tag of tags) {
|
|
210
|
+
tagsForKey.add(tag);
|
|
211
|
+
let keysForTag = this.tagIndex.get(tag);
|
|
212
|
+
if (!keysForTag) {
|
|
213
|
+
keysForTag = new Set;
|
|
214
|
+
this.tagIndex.set(tag, keysForTag);
|
|
215
|
+
}
|
|
216
|
+
keysForTag.add(key);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
flushTags(tags) {
|
|
220
|
+
const keysToRemove = new Set;
|
|
221
|
+
for (const tag of tags) {
|
|
222
|
+
const keys = this.tagIndex.get(tag);
|
|
223
|
+
if (!keys) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
for (const key of keys) {
|
|
227
|
+
keysToRemove.add(key);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
let removed = 0;
|
|
231
|
+
for (const key of keysToRemove) {
|
|
232
|
+
if (this.invalidate(key)) {
|
|
233
|
+
removed += 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (const tag of tags) {
|
|
237
|
+
this.tagIndex.delete(tag);
|
|
238
|
+
}
|
|
239
|
+
return removed;
|
|
240
|
+
}
|
|
241
|
+
invalidate(key) {
|
|
242
|
+
const removed = this.cache.delete(key);
|
|
243
|
+
if (removed) {
|
|
244
|
+
this.detachKeyFromTags(key);
|
|
245
|
+
}
|
|
246
|
+
return removed;
|
|
247
|
+
}
|
|
248
|
+
invalidateByPrefix(prefix) {
|
|
249
|
+
let removed = 0;
|
|
250
|
+
for (const key of [...this.cache.keys()]) {
|
|
251
|
+
if (key === prefix || key.startsWith(`${prefix}?`)) {
|
|
252
|
+
if (this.invalidate(key)) {
|
|
253
|
+
removed += 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return removed;
|
|
258
|
+
}
|
|
259
|
+
clear() {
|
|
260
|
+
this.cache.clear();
|
|
261
|
+
this.inflight.clear();
|
|
262
|
+
this.tagIndex.clear();
|
|
263
|
+
this.keyTags.clear();
|
|
264
|
+
}
|
|
265
|
+
size() {
|
|
266
|
+
this.pruneExpired();
|
|
267
|
+
return this.cache.size;
|
|
268
|
+
}
|
|
269
|
+
detachKeyFromTags(key) {
|
|
270
|
+
const tags = this.keyTags.get(key);
|
|
271
|
+
if (!tags) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
for (const tag of tags) {
|
|
275
|
+
const keys = this.tagIndex.get(tag);
|
|
276
|
+
if (!keys) {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
keys.delete(key);
|
|
280
|
+
if (keys.size === 0) {
|
|
281
|
+
this.tagIndex.delete(tag);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
this.keyTags.delete(key);
|
|
285
|
+
}
|
|
286
|
+
getFreshEntry(key) {
|
|
287
|
+
const entry = this.cache.get(key);
|
|
288
|
+
if (!entry) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (entry.expiresAt <= Date.now()) {
|
|
292
|
+
this.invalidate(key);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
entry.lastAccessedAt = Date.now();
|
|
296
|
+
return entry;
|
|
297
|
+
}
|
|
298
|
+
pruneExpired() {
|
|
299
|
+
const now = Date.now();
|
|
300
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
301
|
+
if (entry.expiresAt <= now) {
|
|
302
|
+
this.invalidate(key);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
evictOverflow() {
|
|
307
|
+
while (this.cache.size > this.maxEntries) {
|
|
308
|
+
let oldestKey;
|
|
309
|
+
let oldestAccessTime = Number.POSITIVE_INFINITY;
|
|
310
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
311
|
+
if (entry.lastAccessedAt < oldestAccessTime) {
|
|
312
|
+
oldestAccessTime = entry.lastAccessedAt;
|
|
313
|
+
oldestKey = key;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (!oldestKey) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
this.invalidate(oldestKey);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
var simpleCache_default = SimpleCache;
|
|
324
|
+
|
|
325
|
+
// ../../src/core/cache/simpleCacheStore.ts
|
|
326
|
+
class SimpleCacheStore {
|
|
327
|
+
cache;
|
|
328
|
+
constructor(cache) {
|
|
329
|
+
this.cache = cache;
|
|
330
|
+
}
|
|
331
|
+
get(key) {
|
|
332
|
+
return Promise.resolve(this.cache.get(key));
|
|
333
|
+
}
|
|
334
|
+
set(key, value, ttlMs) {
|
|
335
|
+
this.cache.set(key, value, ttlMs);
|
|
336
|
+
return Promise.resolve();
|
|
337
|
+
}
|
|
338
|
+
getOrSet(key, loader, ttlMs) {
|
|
339
|
+
return this.cache.getOrSet(key, loader, ttlMs);
|
|
340
|
+
}
|
|
341
|
+
attachTags(key, tags) {
|
|
342
|
+
this.cache.attachTags(key, tags);
|
|
343
|
+
return Promise.resolve();
|
|
344
|
+
}
|
|
345
|
+
flushTags(tags) {
|
|
346
|
+
return Promise.resolve(this.cache.flushTags(tags));
|
|
347
|
+
}
|
|
348
|
+
invalidate(key) {
|
|
349
|
+
return Promise.resolve(this.cache.invalidate(key));
|
|
350
|
+
}
|
|
351
|
+
invalidateByPrefix(prefix) {
|
|
352
|
+
return Promise.resolve(this.cache.invalidateByPrefix(prefix));
|
|
353
|
+
}
|
|
354
|
+
clear() {
|
|
355
|
+
this.cache.clear();
|
|
356
|
+
return Promise.resolve();
|
|
357
|
+
}
|
|
358
|
+
size() {
|
|
359
|
+
return Promise.resolve(this.cache.size());
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
var simpleCacheStore_default = SimpleCacheStore;
|
|
363
|
+
|
|
364
|
+
// ../../src/core/cache/createCacheStore.ts
|
|
365
|
+
function createCacheStore(options) {
|
|
366
|
+
if (options.driver === "redis") {
|
|
367
|
+
if (!options.redisUrl) {
|
|
368
|
+
throw new Error('CACHE_DRIVER="redis" requires REDIS_URL to be set.');
|
|
369
|
+
}
|
|
370
|
+
return new redisCacheStore_default(options.redisUrl, options.ttlMs, options.maxEntries);
|
|
371
|
+
}
|
|
372
|
+
return new simpleCacheStore_default(new simpleCache_default(options.ttlMs, options.maxEntries));
|
|
373
|
+
}
|
|
374
|
+
export {
|
|
375
|
+
createCacheStore
|
|
376
|
+
};
|