@kindgi/bounds 0.0.0-stage → 0.1.8-rc.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/LICENSE +201 -0
- package/README.md +110 -2
- package/dist/bounds.d.ts +111 -0
- package/dist/bounds.d.ts.map +1 -0
- package/dist/bounds.js +95 -0
- package/dist/bounds.js.map +1 -0
- package/dist/budget.d.ts +65 -0
- package/dist/budget.d.ts.map +1 -0
- package/dist/budget.js +145 -0
- package/dist/budget.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/pattern.d.ts +62 -0
- package/dist/pattern.d.ts.map +1 -0
- package/dist/pattern.js +253 -0
- package/dist/pattern.js.map +1 -0
- package/dist/tenant.d.ts +31 -0
- package/dist/tenant.d.ts.map +1 -0
- package/dist/tenant.js +33 -0
- package/dist/tenant.js.map +1 -0
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +8 -0
- package/dist/testing.js.map +1 -0
- package/package.json +53 -3
- package/src/bounds.ts +192 -0
- package/src/budget.ts +181 -0
- package/src/index.ts +32 -0
- package/src/pattern.ts +271 -0
- package/src/tenant.ts +60 -0
- package/src/testing.ts +8 -0
package/dist/budget.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* Budgets kept per tenant: the time a tenant's work of one kind (its
|
|
5
|
+
* schema compiles, its template renders) takes in any 60 seconds. That
|
|
6
|
+
* work holds the event loop while it runs, so a tenant past its budget
|
|
7
|
+
* waits instead of the others: its next piece of that work is refused
|
|
8
|
+
* until the window moves on, and may be retried then. The budget doesn't
|
|
9
|
+
* stop a piece it let start, so a tenant's minute can run past the budget
|
|
10
|
+
* by one piece.
|
|
11
|
+
*/
|
|
12
|
+
/** The window a budget kept per tenant counts over, in milliseconds. */
|
|
13
|
+
export const BUDGET_WINDOW_MS = 60_000;
|
|
14
|
+
/**
|
|
15
|
+
* A tenant's work refused: it spent its budget of `budgetMs` (`limit`) in
|
|
16
|
+
* the last 60 seconds, and is under it again after `retryAfterMs`. Every
|
|
17
|
+
* budget's refusal is one of these, so what answers for one (the API, an
|
|
18
|
+
* agent's turn, a run's step) answers for them all.
|
|
19
|
+
*/
|
|
20
|
+
export class TenantBudgetError extends Error {
|
|
21
|
+
limit;
|
|
22
|
+
budgetMs;
|
|
23
|
+
retryAfterMs;
|
|
24
|
+
constructor(message, limit, budgetMs, retryAfterMs) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.limit = limit;
|
|
27
|
+
this.budgetMs = budgetMs;
|
|
28
|
+
this.retryAfterMs = retryAfterMs;
|
|
29
|
+
this.name = 'TenantBudgetError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** The system's clock, read when asked: `Date.now` and `performance.now`. */
|
|
33
|
+
export const SYSTEM_CLOCK = {
|
|
34
|
+
now: () => Date.now(),
|
|
35
|
+
monotonic: () => performance.now(),
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The time each tenant spent on one kind of work in the last 60 seconds,
|
|
39
|
+
* against `budgetMs`; `refuse` makes the error for a tenant past it. An
|
|
40
|
+
* infinite budget keeps no count. Charging a piece costs the same however
|
|
41
|
+
* many pieces the window holds.
|
|
42
|
+
*/
|
|
43
|
+
export class TenantTimeBudget {
|
|
44
|
+
budgetMs;
|
|
45
|
+
refuse;
|
|
46
|
+
clock;
|
|
47
|
+
ledgers = new Map();
|
|
48
|
+
budgetUs;
|
|
49
|
+
sweptAt = 0;
|
|
50
|
+
constructor(budgetMs, refuse, clock = SYSTEM_CLOCK) {
|
|
51
|
+
this.budgetMs = budgetMs;
|
|
52
|
+
this.refuse = refuse;
|
|
53
|
+
this.clock = clock;
|
|
54
|
+
this.budgetUs = budgetMs * 1_000;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Runs `work` as `tenant`'s, and returns what it returns: refused with
|
|
58
|
+
* `refuse`'s error, before it starts, once the tenant has spent its
|
|
59
|
+
* budget. Its time is charged, a failed piece's too.
|
|
60
|
+
*/
|
|
61
|
+
charge(tenant, work) {
|
|
62
|
+
if (this.budgetMs === Number.POSITIVE_INFINITY)
|
|
63
|
+
return work();
|
|
64
|
+
const now = this.clock.now();
|
|
65
|
+
this.sweep(now);
|
|
66
|
+
this.check(tenant, now);
|
|
67
|
+
const started = this.clock.monotonic();
|
|
68
|
+
try {
|
|
69
|
+
return work();
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
this.spend(tenant, now, this.clock.monotonic() - started);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Once a window: drops the work that left it, and the tenants with none left. */
|
|
76
|
+
sweep(now) {
|
|
77
|
+
if (now - this.sweptAt <= BUDGET_WINDOW_MS)
|
|
78
|
+
return;
|
|
79
|
+
this.sweptAt = now;
|
|
80
|
+
for (const [tenant, ledger] of this.ledgers) {
|
|
81
|
+
if (this.inWindow(ledger, now) === 0)
|
|
82
|
+
this.ledgers.delete(tenant);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Throws `refuse`'s error when `tenant` has spent its budget. */
|
|
86
|
+
check(tenant, now) {
|
|
87
|
+
const ledger = this.ledgerOf(tenant);
|
|
88
|
+
this.inWindow(ledger, now);
|
|
89
|
+
if (ledger.spentUs < this.budgetUs)
|
|
90
|
+
return;
|
|
91
|
+
// Read after the count, so a wall clock set back never refuses a tenant under its budget.
|
|
92
|
+
if (now < ledger.refusedUntil)
|
|
93
|
+
throw this.refuse(this.budgetMs, ledger.refusedUntil - now);
|
|
94
|
+
// Under the budget again once enough of the oldest work leaves the window.
|
|
95
|
+
let left = ledger.spentUs;
|
|
96
|
+
let under = now;
|
|
97
|
+
for (let i = ledger.head; i < ledger.pieces.length; i += 1) {
|
|
98
|
+
const spent = ledger.pieces[i];
|
|
99
|
+
left -= spent.us;
|
|
100
|
+
under = spent.at + BUDGET_WINDOW_MS;
|
|
101
|
+
if (left < this.budgetUs)
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
// It holds until a piece is charged: what the tenant starts now is refused.
|
|
105
|
+
ledger.refusedUntil = under;
|
|
106
|
+
throw this.refuse(this.budgetMs, under - now);
|
|
107
|
+
}
|
|
108
|
+
/** Charges `tenant` for work started `at` that took `ms`. */
|
|
109
|
+
spend(tenant, at, ms) {
|
|
110
|
+
const ledger = this.ledgerOf(tenant);
|
|
111
|
+
const us = Math.round(ms * 1_000);
|
|
112
|
+
ledger.spentUs += us;
|
|
113
|
+
// A piece that started before a refusal can end after it, and move the wait.
|
|
114
|
+
ledger.refusedUntil = 0;
|
|
115
|
+
// Work ends in the order it started, but for a piece inside another one.
|
|
116
|
+
let i = ledger.pieces.length;
|
|
117
|
+
while (i > ledger.head && ledger.pieces[i - 1].at > at)
|
|
118
|
+
i -= 1;
|
|
119
|
+
ledger.pieces.splice(i, 0, { at, us });
|
|
120
|
+
}
|
|
121
|
+
/** Drops `ledger`'s work that left the window ending `now`; returns how many pieces are left. */
|
|
122
|
+
inWindow(ledger, now) {
|
|
123
|
+
const { pieces } = ledger;
|
|
124
|
+
while (ledger.head < pieces.length &&
|
|
125
|
+
pieces[ledger.head].at <= now - BUDGET_WINDOW_MS) {
|
|
126
|
+
ledger.spentUs -= pieces[ledger.head].us;
|
|
127
|
+
ledger.head += 1;
|
|
128
|
+
}
|
|
129
|
+
// Copied down once as many were dropped as are kept: each piece is copied about once.
|
|
130
|
+
if (ledger.head > 0 && ledger.head * 2 >= pieces.length) {
|
|
131
|
+
ledger.pieces = pieces.slice(ledger.head);
|
|
132
|
+
ledger.head = 0;
|
|
133
|
+
}
|
|
134
|
+
return ledger.pieces.length - ledger.head;
|
|
135
|
+
}
|
|
136
|
+
ledgerOf(tenant) {
|
|
137
|
+
let ledger = this.ledgers.get(tenant);
|
|
138
|
+
if (ledger === undefined) {
|
|
139
|
+
ledger = { pieces: [], head: 0, spentUs: 0, refusedUntil: 0 };
|
|
140
|
+
this.ledgers.set(tenant, ledger);
|
|
141
|
+
}
|
|
142
|
+
return ledger;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"budget.js","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { HOSTED_BOUNDS, SELF_HOSTED_BOUNDS, type TenantInputBounds, configureTenantInputBounds, tenantInputBounds, } from './bounds.js';
|
|
2
|
+
export { BUDGET_WINDOW_MS, type BudgetClock, SYSTEM_CLOCK, TenantBudgetError, type TenantBudgetLimit, TenantTimeBudget, } from './budget.js';
|
|
3
|
+
export { type CompiledTenantPattern, type TenantPattern, compileTenantPattern, tenantPatternSyntaxProblem, } from './pattern.js';
|
|
4
|
+
export { type RunAsTenantOptions, type TenantCompiles, type TenantScope, currentTenant, runAsTenant, runOutsideTenant, } from './tenant.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,0BAA0B,EAC1B,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,KAAK,WAAW,EAChB,YAAY,EACZ,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,oBAAoB,EACpB,0BAA0B,GAC3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,aAAa,EACb,WAAW,EACX,gBAAgB,GACjB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
export { HOSTED_BOUNDS, SELF_HOSTED_BOUNDS, configureTenantInputBounds, tenantInputBounds, } from './bounds.js';
|
|
4
|
+
export { BUDGET_WINDOW_MS, SYSTEM_CLOCK, TenantBudgetError, TenantTimeBudget, } from './budget.js';
|
|
5
|
+
export { compileTenantPattern, tenantPatternSyntaxProblem, } from './pattern.js';
|
|
6
|
+
export { currentTenant, runAsTenant, runOutsideTenant, } from './tenant.js';
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC,OAAO,EACL,aAAa,EACb,kBAAkB,EAElB,0BAA0B,EAC1B,iBAAiB,GAClB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAEhB,YAAY,EACZ,iBAAiB,EAEjB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAGL,oBAAoB,EACpB,0BAA0B,GAC3B,MAAM,cAAc,CAAC;AACtB,OAAO,EAIL,aAAa,EACb,WAAW,EACX,gBAAgB,GACjB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tenant's pattern, compiled for the engine the process's bounds name.
|
|
3
|
+
* Both answers are those of the JavaScript `RegExp` it was written as,
|
|
4
|
+
* starting from the beginning of `input` each time (`lastIndex` 0).
|
|
5
|
+
*/
|
|
6
|
+
export interface TenantPattern {
|
|
7
|
+
/** Whether it matches in `input`: anywhere, or at the start under the `y` flag. */
|
|
8
|
+
test(input: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* How many times it matches in `input`, one after another without
|
|
11
|
+
* overlapping, as `input.match()` with the `g` flag counts them. It stops
|
|
12
|
+
* counting at `atMost`: a caller that needs "at least n" asks for n, so a
|
|
13
|
+
* long input costs no more than that.
|
|
14
|
+
*/
|
|
15
|
+
count(input: string, atMost?: number): number;
|
|
16
|
+
}
|
|
17
|
+
export type CompiledTenantPattern = {
|
|
18
|
+
readonly kind: 'ok';
|
|
19
|
+
readonly pattern: TenantPattern;
|
|
20
|
+
}
|
|
21
|
+
/** Why it can't run here: a fragment that reads after a colon, with no full stop. */
|
|
22
|
+
| {
|
|
23
|
+
readonly kind: 'refused';
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Compiles a pattern a tenant wrote (a JavaScript regular expression's
|
|
28
|
+
* source and flags) under the process's bounds: no longer than
|
|
29
|
+
* `patternMaxLength` with its flags, on V8's engine or on RE2's.
|
|
30
|
+
*
|
|
31
|
+
* Under RE2 (the hosted profile), a pattern runs in linear time in its
|
|
32
|
+
* input. The price is what RE2 doesn't have, refused by name:
|
|
33
|
+
* backreferences, lookahead and lookbehind. A few things also read
|
|
34
|
+
* differently, the same way for every pattern (the package README lists
|
|
35
|
+
* them): RE2 always matches by code point and folds case as the `u` flag
|
|
36
|
+
* does; `.` stops only at `\n`, and with `m`, `^` and `$` only around
|
|
37
|
+
* `\n`; `\s` is ASCII whitespace. What RE2 would silently read otherwise
|
|
38
|
+
* is refused: `[]` and `[^]` (the start of a class to RE2), `[:` in a
|
|
39
|
+
* class (a POSIX class to RE2) and, without the `u` flag, the escapes
|
|
40
|
+
* JavaScript reads as the letter itself (`\p`, `\P`, `\A`, `\z`, `\Q`,
|
|
41
|
+
* `\a`, `\x{`). So is `\u`, which RE2 doesn't have. And it accepts only
|
|
42
|
+
* what `RegExp` accepts: a pattern V8 refuses is refused in V8's words, even
|
|
43
|
+
* where RE2 would compile it (`(?i)`, `(?P<n>…)`).
|
|
44
|
+
*
|
|
45
|
+
* Flags under RE2: `i`, `m`, `s` and `u` map to RE2's own; `y` anchors at
|
|
46
|
+
* the start as it does from `lastIndex` 0; `g` and `d` change no answer
|
|
47
|
+
* here. `v` is refused: RE2 has no set notation.
|
|
48
|
+
*/
|
|
49
|
+
export declare function compileTenantPattern(source: string, flags?: string): CompiledTenantPattern;
|
|
50
|
+
/**
|
|
51
|
+
* Why `compileTenantPattern` would refuse `source` with `flags` for its
|
|
52
|
+
* syntax, or `undefined` when it wouldn't: over `patternMaxLength`, what
|
|
53
|
+
* `RegExp` refuses and, under RE2, a flag or a construct RE2 doesn't have
|
|
54
|
+
* or reads otherwise. It builds no RE2 program, so it costs about what
|
|
55
|
+
* V8's parse does. A pattern RE2 refuses only as it builds the program
|
|
56
|
+
* passes here, and is refused when it's compiled: one too large (a
|
|
57
|
+
* repeat over 1,000, say), or with an escape RE2 doesn't have that this
|
|
58
|
+
* check doesn't name (`\N`, `[\b]`, `\cA`, a long property name such as
|
|
59
|
+
* `\p{Letter}`).
|
|
60
|
+
*/
|
|
61
|
+
export declare function tenantPatternSyntaxProblem(source: string, flags?: string): string | undefined;
|
|
62
|
+
//# sourceMappingURL=pattern.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern.d.ts","sourceRoot":"","sources":["../src/pattern.ts"],"names":[],"mappings":"AAOA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,mFAAmF;IACnF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC/C;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;CAAE;AAC1D,qFAAqF;GACnF;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,qBAAqB,CAKtF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,GAAG,SAAS,CASzF"}
|
package/dist/pattern.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
import { RE2JS, RE2JSException, RE2JSSyntaxException } from 're2js';
|
|
4
|
+
import { tenantInputBounds } from './bounds.js';
|
|
5
|
+
/**
|
|
6
|
+
* Compiles a pattern a tenant wrote (a JavaScript regular expression's
|
|
7
|
+
* source and flags) under the process's bounds: no longer than
|
|
8
|
+
* `patternMaxLength` with its flags, on V8's engine or on RE2's.
|
|
9
|
+
*
|
|
10
|
+
* Under RE2 (the hosted profile), a pattern runs in linear time in its
|
|
11
|
+
* input. The price is what RE2 doesn't have, refused by name:
|
|
12
|
+
* backreferences, lookahead and lookbehind. A few things also read
|
|
13
|
+
* differently, the same way for every pattern (the package README lists
|
|
14
|
+
* them): RE2 always matches by code point and folds case as the `u` flag
|
|
15
|
+
* does; `.` stops only at `\n`, and with `m`, `^` and `$` only around
|
|
16
|
+
* `\n`; `\s` is ASCII whitespace. What RE2 would silently read otherwise
|
|
17
|
+
* is refused: `[]` and `[^]` (the start of a class to RE2), `[:` in a
|
|
18
|
+
* class (a POSIX class to RE2) and, without the `u` flag, the escapes
|
|
19
|
+
* JavaScript reads as the letter itself (`\p`, `\P`, `\A`, `\z`, `\Q`,
|
|
20
|
+
* `\a`, `\x{`). So is `\u`, which RE2 doesn't have. And it accepts only
|
|
21
|
+
* what `RegExp` accepts: a pattern V8 refuses is refused in V8's words, even
|
|
22
|
+
* where RE2 would compile it (`(?i)`, `(?P<n>…)`).
|
|
23
|
+
*
|
|
24
|
+
* Flags under RE2: `i`, `m`, `s` and `u` map to RE2's own; `y` anchors at
|
|
25
|
+
* the start as it does from `lastIndex` 0; `g` and `d` change no answer
|
|
26
|
+
* here. `v` is refused: RE2 has no set notation.
|
|
27
|
+
*/
|
|
28
|
+
export function compileTenantPattern(source, flags = '') {
|
|
29
|
+
const bounds = tenantInputBounds();
|
|
30
|
+
const tooLong = lengthProblem(source, flags);
|
|
31
|
+
if (tooLong !== undefined)
|
|
32
|
+
return { kind: 'refused', reason: tooLong };
|
|
33
|
+
return bounds.regex === 're2' ? compileRe2(source, flags) : compileV8(source, flags);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Why `compileTenantPattern` would refuse `source` with `flags` for its
|
|
37
|
+
* syntax, or `undefined` when it wouldn't: over `patternMaxLength`, what
|
|
38
|
+
* `RegExp` refuses and, under RE2, a flag or a construct RE2 doesn't have
|
|
39
|
+
* or reads otherwise. It builds no RE2 program, so it costs about what
|
|
40
|
+
* V8's parse does. A pattern RE2 refuses only as it builds the program
|
|
41
|
+
* passes here, and is refused when it's compiled: one too large (a
|
|
42
|
+
* repeat over 1,000, say), or with an escape RE2 doesn't have that this
|
|
43
|
+
* check doesn't name (`\N`, `[\b]`, `\cA`, a long property name such as
|
|
44
|
+
* `\p{Letter}`).
|
|
45
|
+
*/
|
|
46
|
+
export function tenantPatternSyntaxProblem(source, flags = '') {
|
|
47
|
+
const tooLong = lengthProblem(source, flags);
|
|
48
|
+
if (tooLong !== undefined)
|
|
49
|
+
return tooLong;
|
|
50
|
+
if (tenantInputBounds().regex !== 're2') {
|
|
51
|
+
const parsed = parse(source, flags);
|
|
52
|
+
return typeof parsed === 'string' ? parsed : undefined;
|
|
53
|
+
}
|
|
54
|
+
const re2Flags = re2FlagsFor(flags);
|
|
55
|
+
return typeof re2Flags === 'string' ? re2Flags : re2SyntaxProblem(source, flags);
|
|
56
|
+
}
|
|
57
|
+
/** Why `source` with `flags` is over `patternMaxLength`, or `undefined`. */
|
|
58
|
+
function lengthProblem(source, flags) {
|
|
59
|
+
const max = tenantInputBounds().patternMaxLength;
|
|
60
|
+
const length = source.length + flags.length;
|
|
61
|
+
return length > max ? `${length} characters with its flags, over the limit of ${max}` : undefined;
|
|
62
|
+
}
|
|
63
|
+
/** `source` with `flags` as V8 parses it, or V8's reason for refusing it. */
|
|
64
|
+
function parse(source, flags) {
|
|
65
|
+
try {
|
|
66
|
+
return new RegExp(source, flags);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return error.message;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function compileV8(source, flags) {
|
|
73
|
+
const once = parse(source, flags);
|
|
74
|
+
if (typeof once === 'string')
|
|
75
|
+
return { kind: 'refused', reason: once };
|
|
76
|
+
const every = flags.includes('g') ? once : new RegExp(source, `${flags}g`);
|
|
77
|
+
const byCodePoint = flags.includes('u') || flags.includes('v');
|
|
78
|
+
return {
|
|
79
|
+
kind: 'ok',
|
|
80
|
+
pattern: {
|
|
81
|
+
test(input) {
|
|
82
|
+
once.lastIndex = 0;
|
|
83
|
+
return once.test(input);
|
|
84
|
+
},
|
|
85
|
+
count(input, atMost = Number.POSITIVE_INFINITY) {
|
|
86
|
+
every.lastIndex = 0;
|
|
87
|
+
let found = 0;
|
|
88
|
+
while (found < atMost) {
|
|
89
|
+
const match = every.exec(input);
|
|
90
|
+
if (match === null)
|
|
91
|
+
break;
|
|
92
|
+
found += 1;
|
|
93
|
+
// As `match()` steps past an empty match, so the next one can't be the same.
|
|
94
|
+
if (match[0] === '')
|
|
95
|
+
every.lastIndex = after(input, every.lastIndex, byCodePoint);
|
|
96
|
+
}
|
|
97
|
+
return found;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const RE2_ENGINE = 'RE2, the engine patterns run on here,';
|
|
103
|
+
const RE2_FLAGS = {
|
|
104
|
+
i: RE2JS.CASE_INSENSITIVE,
|
|
105
|
+
m: RE2JS.MULTILINE,
|
|
106
|
+
s: RE2JS.DOTALL,
|
|
107
|
+
u: 0,
|
|
108
|
+
g: 0,
|
|
109
|
+
d: 0,
|
|
110
|
+
y: 0,
|
|
111
|
+
};
|
|
112
|
+
/** RE2's flags for `flags`, or why there are none. */
|
|
113
|
+
function re2FlagsFor(flags) {
|
|
114
|
+
let re2Flags = 0;
|
|
115
|
+
for (const [index, flag] of [...flags].entries()) {
|
|
116
|
+
if (flags.indexOf(flag) !== index)
|
|
117
|
+
return `the \`${flag}\` flag is given twice`;
|
|
118
|
+
if (flag === 'v')
|
|
119
|
+
return `${RE2_ENGINE} has no \`v\` flag (set notation)`;
|
|
120
|
+
const mapped = RE2_FLAGS[flag];
|
|
121
|
+
if (mapped === undefined)
|
|
122
|
+
return `\`${flag}\` isn't a flag`;
|
|
123
|
+
re2Flags |= mapped;
|
|
124
|
+
}
|
|
125
|
+
return re2Flags;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Why RE2 can't run `source` with `flags` as JavaScript reads it, for its
|
|
129
|
+
* syntax; `undefined` when it can. What `RegExp` accepts, never more: RE2
|
|
130
|
+
* compiles `(?i)`, `(?P<n>…)` and, with `u`, `\pL` or `\x{41}`, which V8
|
|
131
|
+
* refuses. Constructing it only parses (it never matches), and the pattern
|
|
132
|
+
* is already within its length.
|
|
133
|
+
*/
|
|
134
|
+
function re2SyntaxProblem(source, flags) {
|
|
135
|
+
const parsed = parse(source, flags);
|
|
136
|
+
if (typeof parsed === 'string')
|
|
137
|
+
return parsed;
|
|
138
|
+
return unsupportedOnRe2(source, flags.includes('u'));
|
|
139
|
+
}
|
|
140
|
+
function compileRe2(source, flags) {
|
|
141
|
+
const re2Flags = re2FlagsFor(flags);
|
|
142
|
+
if (typeof re2Flags === 'string')
|
|
143
|
+
return { kind: 'refused', reason: re2Flags };
|
|
144
|
+
const problem = re2SyntaxProblem(source, flags);
|
|
145
|
+
if (problem !== undefined)
|
|
146
|
+
return { kind: 'refused', reason: problem };
|
|
147
|
+
let re;
|
|
148
|
+
try {
|
|
149
|
+
re = RE2JS.compile(source, re2Flags);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (!(error instanceof RE2JSException))
|
|
153
|
+
throw error;
|
|
154
|
+
const detail = error instanceof RE2JSSyntaxException
|
|
155
|
+
? error.message.replace(/^error parsing regexp: /, '')
|
|
156
|
+
: error.message;
|
|
157
|
+
return { kind: 'refused', reason: `${RE2_ENGINE} refuses it: ${detail}` };
|
|
158
|
+
}
|
|
159
|
+
const sticky = flags.includes('y');
|
|
160
|
+
return {
|
|
161
|
+
kind: 'ok',
|
|
162
|
+
pattern: {
|
|
163
|
+
test(input) {
|
|
164
|
+
return sticky ? re.matcher(input).lookingAt() : re.test(input);
|
|
165
|
+
},
|
|
166
|
+
count(input, atMost = Number.POSITIVE_INFINITY) {
|
|
167
|
+
const matcher = re.matcher(input);
|
|
168
|
+
let found = 0;
|
|
169
|
+
let from = 0;
|
|
170
|
+
while (found < atMost && from <= input.length && matcher.find(from)) {
|
|
171
|
+
const start = matcher.start();
|
|
172
|
+
const end = matcher.end();
|
|
173
|
+
if (sticky && start !== from)
|
|
174
|
+
break;
|
|
175
|
+
found += 1;
|
|
176
|
+
from = end === start ? after(input, end, true) : end;
|
|
177
|
+
}
|
|
178
|
+
return found;
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* What RE2 doesn't have, or reads differently from JavaScript, by name
|
|
185
|
+
* and where it is; `undefined` when there's none. RE2's own messages
|
|
186
|
+
* would call a lookbehind an "invalid named capture". It runs on a
|
|
187
|
+
* pattern `RegExp` has already parsed with the same flags (`unicode`: `u`).
|
|
188
|
+
*/
|
|
189
|
+
function unsupportedOnRe2(source, unicode) {
|
|
190
|
+
let inClass = false;
|
|
191
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
192
|
+
const c = source[i];
|
|
193
|
+
if (c === '\\') {
|
|
194
|
+
const next = source[i + 1] ?? '';
|
|
195
|
+
if (next === 'u') {
|
|
196
|
+
return `${RE2_ENGINE} has no \`\\u\` escapes: write the character itself (at index ${i})`;
|
|
197
|
+
}
|
|
198
|
+
const misread = unicode ? undefined : RE2_READS_OTHERWISE[next];
|
|
199
|
+
if (misread !== undefined && (next !== 'x' || source[i + 2] === '{')) {
|
|
200
|
+
const written = next === 'x' ? '\\x{' : `\\${next}`;
|
|
201
|
+
return `${RE2_ENGINE} reads \`${written}\` as ${misread}, where JavaScript without the \`u\` flag reads the letter \`${next}\` (at index ${i})`;
|
|
202
|
+
}
|
|
203
|
+
if (!inClass && next >= '1' && next <= '9') {
|
|
204
|
+
return `${RE2_ENGINE} has no backreferences (\`\\${next}\` at index ${i})`;
|
|
205
|
+
}
|
|
206
|
+
if (!inClass && next === 'k' && source[i + 2] === '<') {
|
|
207
|
+
return `${RE2_ENGINE} has no backreferences (\`\\k<\` at index ${i})`;
|
|
208
|
+
}
|
|
209
|
+
i += 1;
|
|
210
|
+
}
|
|
211
|
+
else if (inClass) {
|
|
212
|
+
if (c === ']')
|
|
213
|
+
inClass = false;
|
|
214
|
+
else if (c === '[' && source[i + 1] === ':') {
|
|
215
|
+
return `${RE2_ENGINE} reads \`[:\` in a class as a POSIX class such as \`[:alpha:]\`, not as JavaScript does (at index ${i})`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (c === '[') {
|
|
219
|
+
const first = source[i + 1] === '^' ? i + 2 : i + 1;
|
|
220
|
+
if (source[first] === ']') {
|
|
221
|
+
return `${RE2_ENGINE} reads \`${source.slice(i, first + 1)}\` as the start of a class, not as JavaScript does (at index ${i})`;
|
|
222
|
+
}
|
|
223
|
+
inClass = true;
|
|
224
|
+
i = first - 1;
|
|
225
|
+
}
|
|
226
|
+
else if (c === '(' && source[i + 1] === '?') {
|
|
227
|
+
const look = LOOKAROUND.find((l) => source.startsWith(l, i));
|
|
228
|
+
if (look !== undefined) {
|
|
229
|
+
return `${RE2_ENGINE} has no lookahead or lookbehind (\`${look}\` at index ${i})`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
const LOOKAROUND = ['(?=', '(?!', '(?<=', '(?<!'];
|
|
236
|
+
/** Escapes JavaScript without `u` reads as the letter itself, and RE2 as something else. */
|
|
237
|
+
const RE2_READS_OTHERWISE = {
|
|
238
|
+
p: 'a Unicode class',
|
|
239
|
+
P: 'a negated Unicode class',
|
|
240
|
+
A: 'the start of the text',
|
|
241
|
+
z: 'the end of the text',
|
|
242
|
+
Q: 'the start of a quoted literal (`\\Q…\\E`)',
|
|
243
|
+
a: 'the bell character',
|
|
244
|
+
x: 'the start of a code point (`\\x{41}`)',
|
|
245
|
+
};
|
|
246
|
+
/** The index after an empty match at `index`: past a whole surrogate pair when matching by code point. */
|
|
247
|
+
function after(input, index, byCodePoint) {
|
|
248
|
+
if (!byCodePoint)
|
|
249
|
+
return index + 1;
|
|
250
|
+
const code = input.codePointAt(index);
|
|
251
|
+
return index + (code !== undefined && code > 0xffff ? 2 : 1);
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=pattern.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern.js","sourceRoot":"","sources":["../src/pattern.ts"],"names":[],"mappings":"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"}
|
package/dist/tenant.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of a tenant's schema compiles count against its budget.
|
|
3
|
+
* `recompiles`: compiling a schema again, after the cache dropped it; the
|
|
4
|
+
* first compile of a schema the tenant published doesn't count, so a
|
|
5
|
+
* restarted server compiles each tenant's schemas freely. `all`: every
|
|
6
|
+
* compile, for a request that publishes, deploys or registers schemas,
|
|
7
|
+
* which is where new ones come from.
|
|
8
|
+
*/
|
|
9
|
+
export type TenantCompiles = 'recompiles' | 'all';
|
|
10
|
+
/** The tenant whose work is running, and which of its compiles count. */
|
|
11
|
+
export interface TenantScope {
|
|
12
|
+
readonly tenantId: string;
|
|
13
|
+
readonly compiles: TenantCompiles;
|
|
14
|
+
}
|
|
15
|
+
/** `runAsTenant`'s options. */
|
|
16
|
+
export interface RunAsTenantOptions {
|
|
17
|
+
/** Which of the tenant's compiles count: `recompiles` unless set. */
|
|
18
|
+
readonly compiles?: TenantCompiles;
|
|
19
|
+
}
|
|
20
|
+
/** Runs `fn` as `tenantId`'s work, and returns what it returns. */
|
|
21
|
+
export declare function runAsTenant<T>(tenantId: string, fn: () => T, options?: RunAsTenantOptions): T;
|
|
22
|
+
/**
|
|
23
|
+
* Runs `fn` outside any tenant's scope, and returns what it returns: for
|
|
24
|
+
* Kindgi's own work that a tenant's request happens to start (compiling
|
|
25
|
+
* one of Kindgi's schemas the first time, say), which no tenant is
|
|
26
|
+
* charged for.
|
|
27
|
+
*/
|
|
28
|
+
export declare function runOutsideTenant<T>(fn: () => T): T;
|
|
29
|
+
/** Whose work this is, or undefined outside `runAsTenant`. */
|
|
30
|
+
export declare function currentTenant(): TenantScope | undefined;
|
|
31
|
+
//# sourceMappingURL=tenant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAiBA;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,KAAK,CAAC;AAElD,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;CACnC;AAED,+BAA+B;AAC/B,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;CACpC;AAID,mEAAmE;AACnE,wBAAgB,WAAW,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,OAAO,GAAE,kBAAuB,GAAG,CAAC,CAEjG;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAElD;AAED,8DAA8D;AAC9D,wBAAgB,aAAa,IAAI,WAAW,GAAG,SAAS,CAEvD"}
|
package/dist/tenant.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* Whose work this is: the tenant a budget kept per tenant charges
|
|
5
|
+
* (`schemaCompileBudgetMs`, `templateRenderBudgetMs`), and which of its
|
|
6
|
+
* compiles count. The API runs each authenticated request with
|
|
7
|
+
* `runAsTenant`, and a request that publishes schemas in a scope of its
|
|
8
|
+
* own that counts every compile; a server that does a tenant's work
|
|
9
|
+
* outside a request (a run, a job) wraps it the same way. The scope is an
|
|
10
|
+
* `AsyncLocalStorage` context: it follows `fn`'s own awaits and callbacks,
|
|
11
|
+
* never another request's, however they interleave. Outside any scope
|
|
12
|
+
* (boot, Kindgi's own work), nothing is charged.
|
|
13
|
+
*/
|
|
14
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
15
|
+
const scopes = new AsyncLocalStorage();
|
|
16
|
+
/** Runs `fn` as `tenantId`'s work, and returns what it returns. */
|
|
17
|
+
export function runAsTenant(tenantId, fn, options = {}) {
|
|
18
|
+
return scopes.run({ tenantId, compiles: options.compiles ?? 'recompiles' }, fn);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Runs `fn` outside any tenant's scope, and returns what it returns: for
|
|
22
|
+
* Kindgi's own work that a tenant's request happens to start (compiling
|
|
23
|
+
* one of Kindgi's schemas the first time, say), which no tenant is
|
|
24
|
+
* charged for.
|
|
25
|
+
*/
|
|
26
|
+
export function runOutsideTenant(fn) {
|
|
27
|
+
return scopes.exit(fn);
|
|
28
|
+
}
|
|
29
|
+
/** Whose work this is, or undefined outside `runAsTenant`. */
|
|
30
|
+
export function currentTenant() {
|
|
31
|
+
return scopes.getStore();
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=tenant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenant.js","sourceRoot":"","sources":["../src/tenant.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAwBrD,MAAM,MAAM,GAAG,IAAI,iBAAiB,EAAe,CAAC;AAEpD,mEAAmE;AACnE,MAAM,UAAU,WAAW,CAAI,QAAgB,EAAE,EAAW,EAAE,UAA8B,EAAE;IAC5F,OAAO,MAAM,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,YAAY,EAAE,EAAE,EAAE,CAAC,CAAC;AAClF,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAI,EAAW;IAC7C,OAAO,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACzB,CAAC;AAED,8DAA8D;AAC9D,MAAM,UAAU,aAAa;IAC3B,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;AAC3B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (C) 2026 Kindgi Inc.
|
|
3
|
+
/**
|
|
4
|
+
* For tests: run with other bounds than the process's, then put them back.
|
|
5
|
+
* Never in a server: its bounds are set once, at boot.
|
|
6
|
+
*/
|
|
7
|
+
export { overrideTenantInputBounds } from './bounds.js';
|
|
8
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iCAAiC;AAEjC;;;GAGG;AACH,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC"}
|