@tstdl/base 0.93.187 → 0.93.188
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/ai/genkit/multi-region.plugin.js +14 -11
- package/ai/genkit/types.d.ts +1 -1
- package/ai/genkit/types.js +1 -1
- package/authentication/client/authentication.service.js +34 -9
- package/authentication/models/authentication-totp-recovery-code.model.js +3 -2
- package/authentication/server/authentication.api-controller.d.ts +1 -0
- package/authentication/server/authentication.api-controller.js +9 -2
- package/authentication/server/authentication.service.js +108 -91
- package/authentication/server/drizzle/{0000_odd_echo.sql → 0000_dry_stepford_cuckoos.sql} +2 -1
- package/authentication/server/drizzle/meta/0000_snapshot.json +24 -2
- package/authentication/server/drizzle/meta/_journal.json +2 -2
- package/circuit-breaker/circuit-breaker.d.ts +22 -10
- package/circuit-breaker/postgres/circuit-breaker.d.ts +5 -4
- package/circuit-breaker/postgres/circuit-breaker.js +21 -19
- package/circuit-breaker/postgres/drizzle/{0000_same_captain_cross.sql → 0000_dapper_hercules.sql} +3 -2
- package/circuit-breaker/postgres/drizzle/meta/0000_snapshot.json +13 -6
- package/circuit-breaker/postgres/drizzle/meta/_journal.json +2 -2
- package/circuit-breaker/postgres/model.d.ts +2 -1
- package/circuit-breaker/postgres/model.js +9 -4
- package/circuit-breaker/postgres/provider.d.ts +1 -1
- package/circuit-breaker/postgres/provider.js +2 -2
- package/circuit-breaker/provider.d.ts +6 -1
- package/orm/sqls/sqls.d.ts +5 -3
- package/orm/sqls/sqls.js +5 -3
- package/package.json +4 -4
- package/rate-limit/postgres/drizzle/{0000_serious_sauron.sql → 0000_previous_zeigeist.sql} +2 -1
- package/rate-limit/postgres/drizzle/meta/0000_snapshot.json +10 -3
- package/rate-limit/postgres/drizzle/meta/_journal.json +2 -2
- package/rate-limit/postgres/postgres-rate-limiter.d.ts +1 -0
- package/rate-limit/postgres/postgres-rate-limiter.js +3 -2
- package/rate-limit/postgres/rate-limit.model.d.ts +1 -0
- package/rate-limit/postgres/rate-limit.model.js +6 -1
- package/rate-limit/postgres/rate-limiter.provider.d.ts +1 -1
- package/rate-limit/postgres/rate-limiter.provider.js +2 -2
- package/rate-limit/provider.d.ts +3 -3
- package/rate-limit/rate-limiter.d.ts +2 -1
- package/signals/operators/derive-async.js +11 -6
- package/task-queue/postgres/task-queue.js +8 -8
- package/testing/integration-setup.js +4 -0
|
@@ -15,20 +15,32 @@ export type CircuitBreakerCheckResult = {
|
|
|
15
15
|
state: CircuitBreakerState;
|
|
16
16
|
isProbe: boolean;
|
|
17
17
|
};
|
|
18
|
-
export type CircuitBreakerArgument = string | (CircuitBreakerConfig & {
|
|
19
|
-
|
|
18
|
+
export type CircuitBreakerArgument = string | (Partial<CircuitBreakerConfig> & {
|
|
19
|
+
namespace: string;
|
|
20
20
|
});
|
|
21
21
|
export declare abstract class CircuitBreaker implements Resolvable<CircuitBreakerArgument> {
|
|
22
22
|
readonly [resolveArgumentType]: CircuitBreakerArgument;
|
|
23
|
+
abstract readonly namespace: string;
|
|
23
24
|
/**
|
|
24
|
-
* Checks if execution is allowed.
|
|
25
|
+
* Checks if execution is allowed for a resource.
|
|
25
26
|
* Transitions from Open to Half-Open automatically if timeout passed.
|
|
27
|
+
* @param resource The resource to check.
|
|
26
28
|
*/
|
|
27
|
-
abstract check(): Promise<CircuitBreakerCheckResult>;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
abstract check(resource: string): Promise<CircuitBreakerCheckResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Records a success for a resource, resetting its breaker to Closed.
|
|
32
|
+
* @param resource The resource to record success for.
|
|
33
|
+
*/
|
|
34
|
+
abstract recordSuccess(resource: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Records a failure for a resource, potentially tripping its breaker to Open.
|
|
37
|
+
* @param resource The resource to record failure for.
|
|
38
|
+
*/
|
|
39
|
+
abstract recordFailure(resource: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Records multiple failures for a resource at once.
|
|
42
|
+
* @param resource The resource to record failures for.
|
|
43
|
+
* @param count The number of failures to record.
|
|
44
|
+
*/
|
|
45
|
+
abstract recordFailures(resource: string, count: number): Promise<void>;
|
|
34
46
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { CircuitBreaker, type CircuitBreakerCheckResult } from '../circuit-breaker.js';
|
|
2
2
|
export declare class PostgresCircuitBreakerService extends CircuitBreaker {
|
|
3
3
|
#private;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
readonly namespace: string;
|
|
5
|
+
check(resource: string): Promise<CircuitBreakerCheckResult>;
|
|
6
|
+
recordSuccess(resource: string): Promise<void>;
|
|
7
|
+
recordFailure(resource: string): Promise<void>;
|
|
8
|
+
recordFailures(resource: string, count: number): Promise<void>;
|
|
8
9
|
private getPreparedCheckStatement;
|
|
9
10
|
}
|
|
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
import { and, eq, lte, sql, isNotNull as sqlIsNotNull } from 'drizzle-orm';
|
|
8
8
|
import { injectArgument, provide, Singleton } from '../../injector/index.js';
|
|
9
|
-
import { coalesce, interval
|
|
9
|
+
import { CLOCK_TIMESTAMP, coalesce, interval } from '../../orm/index.js';
|
|
10
10
|
import { DatabaseConfig, injectRepository } from '../../orm/server/index.js';
|
|
11
11
|
import { isString, isUndefined } from '../../utils/type-guards.js';
|
|
12
12
|
import { millisecondsPerSecond } from '../../utils/units.js';
|
|
@@ -17,12 +17,12 @@ import { circuitBreaker } from './schemas.js';
|
|
|
17
17
|
let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends CircuitBreaker {
|
|
18
18
|
#repository = injectRepository(PostgresCircuitBreaker);
|
|
19
19
|
#arg = injectArgument(this);
|
|
20
|
-
#key = isString(this.#arg) ? this.#arg : this.#arg.key;
|
|
21
20
|
#threshold = (isString(this.#arg) ? undefined : this.#arg.threshold) ?? 5;
|
|
22
21
|
#resetTimeout = (isString(this.#arg) ? undefined : this.#arg.resetTimeout) ?? 30 * millisecondsPerSecond;
|
|
23
22
|
#checkStatement = this.getPreparedCheckStatement();
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
namespace = isString(this.#arg) ? this.#arg : this.#arg.namespace;
|
|
24
|
+
async check(resource) {
|
|
25
|
+
const [result] = await this.#checkStatement.execute({ namespace: this.namespace, resource });
|
|
26
26
|
// 1. Breaker doesn't exist or is Closed
|
|
27
27
|
if (isUndefined(result) || (result.state === CircuitBreakerState.Closed)) {
|
|
28
28
|
return { allowed: true, state: CircuitBreakerState.Closed, isProbe: false };
|
|
@@ -38,20 +38,21 @@ let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends
|
|
|
38
38
|
// 4. Fallback: Catch-all for Open state where timeout hasn't expired. Reject
|
|
39
39
|
return { allowed: false, state: result.state, isProbe: false };
|
|
40
40
|
}
|
|
41
|
-
async recordSuccess() {
|
|
42
|
-
await this.#repository.tryDeleteByQuery({
|
|
41
|
+
async recordSuccess(resource) {
|
|
42
|
+
await this.#repository.tryDeleteByQuery({ namespace: this.namespace, resource });
|
|
43
43
|
}
|
|
44
|
-
async recordFailure() {
|
|
45
|
-
await this.recordFailures(1);
|
|
44
|
+
async recordFailure(resource) {
|
|
45
|
+
await this.recordFailures(resource, 1);
|
|
46
46
|
}
|
|
47
|
-
async recordFailures(count) {
|
|
47
|
+
async recordFailures(resource, count) {
|
|
48
48
|
const initialTrip = count >= this.#threshold;
|
|
49
49
|
const initialState = initialTrip ? CircuitBreakerState.Open : CircuitBreakerState.Closed;
|
|
50
50
|
const initialResetTimestamp = initialTrip
|
|
51
|
-
? sql `${
|
|
51
|
+
? sql `${CLOCK_TIMESTAMP} + ${interval(this.#resetTimeout, 'milliseconds')}`
|
|
52
52
|
: null;
|
|
53
|
-
await this.#repository.upsert(['
|
|
54
|
-
|
|
53
|
+
await this.#repository.upsert(['namespace', 'resource'], {
|
|
54
|
+
namespace: this.namespace,
|
|
55
|
+
resource,
|
|
55
56
|
state: initialState,
|
|
56
57
|
failureCount: count,
|
|
57
58
|
resetTimestamp: initialResetTimestamp,
|
|
@@ -62,7 +63,7 @@ let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends
|
|
|
62
63
|
ELSE ${circuitBreaker.state}
|
|
63
64
|
END`,
|
|
64
65
|
resetTimestamp: sql `CASE
|
|
65
|
-
WHEN ${circuitBreaker.failureCount} + ${count} >= ${this.#threshold} THEN ${
|
|
66
|
+
WHEN ${circuitBreaker.failureCount} + ${count} >= ${this.#threshold} THEN ${CLOCK_TIMESTAMP} + ${interval(this.#resetTimeout, 'milliseconds')}
|
|
66
67
|
ELSE ${circuitBreaker.resetTimestamp}
|
|
67
68
|
END`,
|
|
68
69
|
});
|
|
@@ -72,26 +73,27 @@ let PostgresCircuitBreakerService = class PostgresCircuitBreakerService extends
|
|
|
72
73
|
const attemptUpdate = session.$with('attempt_update').as(() => session
|
|
73
74
|
.update(circuitBreaker)
|
|
74
75
|
.set({ state: CircuitBreakerState.HalfOpen })
|
|
75
|
-
.where(and(eq(circuitBreaker.
|
|
76
|
+
.where(and(eq(circuitBreaker.namespace, sql.placeholder('namespace')), eq(circuitBreaker.resource, sql.placeholder('resource')), eq(circuitBreaker.state, CircuitBreakerState.Open), lte(circuitBreaker.resetTimestamp, CLOCK_TIMESTAMP)))
|
|
76
77
|
.returning({
|
|
77
|
-
|
|
78
|
+
namespace: circuitBreaker.namespace,
|
|
79
|
+
resource: circuitBreaker.resource,
|
|
78
80
|
state: circuitBreaker.state,
|
|
79
81
|
}));
|
|
80
82
|
return session
|
|
81
83
|
.with(attemptUpdate)
|
|
82
84
|
.select({
|
|
83
85
|
state: coalesce(attemptUpdate.state, circuitBreaker.state),
|
|
84
|
-
isProbe: sqlIsNotNull(attemptUpdate.
|
|
86
|
+
isProbe: sqlIsNotNull(attemptUpdate.resource),
|
|
85
87
|
})
|
|
86
88
|
.from(circuitBreaker)
|
|
87
|
-
.leftJoin(attemptUpdate, eq(circuitBreaker.
|
|
88
|
-
.where(eq(circuitBreaker.
|
|
89
|
+
.leftJoin(attemptUpdate, and(eq(circuitBreaker.namespace, attemptUpdate.namespace), eq(circuitBreaker.resource, attemptUpdate.resource)))
|
|
90
|
+
.where(and(eq(circuitBreaker.namespace, sql.placeholder('namespace')), eq(circuitBreaker.resource, sql.placeholder('resource'))))
|
|
89
91
|
.prepare('circuit_breaker_check');
|
|
90
92
|
}
|
|
91
93
|
};
|
|
92
94
|
PostgresCircuitBreakerService = __decorate([
|
|
93
95
|
Singleton({
|
|
94
|
-
argumentIdentityProvider: (arg) => isString(arg) ? arg : arg.
|
|
96
|
+
argumentIdentityProvider: (arg) => isString(arg) ? arg : arg.namespace,
|
|
95
97
|
providers: [
|
|
96
98
|
provide(DatabaseConfig, { useFactory: (_, context) => context.resolve(PostgresCircuitBreakerModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: true }) }),
|
|
97
99
|
],
|
package/circuit-breaker/postgres/drizzle/{0000_same_captain_cross.sql → 0000_dapper_hercules.sql}
RENAMED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
CREATE TYPE "circuit_breaker"."circuit_breaker_state" AS ENUM('closed', 'open', 'half-open');--> statement-breakpoint
|
|
2
2
|
CREATE TABLE "circuit_breaker"."circuit_breaker" (
|
|
3
3
|
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
4
|
-
"
|
|
4
|
+
"namespace" text NOT NULL,
|
|
5
|
+
"resource" text NOT NULL,
|
|
5
6
|
"state" "circuit_breaker"."circuit_breaker_state" NOT NULL,
|
|
6
7
|
"failure_count" integer NOT NULL,
|
|
7
8
|
"reset_timestamp" timestamp with time zone,
|
|
8
|
-
CONSTRAINT "
|
|
9
|
+
CONSTRAINT "circuit_breaker_namespace_resource_unique" UNIQUE("namespace","resource")
|
|
9
10
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "d6defc88-8e78-4652-bce7-95d26f93e753",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
"notNull": true,
|
|
16
16
|
"default": "uuidv7()"
|
|
17
17
|
},
|
|
18
|
-
"
|
|
19
|
-
"name": "
|
|
18
|
+
"namespace": {
|
|
19
|
+
"name": "namespace",
|
|
20
|
+
"type": "text",
|
|
21
|
+
"primaryKey": false,
|
|
22
|
+
"notNull": true
|
|
23
|
+
},
|
|
24
|
+
"resource": {
|
|
25
|
+
"name": "resource",
|
|
20
26
|
"type": "text",
|
|
21
27
|
"primaryKey": false,
|
|
22
28
|
"notNull": true
|
|
@@ -45,11 +51,12 @@
|
|
|
45
51
|
"foreignKeys": {},
|
|
46
52
|
"compositePrimaryKeys": {},
|
|
47
53
|
"uniqueConstraints": {
|
|
48
|
-
"
|
|
49
|
-
"name": "
|
|
54
|
+
"circuit_breaker_namespace_resource_unique": {
|
|
55
|
+
"name": "circuit_breaker_namespace_resource_unique",
|
|
50
56
|
"nullsNotDistinct": false,
|
|
51
57
|
"columns": [
|
|
52
|
-
"
|
|
58
|
+
"namespace",
|
|
59
|
+
"resource"
|
|
53
60
|
]
|
|
54
61
|
}
|
|
55
62
|
},
|
|
@@ -2,7 +2,8 @@ import { BaseEntity, type Timestamp } from '../../orm/index.js';
|
|
|
2
2
|
import { CircuitBreakerState } from '../circuit-breaker.js';
|
|
3
3
|
export declare class PostgresCircuitBreaker extends BaseEntity {
|
|
4
4
|
static readonly entityName = "CircuitBreaker";
|
|
5
|
-
|
|
5
|
+
namespace: string;
|
|
6
|
+
resource: string;
|
|
6
7
|
state: CircuitBreakerState;
|
|
7
8
|
failureCount: number;
|
|
8
9
|
resetTimestamp: Timestamp | null;
|
|
@@ -12,16 +12,20 @@ import { Enumeration, Integer, StringProperty } from '../../schema/index.js';
|
|
|
12
12
|
import { CircuitBreakerState } from '../circuit-breaker.js';
|
|
13
13
|
let PostgresCircuitBreaker = class PostgresCircuitBreaker extends BaseEntity {
|
|
14
14
|
static entityName = 'CircuitBreaker';
|
|
15
|
-
|
|
15
|
+
namespace;
|
|
16
|
+
resource;
|
|
16
17
|
state;
|
|
17
18
|
failureCount;
|
|
18
19
|
resetTimestamp;
|
|
19
20
|
};
|
|
20
21
|
__decorate([
|
|
21
22
|
StringProperty(),
|
|
22
|
-
Unique(),
|
|
23
23
|
__metadata("design:type", String)
|
|
24
|
-
], PostgresCircuitBreaker.prototype, "
|
|
24
|
+
], PostgresCircuitBreaker.prototype, "namespace", void 0);
|
|
25
|
+
__decorate([
|
|
26
|
+
StringProperty(),
|
|
27
|
+
__metadata("design:type", String)
|
|
28
|
+
], PostgresCircuitBreaker.prototype, "resource", void 0);
|
|
25
29
|
__decorate([
|
|
26
30
|
Enumeration(CircuitBreakerState),
|
|
27
31
|
__metadata("design:type", String)
|
|
@@ -35,6 +39,7 @@ __decorate([
|
|
|
35
39
|
__metadata("design:type", Object)
|
|
36
40
|
], PostgresCircuitBreaker.prototype, "resetTimestamp", void 0);
|
|
37
41
|
PostgresCircuitBreaker = __decorate([
|
|
38
|
-
Table('circuit_breaker', { schema: 'circuit_breaker' })
|
|
42
|
+
Table('circuit_breaker', { schema: 'circuit_breaker' }),
|
|
43
|
+
Unique(['namespace', 'resource'])
|
|
39
44
|
], PostgresCircuitBreaker);
|
|
40
45
|
export { PostgresCircuitBreaker };
|
|
@@ -2,5 +2,5 @@ import type { CircuitBreaker, CircuitBreakerConfig } from '../circuit-breaker.js
|
|
|
2
2
|
import { CircuitBreakerProvider } from '../provider.js';
|
|
3
3
|
export declare class PostgresCircuitBreakerProvider extends CircuitBreakerProvider {
|
|
4
4
|
#private;
|
|
5
|
-
provide(
|
|
5
|
+
provide(namespace: string, config?: CircuitBreakerConfig): CircuitBreaker;
|
|
6
6
|
}
|
|
@@ -10,8 +10,8 @@ import { CircuitBreakerProvider } from '../provider.js';
|
|
|
10
10
|
import { PostgresCircuitBreakerService } from './circuit-breaker.js';
|
|
11
11
|
let PostgresCircuitBreakerProvider = class PostgresCircuitBreakerProvider extends CircuitBreakerProvider {
|
|
12
12
|
#injector = inject(Injector);
|
|
13
|
-
provide(
|
|
14
|
-
const argument = config ? {
|
|
13
|
+
provide(namespace, config) {
|
|
14
|
+
const argument = config ? { namespace, ...config } : namespace;
|
|
15
15
|
return this.#injector.resolve(PostgresCircuitBreakerService, argument);
|
|
16
16
|
}
|
|
17
17
|
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { CircuitBreaker, CircuitBreakerConfig } from './circuit-breaker.js';
|
|
2
2
|
export declare abstract class CircuitBreakerProvider {
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Provides a CircuitBreaker for a namespace.
|
|
5
|
+
* @param namespace The namespace for the circuit breaker.
|
|
6
|
+
* @param config The configuration for the circuit breaker.
|
|
7
|
+
*/
|
|
8
|
+
abstract provide(namespace: string, config?: CircuitBreakerConfig): CircuitBreaker;
|
|
4
9
|
}
|
package/orm/sqls/sqls.d.ts
CHANGED
|
@@ -55,11 +55,13 @@ export type TsHeadlineOptions = {
|
|
|
55
55
|
*/
|
|
56
56
|
fragmentDelimiter?: string;
|
|
57
57
|
};
|
|
58
|
-
/**
|
|
58
|
+
/** Get the current transaction's timestamp. */
|
|
59
59
|
export declare const TRANSACTION_TIMESTAMP: SQL<Date>;
|
|
60
|
-
/**
|
|
60
|
+
/** Get the current clock timestamp (real time, even within transaction). */
|
|
61
|
+
export declare const CLOCK_TIMESTAMP: SQL<Date>;
|
|
62
|
+
/** Generate a random UUID (v4). */
|
|
61
63
|
export declare const RANDOM_UUID_V4: SQL<Uuid>;
|
|
62
|
-
/**
|
|
64
|
+
/** Generate a random UUID (v7). */
|
|
63
65
|
export declare const RANDOM_UUID_V7: SQL<Uuid>;
|
|
64
66
|
export declare const SQL_TRUE: SQL<boolean>;
|
|
65
67
|
export declare const SQL_FALSE: SQL<boolean>;
|
package/orm/sqls/sqls.js
CHANGED
|
@@ -25,11 +25,13 @@ function isJsonb(value) {
|
|
|
25
25
|
}
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
|
-
/**
|
|
28
|
+
/** Get the current transaction's timestamp. */
|
|
29
29
|
export const TRANSACTION_TIMESTAMP = sql `transaction_timestamp()`;
|
|
30
|
-
/**
|
|
30
|
+
/** Get the current clock timestamp (real time, even within transaction). */
|
|
31
|
+
export const CLOCK_TIMESTAMP = sql `clock_timestamp()`;
|
|
32
|
+
/** Generate a random UUID (v4). */
|
|
31
33
|
export const RANDOM_UUID_V4 = sql `gen_random_uuid()`;
|
|
32
|
-
/**
|
|
34
|
+
/** Generate a random UUID (v7). */
|
|
33
35
|
export const RANDOM_UUID_V7 = sql `uuidv7()`;
|
|
34
36
|
export const SQL_TRUE = sql `TRUE`;
|
|
35
37
|
export const SQL_FALSE = sql `FALSE`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.188",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -153,8 +153,8 @@
|
|
|
153
153
|
"type-fest": "^5.5"
|
|
154
154
|
},
|
|
155
155
|
"peerDependencies": {
|
|
156
|
-
"@aws-sdk/client-s3": "^3.
|
|
157
|
-
"@aws-sdk/s3-request-presigner": "^3.
|
|
156
|
+
"@aws-sdk/client-s3": "^3.1022",
|
|
157
|
+
"@aws-sdk/s3-request-presigner": "^3.1022",
|
|
158
158
|
"@genkit-ai/google-genai": "^1.31",
|
|
159
159
|
"@google-cloud/storage": "^7.19",
|
|
160
160
|
"@toon-format/toon": "^2.1.0",
|
|
@@ -174,7 +174,7 @@
|
|
|
174
174
|
"preact": "^10.29",
|
|
175
175
|
"preact-render-to-string": "^6.6",
|
|
176
176
|
"sharp": "^0.34",
|
|
177
|
-
"undici": "^
|
|
177
|
+
"undici": "^8.0",
|
|
178
178
|
"urlpattern-polyfill": "^10.1",
|
|
179
179
|
"zod": "^3.25"
|
|
180
180
|
},
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
CREATE TABLE "rate_limit"."rate_limit" (
|
|
2
2
|
"id" uuid PRIMARY KEY DEFAULT uuidv7() NOT NULL,
|
|
3
|
+
"namespace" text NOT NULL,
|
|
3
4
|
"resource" text NOT NULL,
|
|
4
5
|
"tokens" double precision NOT NULL,
|
|
5
6
|
"last_refill_timestamp" timestamp with time zone NOT NULL,
|
|
6
|
-
CONSTRAINT "
|
|
7
|
+
CONSTRAINT "rate_limit_namespace_resource_unique" UNIQUE("namespace","resource")
|
|
7
8
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "e46716f4-2778-4a99-b7ac-731d9efead2c",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
"notNull": true,
|
|
16
16
|
"default": "uuidv7()"
|
|
17
17
|
},
|
|
18
|
+
"namespace": {
|
|
19
|
+
"name": "namespace",
|
|
20
|
+
"type": "text",
|
|
21
|
+
"primaryKey": false,
|
|
22
|
+
"notNull": true
|
|
23
|
+
},
|
|
18
24
|
"resource": {
|
|
19
25
|
"name": "resource",
|
|
20
26
|
"type": "text",
|
|
@@ -38,10 +44,11 @@
|
|
|
38
44
|
"foreignKeys": {},
|
|
39
45
|
"compositePrimaryKeys": {},
|
|
40
46
|
"uniqueConstraints": {
|
|
41
|
-
"
|
|
42
|
-
"name": "
|
|
47
|
+
"rate_limit_namespace_resource_unique": {
|
|
48
|
+
"name": "rate_limit_namespace_resource_unique",
|
|
43
49
|
"nullsNotDistinct": false,
|
|
44
50
|
"columns": [
|
|
51
|
+
"namespace",
|
|
45
52
|
"resource"
|
|
46
53
|
]
|
|
47
54
|
}
|
|
@@ -16,6 +16,7 @@ import { rateLimit } from './schemas.js';
|
|
|
16
16
|
let PostgresRateLimiter = class PostgresRateLimiter extends RateLimiter {
|
|
17
17
|
#repository = injectRepository(PostgresRateLimit);
|
|
18
18
|
#config = this.transactionalContextData?.config ?? injectArgument(this);
|
|
19
|
+
namespace = isString(this.#config) ? this.#config : this.#config.namespace;
|
|
19
20
|
burstCapacity = isString(this.#config) ? 100 : this.#config.burstCapacity;
|
|
20
21
|
refillInterval = isString(this.#config) ? 1000 : this.#config.refillInterval;
|
|
21
22
|
refillRate = this.burstCapacity / this.refillInterval;
|
|
@@ -38,7 +39,7 @@ let PostgresRateLimiter = class PostgresRateLimiter extends RateLimiter {
|
|
|
38
39
|
const lastRefillMs = sql `(EXTRACT(EPOCH FROM ${rateLimit.lastRefillTimestamp}) * 1000)`;
|
|
39
40
|
const tokensToAdd = sql `(${nowMs} - ${lastRefillMs}) * ${refillRate}`;
|
|
40
41
|
const newTokensExpression = least(burstCapacity, sql `${rateLimit.tokens} + ${tokensToAdd}`);
|
|
41
|
-
const result = await this.#repository.tryUpsert('resource', { resource, tokens: burstCapacity - cost, lastRefillTimestamp: TRANSACTION_TIMESTAMP }, { tokens: sql `${newTokensExpression} - ${cost}`, lastRefillTimestamp: TRANSACTION_TIMESTAMP }, { set: gte(newTokensExpression, cost) });
|
|
42
|
+
const result = await this.#repository.tryUpsert(['namespace', 'resource'], { namespace: this.namespace, resource, tokens: burstCapacity - cost, lastRefillTimestamp: TRANSACTION_TIMESTAMP }, { tokens: sql `${newTokensExpression} - ${cost}`, lastRefillTimestamp: TRANSACTION_TIMESTAMP }, { set: gte(newTokensExpression, cost) });
|
|
42
43
|
return isDefined(result);
|
|
43
44
|
}
|
|
44
45
|
async refund(resource, amount = 1, options) {
|
|
@@ -46,7 +47,7 @@ let PostgresRateLimiter = class PostgresRateLimiter extends RateLimiter {
|
|
|
46
47
|
return;
|
|
47
48
|
}
|
|
48
49
|
const burstCapacity = options?.burstCapacity ?? this.burstCapacity;
|
|
49
|
-
await this.#repository.updateByQuery({ resource }, { tokens: least(burstCapacity, sql `${rateLimit.tokens} + ${amount}`) });
|
|
50
|
+
await this.#repository.updateByQuery({ namespace: this.namespace, resource }, { tokens: least(burstCapacity, sql `${rateLimit.tokens} + ${amount}`) });
|
|
50
51
|
}
|
|
51
52
|
getTransactionalContextData() {
|
|
52
53
|
return { config: this.#config };
|
|
@@ -2,6 +2,7 @@ import { BaseEntity } from '../../orm/index.js';
|
|
|
2
2
|
import type { Timestamp } from '../../orm/types.js';
|
|
3
3
|
export declare class PostgresRateLimit extends BaseEntity {
|
|
4
4
|
static readonly entityName = "RateLimit";
|
|
5
|
+
namespace: string;
|
|
5
6
|
resource: string;
|
|
6
7
|
tokens: number;
|
|
7
8
|
lastRefillTimestamp: Timestamp;
|
|
@@ -12,10 +12,15 @@ import { TimestampProperty } from '../../orm/schemas/timestamp.js';
|
|
|
12
12
|
import { NumberProperty, StringProperty } from '../../schema/index.js';
|
|
13
13
|
let PostgresRateLimit = class PostgresRateLimit extends BaseEntity {
|
|
14
14
|
static entityName = 'RateLimit';
|
|
15
|
+
namespace;
|
|
15
16
|
resource;
|
|
16
17
|
tokens;
|
|
17
18
|
lastRefillTimestamp;
|
|
18
19
|
};
|
|
20
|
+
__decorate([
|
|
21
|
+
StringProperty(),
|
|
22
|
+
__metadata("design:type", String)
|
|
23
|
+
], PostgresRateLimit.prototype, "namespace", void 0);
|
|
19
24
|
__decorate([
|
|
20
25
|
StringProperty(),
|
|
21
26
|
__metadata("design:type", String)
|
|
@@ -30,6 +35,6 @@ __decorate([
|
|
|
30
35
|
], PostgresRateLimit.prototype, "lastRefillTimestamp", void 0);
|
|
31
36
|
PostgresRateLimit = __decorate([
|
|
32
37
|
Table('rate_limit', { schema: 'rate_limit' }),
|
|
33
|
-
Unique(['resource'])
|
|
38
|
+
Unique(['namespace', 'resource'])
|
|
34
39
|
], PostgresRateLimit);
|
|
35
40
|
export { PostgresRateLimit };
|
|
@@ -2,5 +2,5 @@ import { RateLimiterProvider } from '../provider.js';
|
|
|
2
2
|
import { RateLimiter, type RateLimiterConfig } from '../rate-limiter.js';
|
|
3
3
|
export declare class PostgresRateLimiterProvider extends RateLimiterProvider {
|
|
4
4
|
#private;
|
|
5
|
-
get(
|
|
5
|
+
get(namespace: string, config: RateLimiterConfig): RateLimiter;
|
|
6
6
|
}
|
|
@@ -11,8 +11,8 @@ import { RateLimiter } from '../rate-limiter.js';
|
|
|
11
11
|
import { PostgresRateLimiter } from './postgres-rate-limiter.js';
|
|
12
12
|
let PostgresRateLimiterProvider = class PostgresRateLimiterProvider extends RateLimiterProvider {
|
|
13
13
|
#injector = inject(Injector);
|
|
14
|
-
get(
|
|
15
|
-
return this.#injector.resolve(PostgresRateLimiter, { ...config,
|
|
14
|
+
get(namespace, config) {
|
|
15
|
+
return this.#injector.resolve(PostgresRateLimiter, { ...config, namespace });
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
18
|
PostgresRateLimiterProvider = __decorate([
|
package/rate-limit/provider.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { RateLimiter, RateLimiterConfig } from './rate-limiter.js';
|
|
2
2
|
export declare abstract class RateLimiterProvider {
|
|
3
3
|
/**
|
|
4
|
-
* Gets a RateLimiter instance for the specified
|
|
5
|
-
* @param
|
|
4
|
+
* Gets a RateLimiter instance for the specified namespace and configuration.
|
|
5
|
+
* @param namespace The namespace of the rate limiter.
|
|
6
6
|
* @param config The configuration for the rate limiter.
|
|
7
7
|
*/
|
|
8
|
-
abstract get(
|
|
8
|
+
abstract get(namespace: string, config: RateLimiterConfig): RateLimiter;
|
|
9
9
|
}
|
|
@@ -11,10 +11,11 @@ export type RateLimiterConfig = {
|
|
|
11
11
|
refillInterval: number;
|
|
12
12
|
};
|
|
13
13
|
export type RateLimiterArgument = string | (RateLimiterConfig & {
|
|
14
|
-
|
|
14
|
+
namespace: string;
|
|
15
15
|
});
|
|
16
16
|
export declare abstract class RateLimiter extends Transactional implements Resolvable<RateLimiterArgument> {
|
|
17
17
|
readonly [resolveArgumentType]: RateLimiterArgument;
|
|
18
|
+
abstract readonly namespace: string;
|
|
18
19
|
abstract readonly burstCapacity: number;
|
|
19
20
|
abstract readonly refillInterval: number;
|
|
20
21
|
/**
|
|
@@ -21,7 +21,8 @@ const operatorMap = {
|
|
|
21
21
|
* @returns A Signal that represents the latest value from the async source.
|
|
22
22
|
*/
|
|
23
23
|
export function deriveAsync(source, options) {
|
|
24
|
-
const
|
|
24
|
+
const sourceSignal = computed(source);
|
|
25
|
+
const initialRaw = untracked(sourceSignal);
|
|
25
26
|
const initialSource = isPromise(initialRaw) ? from(initialRaw) : isObservable(initialRaw) ? initialRaw : of(initialRaw);
|
|
26
27
|
const source$ = new BehaviorSubject(initialSource);
|
|
27
28
|
const destroy$ = new Subject();
|
|
@@ -44,14 +45,18 @@ export function deriveAsync(source, options) {
|
|
|
44
45
|
}
|
|
45
46
|
return notification.value;
|
|
46
47
|
}, { equal: options?.equal });
|
|
47
|
-
let
|
|
48
|
+
let lastRaw = initialRaw;
|
|
48
49
|
const effectRef = effect(() => {
|
|
49
|
-
const rawSource =
|
|
50
|
-
if (
|
|
51
|
-
firstRun = false;
|
|
50
|
+
const rawSource = sourceSignal();
|
|
51
|
+
if (rawSource === lastRaw) {
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
lastRaw = rawSource;
|
|
55
|
+
const observableInput = isPromise(rawSource)
|
|
56
|
+
? from(rawSource)
|
|
57
|
+
: isObservable(rawSource)
|
|
58
|
+
? rawSource
|
|
59
|
+
: of(rawSource);
|
|
55
60
|
untracked(() => source$.next(observableInput));
|
|
56
61
|
});
|
|
57
62
|
registerFinalization(result, () => {
|