@kindgi/bounds 0.0.0-stage → 0.1.8

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/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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"}
@@ -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"}
@@ -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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"}
@@ -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,6 @@
1
+ /**
2
+ * For tests: run with other bounds than the process's, then put them back.
3
+ * Never in a server: its bounds are set once, at boot.
4
+ */
5
+ export { overrideTenantInputBounds } from './bounds.js';
6
+ //# sourceMappingURL=testing.d.ts.map
@@ -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"}
@@ -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"}