@lenso/limits 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +352 -2
- package/dist/config.d.ts +5 -0
- package/dist/contracts.d.ts +60 -0
- package/dist/d1.d.ts +546 -0
- package/dist/d1.js +164 -0
- package/dist/index-bz4qnpx8.js +138 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +321 -0
- package/dist/memory.d.ts +5 -0
- package/dist/postgres.d.ts +211 -0
- package/dist/postgres.js +128 -0
- package/dist/service.d.ts +27 -0
- package/dist/sqlite.d.ts +426 -0
- package/dist/sqlite.js +107 -0
- package/dist/state.d.ts +25 -0
- package/migrations/0001_d1.sql +34 -0
- package/migrations/0001_postgres.sql +23 -0
- package/migrations/0001_sqlite.sql +23 -0
- package/package.json +58 -4
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
2
|
+
import { type CounterKind, type LimitStore, type Lease } from "./contracts";
|
|
3
|
+
export declare const postgresLimitSchema: {
|
|
4
|
+
counters: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
5
|
+
name: "lenso_limit_counters";
|
|
6
|
+
schema: undefined;
|
|
7
|
+
columns: {
|
|
8
|
+
scope: import("drizzle-orm/pg-core").PgColumn<{
|
|
9
|
+
name: "scope";
|
|
10
|
+
tableName: "lenso_limit_counters";
|
|
11
|
+
dataType: "string";
|
|
12
|
+
columnType: "PgText";
|
|
13
|
+
data: string;
|
|
14
|
+
driverParam: string;
|
|
15
|
+
notNull: true;
|
|
16
|
+
hasDefault: false;
|
|
17
|
+
isPrimaryKey: false;
|
|
18
|
+
isAutoincrement: false;
|
|
19
|
+
hasRuntimeDefault: false;
|
|
20
|
+
enumValues: [string, ...string[]];
|
|
21
|
+
baseColumn: never;
|
|
22
|
+
identity: undefined;
|
|
23
|
+
generated: undefined;
|
|
24
|
+
}, {}, {}>;
|
|
25
|
+
kind: import("drizzle-orm/pg-core").PgColumn<{
|
|
26
|
+
name: "kind";
|
|
27
|
+
tableName: "lenso_limit_counters";
|
|
28
|
+
dataType: "string";
|
|
29
|
+
columnType: "PgText";
|
|
30
|
+
data: CounterKind;
|
|
31
|
+
driverParam: string;
|
|
32
|
+
notNull: true;
|
|
33
|
+
hasDefault: false;
|
|
34
|
+
isPrimaryKey: false;
|
|
35
|
+
isAutoincrement: false;
|
|
36
|
+
hasRuntimeDefault: false;
|
|
37
|
+
enumValues: [string, ...string[]];
|
|
38
|
+
baseColumn: never;
|
|
39
|
+
identity: undefined;
|
|
40
|
+
generated: undefined;
|
|
41
|
+
}, {}, {
|
|
42
|
+
$type: CounterKind;
|
|
43
|
+
}>;
|
|
44
|
+
capacity: import("drizzle-orm/pg-core").PgColumn<{
|
|
45
|
+
name: "capacity";
|
|
46
|
+
tableName: "lenso_limit_counters";
|
|
47
|
+
dataType: "number";
|
|
48
|
+
columnType: "PgBigInt53";
|
|
49
|
+
data: number;
|
|
50
|
+
driverParam: string | number;
|
|
51
|
+
notNull: true;
|
|
52
|
+
hasDefault: false;
|
|
53
|
+
isPrimaryKey: false;
|
|
54
|
+
isAutoincrement: false;
|
|
55
|
+
hasRuntimeDefault: false;
|
|
56
|
+
enumValues: undefined;
|
|
57
|
+
baseColumn: never;
|
|
58
|
+
identity: undefined;
|
|
59
|
+
generated: undefined;
|
|
60
|
+
}, {}, {}>;
|
|
61
|
+
periodMs: import("drizzle-orm/pg-core").PgColumn<{
|
|
62
|
+
name: "period_ms";
|
|
63
|
+
tableName: "lenso_limit_counters";
|
|
64
|
+
dataType: "number";
|
|
65
|
+
columnType: "PgBigInt53";
|
|
66
|
+
data: number;
|
|
67
|
+
driverParam: string | number;
|
|
68
|
+
notNull: true;
|
|
69
|
+
hasDefault: false;
|
|
70
|
+
isPrimaryKey: false;
|
|
71
|
+
isAutoincrement: false;
|
|
72
|
+
hasRuntimeDefault: false;
|
|
73
|
+
enumValues: undefined;
|
|
74
|
+
baseColumn: never;
|
|
75
|
+
identity: undefined;
|
|
76
|
+
generated: undefined;
|
|
77
|
+
}, {}, {}>;
|
|
78
|
+
windowStart: import("drizzle-orm/pg-core").PgColumn<{
|
|
79
|
+
name: "window_start";
|
|
80
|
+
tableName: "lenso_limit_counters";
|
|
81
|
+
dataType: "number";
|
|
82
|
+
columnType: "PgBigInt53";
|
|
83
|
+
data: number;
|
|
84
|
+
driverParam: string | number;
|
|
85
|
+
notNull: true;
|
|
86
|
+
hasDefault: false;
|
|
87
|
+
isPrimaryKey: false;
|
|
88
|
+
isAutoincrement: false;
|
|
89
|
+
hasRuntimeDefault: false;
|
|
90
|
+
enumValues: undefined;
|
|
91
|
+
baseColumn: never;
|
|
92
|
+
identity: undefined;
|
|
93
|
+
generated: undefined;
|
|
94
|
+
}, {}, {}>;
|
|
95
|
+
used: import("drizzle-orm/pg-core").PgColumn<{
|
|
96
|
+
name: "used";
|
|
97
|
+
tableName: "lenso_limit_counters";
|
|
98
|
+
dataType: "number";
|
|
99
|
+
columnType: "PgBigInt53";
|
|
100
|
+
data: number;
|
|
101
|
+
driverParam: string | number;
|
|
102
|
+
notNull: true;
|
|
103
|
+
hasDefault: false;
|
|
104
|
+
isPrimaryKey: false;
|
|
105
|
+
isAutoincrement: false;
|
|
106
|
+
hasRuntimeDefault: false;
|
|
107
|
+
enumValues: undefined;
|
|
108
|
+
baseColumn: never;
|
|
109
|
+
identity: undefined;
|
|
110
|
+
generated: undefined;
|
|
111
|
+
}, {}, {}>;
|
|
112
|
+
lastNow: import("drizzle-orm/pg-core").PgColumn<{
|
|
113
|
+
name: "last_now";
|
|
114
|
+
tableName: "lenso_limit_counters";
|
|
115
|
+
dataType: "number";
|
|
116
|
+
columnType: "PgBigInt53";
|
|
117
|
+
data: number;
|
|
118
|
+
driverParam: string | number;
|
|
119
|
+
notNull: true;
|
|
120
|
+
hasDefault: false;
|
|
121
|
+
isPrimaryKey: false;
|
|
122
|
+
isAutoincrement: false;
|
|
123
|
+
hasRuntimeDefault: false;
|
|
124
|
+
enumValues: undefined;
|
|
125
|
+
baseColumn: never;
|
|
126
|
+
identity: undefined;
|
|
127
|
+
generated: undefined;
|
|
128
|
+
}, {}, {}>;
|
|
129
|
+
};
|
|
130
|
+
dialect: 'pg';
|
|
131
|
+
}>;
|
|
132
|
+
concurrencyBuckets: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
133
|
+
name: "lenso_limit_concurrency";
|
|
134
|
+
schema: undefined;
|
|
135
|
+
columns: {
|
|
136
|
+
scope: import("drizzle-orm/pg-core").PgColumn<{
|
|
137
|
+
name: "scope";
|
|
138
|
+
tableName: "lenso_limit_concurrency";
|
|
139
|
+
dataType: "string";
|
|
140
|
+
columnType: "PgText";
|
|
141
|
+
data: string;
|
|
142
|
+
driverParam: string;
|
|
143
|
+
notNull: true;
|
|
144
|
+
hasDefault: false;
|
|
145
|
+
isPrimaryKey: true;
|
|
146
|
+
isAutoincrement: false;
|
|
147
|
+
hasRuntimeDefault: false;
|
|
148
|
+
enumValues: [string, ...string[]];
|
|
149
|
+
baseColumn: never;
|
|
150
|
+
identity: undefined;
|
|
151
|
+
generated: undefined;
|
|
152
|
+
}, {}, {}>;
|
|
153
|
+
capacity: import("drizzle-orm/pg-core").PgColumn<{
|
|
154
|
+
name: "capacity";
|
|
155
|
+
tableName: "lenso_limit_concurrency";
|
|
156
|
+
dataType: "number";
|
|
157
|
+
columnType: "PgBigInt53";
|
|
158
|
+
data: number;
|
|
159
|
+
driverParam: string | number;
|
|
160
|
+
notNull: true;
|
|
161
|
+
hasDefault: false;
|
|
162
|
+
isPrimaryKey: false;
|
|
163
|
+
isAutoincrement: false;
|
|
164
|
+
hasRuntimeDefault: false;
|
|
165
|
+
enumValues: undefined;
|
|
166
|
+
baseColumn: never;
|
|
167
|
+
identity: undefined;
|
|
168
|
+
generated: undefined;
|
|
169
|
+
}, {}, {}>;
|
|
170
|
+
lastNow: import("drizzle-orm/pg-core").PgColumn<{
|
|
171
|
+
name: "last_now";
|
|
172
|
+
tableName: "lenso_limit_concurrency";
|
|
173
|
+
dataType: "number";
|
|
174
|
+
columnType: "PgBigInt53";
|
|
175
|
+
data: number;
|
|
176
|
+
driverParam: string | number;
|
|
177
|
+
notNull: true;
|
|
178
|
+
hasDefault: false;
|
|
179
|
+
isPrimaryKey: false;
|
|
180
|
+
isAutoincrement: false;
|
|
181
|
+
hasRuntimeDefault: false;
|
|
182
|
+
enumValues: undefined;
|
|
183
|
+
baseColumn: never;
|
|
184
|
+
identity: undefined;
|
|
185
|
+
generated: undefined;
|
|
186
|
+
}, {}, {}>;
|
|
187
|
+
leases: import("drizzle-orm/pg-core").PgColumn<{
|
|
188
|
+
name: "leases";
|
|
189
|
+
tableName: "lenso_limit_concurrency";
|
|
190
|
+
dataType: "json";
|
|
191
|
+
columnType: "PgJsonb";
|
|
192
|
+
data: Lease[];
|
|
193
|
+
driverParam: unknown;
|
|
194
|
+
notNull: true;
|
|
195
|
+
hasDefault: false;
|
|
196
|
+
isPrimaryKey: false;
|
|
197
|
+
isAutoincrement: false;
|
|
198
|
+
hasRuntimeDefault: false;
|
|
199
|
+
enumValues: undefined;
|
|
200
|
+
baseColumn: never;
|
|
201
|
+
identity: undefined;
|
|
202
|
+
generated: undefined;
|
|
203
|
+
}, {}, {
|
|
204
|
+
$type: Lease[];
|
|
205
|
+
}>;
|
|
206
|
+
};
|
|
207
|
+
dialect: 'pg';
|
|
208
|
+
}>;
|
|
209
|
+
};
|
|
210
|
+
/** Borrows the database; schema creation and connection lifetime belong to the caller. */
|
|
211
|
+
export declare function createPostgresLimitStore<TResult extends PgQueryResultHKT = PgQueryResultHKT, TSchema extends Record<string, unknown> = Record<string, unknown>>(db: PgDatabase<TResult, TSchema>): LimitStore;
|
package/dist/postgres.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LimitError2,
|
|
3
|
+
maxDurationMs,
|
|
4
|
+
positiveInteger,
|
|
5
|
+
scopeKey,
|
|
6
|
+
validateConsume,
|
|
7
|
+
validateAcquire,
|
|
8
|
+
validateToken,
|
|
9
|
+
validateKind,
|
|
10
|
+
consumeState,
|
|
11
|
+
liveState,
|
|
12
|
+
acquireState,
|
|
13
|
+
renewState
|
|
14
|
+
} from "./index-bz4qnpx8.js";
|
|
15
|
+
|
|
16
|
+
// src/postgres.ts
|
|
17
|
+
import { and, eq, sql } from "drizzle-orm";
|
|
18
|
+
import { check, jsonb, pgTable, primaryKey, text, bigint } from "drizzle-orm/pg-core";
|
|
19
|
+
var counters = pgTable("lenso_limit_counters", {
|
|
20
|
+
scope: text("scope").notNull(),
|
|
21
|
+
kind: text("kind").$type().notNull(),
|
|
22
|
+
capacity: bigint("capacity", { mode: "number" }).notNull(),
|
|
23
|
+
periodMs: bigint("period_ms", { mode: "number" }).notNull(),
|
|
24
|
+
windowStart: bigint("window_start", { mode: "number" }).notNull(),
|
|
25
|
+
used: bigint("used", { mode: "number" }).notNull(),
|
|
26
|
+
lastNow: bigint("last_now", { mode: "number" }).notNull()
|
|
27
|
+
}, (t) => [
|
|
28
|
+
primaryKey({ columns: [t.scope, t.kind], name: "lenso_limit_counters_pk" }),
|
|
29
|
+
check("lenso_limit_counter_kind", sql`${t.kind} IN ('rate', 'quota')`),
|
|
30
|
+
check("lenso_limit_counter_capacity", sql`${t.capacity} BETWEEN 1 AND 2147483647`),
|
|
31
|
+
check("lenso_limit_counter_period", sql`${t.periodMs} BETWEEN 1 AND 31622400000`),
|
|
32
|
+
check("lenso_limit_counter_used", sql`${t.used} BETWEEN 0 AND ${t.capacity}`),
|
|
33
|
+
check("lenso_limit_counter_clock", sql`${t.windowStart} >= 0 AND ${t.lastNow} >= 0`)
|
|
34
|
+
]);
|
|
35
|
+
var concurrencyBuckets = pgTable("lenso_limit_concurrency", {
|
|
36
|
+
scope: text("scope").primaryKey().notNull(),
|
|
37
|
+
capacity: bigint("capacity", { mode: "number" }).notNull(),
|
|
38
|
+
lastNow: bigint("last_now", { mode: "number" }).notNull(),
|
|
39
|
+
leases: jsonb("leases").$type().notNull()
|
|
40
|
+
}, (t) => [
|
|
41
|
+
check("lenso_limit_lease_capacity", sql`${t.capacity} BETWEEN 1 AND 2147483647`),
|
|
42
|
+
check("lenso_limit_lease_clock", sql`${t.lastNow} >= 0`)
|
|
43
|
+
]);
|
|
44
|
+
var postgresLimitSchema = { counters, concurrencyBuckets };
|
|
45
|
+
var dbNow = sql`floor(extract(epoch from clock_timestamp()) * 1000)::bigint`;
|
|
46
|
+
var serializedJson = (value) => sql`${JSON.stringify(value)}::text::jsonb`;
|
|
47
|
+
function createPostgresLimitStore(db) {
|
|
48
|
+
async function now(tx) {
|
|
49
|
+
const [row] = await tx.select({ now: dbNow }).from(sql`(SELECT 1) AS clock_source`);
|
|
50
|
+
return Number(row.now);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
async consume(kind, input) {
|
|
54
|
+
validateKind(kind);
|
|
55
|
+
validateConsume(input);
|
|
56
|
+
const key = scopeKey(input.scope);
|
|
57
|
+
return db.transaction(async (tx) => {
|
|
58
|
+
await tx.insert(counters).values({
|
|
59
|
+
scope: key,
|
|
60
|
+
kind,
|
|
61
|
+
capacity: input.capacity,
|
|
62
|
+
periodMs: input.periodMs,
|
|
63
|
+
windowStart: 0,
|
|
64
|
+
used: 0,
|
|
65
|
+
lastNow: 0
|
|
66
|
+
}).onConflictDoNothing();
|
|
67
|
+
const [previous] = await tx.select().from(counters).where(and(eq(counters.scope, key), eq(counters.kind, kind))).for("update");
|
|
68
|
+
if (!previous)
|
|
69
|
+
throw new LimitError2("backend-failure");
|
|
70
|
+
const clock = await now(tx);
|
|
71
|
+
const { state, result } = consumeState(previous, input, clock);
|
|
72
|
+
await tx.update(counters).set(state).where(and(eq(counters.scope, key), eq(counters.kind, kind)));
|
|
73
|
+
return result;
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
async acquire(input, token) {
|
|
77
|
+
validateAcquire(input);
|
|
78
|
+
validateToken(token);
|
|
79
|
+
const key = scopeKey(input.scope);
|
|
80
|
+
return db.transaction(async (tx) => {
|
|
81
|
+
await tx.insert(concurrencyBuckets).values({
|
|
82
|
+
scope: key,
|
|
83
|
+
capacity: input.capacity,
|
|
84
|
+
lastNow: 0,
|
|
85
|
+
leases: serializedJson([])
|
|
86
|
+
}).onConflictDoNothing();
|
|
87
|
+
const [previous] = await tx.select().from(concurrencyBuckets).where(eq(concurrencyBuckets.scope, key)).for("update");
|
|
88
|
+
if (!previous)
|
|
89
|
+
throw new LimitError2("backend-failure");
|
|
90
|
+
const clock = await now(tx);
|
|
91
|
+
const state = liveState(previous, input.capacity, clock);
|
|
92
|
+
const result = acquireState(state, input, token);
|
|
93
|
+
await tx.update(concurrencyBuckets).set({ ...state, leases: serializedJson(state.leases) }).where(eq(concurrencyBuckets.scope, key));
|
|
94
|
+
return result;
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
async renew(scope, token, ttlMs) {
|
|
98
|
+
const key = scopeKey(scope);
|
|
99
|
+
validateToken(token);
|
|
100
|
+
positiveInteger(ttlMs, maxDurationMs);
|
|
101
|
+
return db.transaction(async (tx) => {
|
|
102
|
+
const [previous] = await tx.select().from(concurrencyBuckets).where(eq(concurrencyBuckets.scope, key)).for("update");
|
|
103
|
+
if (!previous)
|
|
104
|
+
return null;
|
|
105
|
+
const state = liveState(previous, previous.capacity, await now(tx));
|
|
106
|
+
const lease = renewState(state, scope, token, ttlMs);
|
|
107
|
+
await tx.update(concurrencyBuckets).set({ ...state, leases: serializedJson(state.leases) }).where(eq(concurrencyBuckets.scope, key));
|
|
108
|
+
return lease;
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
async release(scope, token) {
|
|
112
|
+
const key = scopeKey(scope);
|
|
113
|
+
validateToken(token);
|
|
114
|
+
await db.transaction(async (tx) => {
|
|
115
|
+
const [previous] = await tx.select().from(concurrencyBuckets).where(eq(concurrencyBuckets.scope, key)).for("update");
|
|
116
|
+
if (!previous)
|
|
117
|
+
return;
|
|
118
|
+
const state = liveState(previous, previous.capacity, await now(tx));
|
|
119
|
+
state.leases = state.leases.filter((lease) => lease.token !== token);
|
|
120
|
+
await tx.update(concurrencyBuckets).set({ ...state, leases: serializedJson(state.leases) }).where(eq(concurrencyBuckets.scope, key));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
createPostgresLimitStore,
|
|
127
|
+
postgresLimitSchema
|
|
128
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Logger } from "@lenso/core/plugin";
|
|
2
|
+
import { type Acquisition, type AcquireInput, type ConsumeInput, type Decision, type Lease, type LimitConfig, type LimitStore } from "./contracts";
|
|
3
|
+
export interface LeaseRunOptions {
|
|
4
|
+
readonly signal?: AbortSignal;
|
|
5
|
+
/** The wrapper owns renewal only when explicitly requested, and stops it in finally. */
|
|
6
|
+
readonly renewEveryMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface LeaseExecution {
|
|
9
|
+
readonly signal: AbortSignal;
|
|
10
|
+
/** Null is possible only with explicitly configured fail-open admission. */
|
|
11
|
+
readonly lease: Lease | null;
|
|
12
|
+
}
|
|
13
|
+
export interface Limits {
|
|
14
|
+
consumeRate(input: ConsumeInput): Promise<Decision>;
|
|
15
|
+
consumeQuota(input: ConsumeInput): Promise<Decision>;
|
|
16
|
+
acquire(input: AcquireInput): Promise<Acquisition>;
|
|
17
|
+
renew(lease: Lease, ttlMs: number): Promise<Lease | null>;
|
|
18
|
+
release(lease: Lease): Promise<void>;
|
|
19
|
+
withLease<T>(input: AcquireInput, execute: (context: LeaseExecution) => Promise<T>, options?: LeaseRunOptions): Promise<T>;
|
|
20
|
+
/** Stops admission, aborts/drains wrappers, and releases this service's leases, not the store. */
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createLimits(options: {
|
|
24
|
+
readonly store: LimitStore;
|
|
25
|
+
readonly config: LimitConfig;
|
|
26
|
+
readonly logger?: Logger;
|
|
27
|
+
}): Limits;
|