@beignet/core 0.0.13 → 0.0.14
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/CHANGELOG.md +14 -0
- package/README.md +156 -2
- package/dist/entitlements/index.d.ts +234 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +172 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/error-reporting/index.d.ts +131 -0
- package/dist/error-reporting/index.d.ts.map +1 -0
- package/dist/error-reporting/index.js +112 -0
- package/dist/error-reporting/index.js.map +1 -0
- package/dist/flags/index.d.ts +293 -0
- package/dist/flags/index.d.ts.map +1 -0
- package/dist/flags/index.js +204 -0
- package/dist/flags/index.js.map +1 -0
- package/dist/locks/index.d.ts +155 -0
- package/dist/locks/index.d.ts.map +1 -0
- package/dist/locks/index.js +248 -0
- package/dist/locks/index.js.map +1 -0
- package/dist/payments/index.d.ts +430 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +230 -0
- package/dist/payments/index.js.map +1 -0
- package/dist/ports/index.d.ts +61 -1
- package/dist/ports/index.d.ts.map +1 -1
- package/dist/ports/index.js +33 -0
- package/dist/ports/index.js.map +1 -1
- package/dist/ports/policy.d.ts +104 -0
- package/dist/ports/policy.d.ts.map +1 -1
- package/dist/ports/policy.js +102 -7
- package/dist/ports/policy.js.map +1 -1
- package/dist/search/index.d.ts +175 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +344 -0
- package/dist/search/index.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +9 -1
- package/dist/server/server.js.map +1 -1
- package/dist/testing/index.d.ts +46 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +33 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +25 -1
- package/src/entitlements/index.ts +479 -0
- package/src/error-reporting/index.ts +273 -0
- package/src/flags/index.ts +608 -0
- package/src/locks/index.ts +440 -0
- package/src/payments/index.ts +699 -0
- package/src/ports/index.ts +194 -0
- package/src/ports/policy.ts +272 -7
- package/src/search/index.ts +632 -0
- package/src/server/server.ts +15 -0
- package/src/testing/index.ts +83 -1
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @beignet/core/locks
|
|
3
|
+
*
|
|
4
|
+
* Provider-neutral distributed lock and lease primitives for Beignet apps.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createProvider,
|
|
9
|
+
createProviderInstrumentation,
|
|
10
|
+
} from "../providers/index.js";
|
|
11
|
+
|
|
12
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Metadata attached to lock acquisition instrumentation and diagnostics.
|
|
16
|
+
*/
|
|
17
|
+
export type LeaseMetadata = Record<
|
|
18
|
+
string,
|
|
19
|
+
string | number | boolean | null | undefined
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Options for acquiring a lease.
|
|
24
|
+
*/
|
|
25
|
+
export type LeaseAcquireOptions = {
|
|
26
|
+
/**
|
|
27
|
+
* Lease time-to-live in milliseconds. The provider must treat ownership as
|
|
28
|
+
* expired after this duration unless the lease is renewed.
|
|
29
|
+
*/
|
|
30
|
+
ttlMs: number;
|
|
31
|
+
/**
|
|
32
|
+
* How long to wait for the lease before returning a non-acquired result.
|
|
33
|
+
*
|
|
34
|
+
* @default 0
|
|
35
|
+
*/
|
|
36
|
+
waitMs?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Delay between acquisition attempts while waiting.
|
|
39
|
+
*
|
|
40
|
+
* @default 50
|
|
41
|
+
*/
|
|
42
|
+
retryDelayMs?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Optional owner token supplied by the caller. Providers generate one when
|
|
45
|
+
* omitted. Release and renew operations must only succeed for the current
|
|
46
|
+
* owner token.
|
|
47
|
+
*/
|
|
48
|
+
ownerToken?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Optional diagnostics metadata. Providers should not use this for lock
|
|
51
|
+
* correctness.
|
|
52
|
+
*/
|
|
53
|
+
metadata?: LeaseMetadata;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Options for renewing a lease.
|
|
58
|
+
*/
|
|
59
|
+
export type LeaseRenewOptions = {
|
|
60
|
+
/**
|
|
61
|
+
* New lease time-to-live in milliseconds. Defaults to the original ttlMs
|
|
62
|
+
* used to acquire the lease.
|
|
63
|
+
*/
|
|
64
|
+
ttlMs?: number;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Active lease handle returned by a lock provider.
|
|
69
|
+
*/
|
|
70
|
+
export type LeaseHandle = {
|
|
71
|
+
key: string;
|
|
72
|
+
ownerToken: string;
|
|
73
|
+
expiresAt?: Date;
|
|
74
|
+
/**
|
|
75
|
+
* Optional monotonically increasing token. Apps can store this with writes to
|
|
76
|
+
* reject stale lease owners when the underlying resource supports fencing.
|
|
77
|
+
*/
|
|
78
|
+
fencingToken?: string | number;
|
|
79
|
+
renew(options?: LeaseRenewOptions): Promise<boolean>;
|
|
80
|
+
release(): Promise<boolean>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Lease acquisition result.
|
|
85
|
+
*/
|
|
86
|
+
export type LeaseAcquireResult =
|
|
87
|
+
| { acquired: true; lease: LeaseHandle }
|
|
88
|
+
| { acquired: false; reason: "unavailable" | "timeout" };
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* App-facing lock port.
|
|
92
|
+
*/
|
|
93
|
+
export type LocksPort = {
|
|
94
|
+
acquire(
|
|
95
|
+
key: string,
|
|
96
|
+
options: LeaseAcquireOptions,
|
|
97
|
+
): Promise<LeaseAcquireResult>;
|
|
98
|
+
withLease<T>(
|
|
99
|
+
key: string,
|
|
100
|
+
options: LeaseAcquireOptions,
|
|
101
|
+
fn: (ctx: { lease: LeaseHandle }) => MaybePromise<T>,
|
|
102
|
+
): Promise<T | undefined>;
|
|
103
|
+
restore(key: string, ownerToken: string): LeaseHandle;
|
|
104
|
+
forceRelease(key: string): Promise<boolean>;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Captured memory lease state exposed for tests.
|
|
109
|
+
*/
|
|
110
|
+
export type MemoryLeaseRecord = {
|
|
111
|
+
key: string;
|
|
112
|
+
ownerToken: string;
|
|
113
|
+
expiresAt: Date;
|
|
114
|
+
ttlMs: number;
|
|
115
|
+
fencingToken: number;
|
|
116
|
+
metadata?: LeaseMetadata;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* In-memory locks port exposed for assertions in tests.
|
|
121
|
+
*/
|
|
122
|
+
export type MemoryLocksPort = LocksPort & {
|
|
123
|
+
leases: Map<string, MemoryLeaseRecord>;
|
|
124
|
+
reset(): void;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Options for the in-memory locks adapter.
|
|
129
|
+
*/
|
|
130
|
+
export type CreateMemoryLocksOptions = {
|
|
131
|
+
now?: () => Date;
|
|
132
|
+
createOwnerToken?: () => string;
|
|
133
|
+
sleep?: (ms: number) => Promise<void>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Options for the in-memory locks provider.
|
|
138
|
+
*/
|
|
139
|
+
export type MemoryLocksProviderOptions = CreateMemoryLocksOptions & {
|
|
140
|
+
name?: string;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Ports contributed by the memory locks provider.
|
|
145
|
+
*/
|
|
146
|
+
export interface MemoryLocksProviderPorts {
|
|
147
|
+
locks: LocksPort;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Error thrown when a lease option is invalid.
|
|
152
|
+
*/
|
|
153
|
+
export class LeaseOptionsError extends Error {
|
|
154
|
+
constructor(message: string) {
|
|
155
|
+
super(message);
|
|
156
|
+
this.name = "LeaseOptionsError";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Create an in-memory locks port for tests and single-process development.
|
|
162
|
+
*/
|
|
163
|
+
export function createMemoryLocks(
|
|
164
|
+
options: CreateMemoryLocksOptions = {},
|
|
165
|
+
): MemoryLocksPort {
|
|
166
|
+
const leases = new Map<string, MemoryLeaseRecord>();
|
|
167
|
+
const now = options.now ?? (() => new Date());
|
|
168
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
169
|
+
const createOwnerToken = options.createOwnerToken ?? createRandomToken;
|
|
170
|
+
let nextFencingToken = 1;
|
|
171
|
+
|
|
172
|
+
const port: MemoryLocksPort = {
|
|
173
|
+
leases,
|
|
174
|
+
async acquire(key, acquireOptions) {
|
|
175
|
+
validateAcquireOptions(key, acquireOptions);
|
|
176
|
+
const waitMs = acquireOptions.waitMs ?? 0;
|
|
177
|
+
const retryDelayMs = acquireOptions.retryDelayMs ?? 50;
|
|
178
|
+
const deadline = now().getTime() + waitMs;
|
|
179
|
+
const ownerToken = acquireOptions.ownerToken ?? createOwnerToken();
|
|
180
|
+
|
|
181
|
+
while (true) {
|
|
182
|
+
pruneExpired(key, leases, now);
|
|
183
|
+
if (!leases.has(key)) {
|
|
184
|
+
const record: MemoryLeaseRecord = {
|
|
185
|
+
key,
|
|
186
|
+
ownerToken,
|
|
187
|
+
expiresAt: new Date(now().getTime() + acquireOptions.ttlMs),
|
|
188
|
+
ttlMs: acquireOptions.ttlMs,
|
|
189
|
+
fencingToken: nextFencingToken++,
|
|
190
|
+
metadata: acquireOptions.metadata,
|
|
191
|
+
};
|
|
192
|
+
leases.set(key, record);
|
|
193
|
+
return {
|
|
194
|
+
acquired: true,
|
|
195
|
+
lease: createMemoryLease(port, record, now),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (waitMs <= 0) {
|
|
200
|
+
return { acquired: false, reason: "unavailable" };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const remainingMs = deadline - now().getTime();
|
|
204
|
+
if (remainingMs <= 0) {
|
|
205
|
+
pruneExpired(key, leases, now);
|
|
206
|
+
if (!leases.has(key)) continue;
|
|
207
|
+
return { acquired: false, reason: "timeout" };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
await sleep(Math.min(retryDelayMs, remainingMs));
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
async withLease(key, acquireOptions, fn) {
|
|
214
|
+
const result = await port.acquire(key, acquireOptions);
|
|
215
|
+
if (!result.acquired) return undefined;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
return await fn({ lease: result.lease });
|
|
219
|
+
} finally {
|
|
220
|
+
await result.lease.release();
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
restore(key, ownerToken) {
|
|
224
|
+
if (!key) throw new LeaseOptionsError("Lease key is required.");
|
|
225
|
+
if (!ownerToken) {
|
|
226
|
+
throw new LeaseOptionsError("Lease owner token is required.");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return createMemoryLease(
|
|
230
|
+
port,
|
|
231
|
+
{
|
|
232
|
+
key,
|
|
233
|
+
ownerToken,
|
|
234
|
+
expiresAt: new Date(0),
|
|
235
|
+
ttlMs: 1,
|
|
236
|
+
fencingToken: 0,
|
|
237
|
+
},
|
|
238
|
+
now,
|
|
239
|
+
);
|
|
240
|
+
},
|
|
241
|
+
async forceRelease(key) {
|
|
242
|
+
pruneExpired(key, leases, now);
|
|
243
|
+
return leases.delete(key);
|
|
244
|
+
},
|
|
245
|
+
reset() {
|
|
246
|
+
leases.clear();
|
|
247
|
+
nextFencingToken = 1;
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return port;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Create a provider that contributes an in-memory locks port.
|
|
256
|
+
*/
|
|
257
|
+
export function createMemoryLocksProvider(
|
|
258
|
+
options: MemoryLocksProviderOptions = {},
|
|
259
|
+
) {
|
|
260
|
+
const { name = "memory-locks", ...locksOptions } = options;
|
|
261
|
+
|
|
262
|
+
return createProvider({
|
|
263
|
+
name,
|
|
264
|
+
metadata: {
|
|
265
|
+
ports: ["locks"],
|
|
266
|
+
watchers: ["locks"],
|
|
267
|
+
},
|
|
268
|
+
setup({ ports }) {
|
|
269
|
+
const instrumentation = createProviderInstrumentation(ports, {
|
|
270
|
+
providerName: name,
|
|
271
|
+
watcher: "locks",
|
|
272
|
+
});
|
|
273
|
+
const locks = createMemoryLocks(locksOptions);
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
ports: {
|
|
277
|
+
locks: instrumentLocks(locks, instrumentation),
|
|
278
|
+
} satisfies MemoryLocksProviderPorts,
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Convenience helper for any lock port.
|
|
286
|
+
*/
|
|
287
|
+
export function acquireLease(
|
|
288
|
+
locks: LocksPort,
|
|
289
|
+
key: string,
|
|
290
|
+
options: LeaseAcquireOptions,
|
|
291
|
+
): Promise<LeaseAcquireResult> {
|
|
292
|
+
return locks.acquire(key, options);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Convenience helper for any lock port.
|
|
297
|
+
*/
|
|
298
|
+
export function withLease<T>(
|
|
299
|
+
locks: LocksPort,
|
|
300
|
+
key: string,
|
|
301
|
+
options: LeaseAcquireOptions,
|
|
302
|
+
fn: (ctx: { lease: LeaseHandle }) => MaybePromise<T>,
|
|
303
|
+
): Promise<T | undefined> {
|
|
304
|
+
return locks.withLease(key, options, fn);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function createMemoryLease(
|
|
308
|
+
port: MemoryLocksPort,
|
|
309
|
+
record: MemoryLeaseRecord,
|
|
310
|
+
now: () => Date,
|
|
311
|
+
): LeaseHandle {
|
|
312
|
+
const lease: LeaseHandle = {
|
|
313
|
+
key: record.key,
|
|
314
|
+
ownerToken: record.ownerToken,
|
|
315
|
+
expiresAt: record.expiresAt,
|
|
316
|
+
fencingToken: record.fencingToken,
|
|
317
|
+
async renew(options) {
|
|
318
|
+
const active = port.leases.get(record.key);
|
|
319
|
+
if (
|
|
320
|
+
!active ||
|
|
321
|
+
active.ownerToken !== record.ownerToken ||
|
|
322
|
+
active.expiresAt.getTime() <= now().getTime()
|
|
323
|
+
) {
|
|
324
|
+
port.leases.delete(record.key);
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const ttlMs = options?.ttlMs ?? active.ttlMs;
|
|
329
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
330
|
+
throw new LeaseOptionsError("Lease ttlMs must be a positive number.");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
active.ttlMs = ttlMs;
|
|
334
|
+
active.expiresAt = new Date(now().getTime() + ttlMs);
|
|
335
|
+
lease.expiresAt = active.expiresAt;
|
|
336
|
+
return true;
|
|
337
|
+
},
|
|
338
|
+
async release() {
|
|
339
|
+
const active = port.leases.get(record.key);
|
|
340
|
+
if (!active || active.ownerToken !== record.ownerToken) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if (active.expiresAt.getTime() <= now().getTime()) {
|
|
344
|
+
port.leases.delete(record.key);
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return port.leases.delete(record.key);
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
return lease;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function instrumentLocks(
|
|
356
|
+
locks: LocksPort,
|
|
357
|
+
instrumentation: ReturnType<typeof createProviderInstrumentation>,
|
|
358
|
+
): LocksPort {
|
|
359
|
+
const acquire: LocksPort["acquire"] = async (key, options) => {
|
|
360
|
+
const startedAt = Date.now();
|
|
361
|
+
const result = await locks.acquire(key, options);
|
|
362
|
+
instrumentation.custom({
|
|
363
|
+
name: "locks.acquire",
|
|
364
|
+
label: "Lock acquire",
|
|
365
|
+
summary: result.acquired ? "Lease acquired" : "Lease not acquired",
|
|
366
|
+
details: {
|
|
367
|
+
key,
|
|
368
|
+
acquired: result.acquired,
|
|
369
|
+
reason: result.acquired ? undefined : result.reason,
|
|
370
|
+
ttlMs: options.ttlMs,
|
|
371
|
+
waitMs: options.waitMs ?? 0,
|
|
372
|
+
durationMs: Date.now() - startedAt,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
return result;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
acquire,
|
|
380
|
+
async withLease(key, options, fn) {
|
|
381
|
+
const result = await acquire(key, options);
|
|
382
|
+
if (!result.acquired) return undefined;
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
return await fn({ lease: result.lease });
|
|
386
|
+
} finally {
|
|
387
|
+
await result.lease.release();
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
restore(key, ownerToken) {
|
|
391
|
+
return locks.restore(key, ownerToken);
|
|
392
|
+
},
|
|
393
|
+
async forceRelease(key) {
|
|
394
|
+
const released = await locks.forceRelease(key);
|
|
395
|
+
instrumentation.custom({
|
|
396
|
+
name: "locks.forceRelease",
|
|
397
|
+
label: "Lock force release",
|
|
398
|
+
summary: released ? "Lease force released" : "Lease not found",
|
|
399
|
+
details: { key, released },
|
|
400
|
+
});
|
|
401
|
+
return released;
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function validateAcquireOptions(key: string, options: LeaseAcquireOptions) {
|
|
407
|
+
if (!key) throw new LeaseOptionsError("Lease key is required.");
|
|
408
|
+
if (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) {
|
|
409
|
+
throw new LeaseOptionsError("Lease ttlMs must be a positive number.");
|
|
410
|
+
}
|
|
411
|
+
if (options.waitMs !== undefined && options.waitMs < 0) {
|
|
412
|
+
throw new LeaseOptionsError("Lease waitMs must be zero or greater.");
|
|
413
|
+
}
|
|
414
|
+
if (options.retryDelayMs !== undefined && options.retryDelayMs <= 0) {
|
|
415
|
+
throw new LeaseOptionsError("Lease retryDelayMs must be positive.");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function pruneExpired(
|
|
420
|
+
key: string,
|
|
421
|
+
leases: Map<string, MemoryLeaseRecord>,
|
|
422
|
+
now: () => Date,
|
|
423
|
+
) {
|
|
424
|
+
const active = leases.get(key);
|
|
425
|
+
if (active && active.expiresAt.getTime() <= now().getTime()) {
|
|
426
|
+
leases.delete(key);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function createRandomToken(): string {
|
|
431
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
|
432
|
+
return crypto.randomUUID();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function defaultSleep(ms: number): Promise<void> {
|
|
439
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
440
|
+
}
|