@enbox/dwn-server 0.0.2 → 0.0.4
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 +3 -2
- package/README.md +115 -215
- package/dist/esm/src/admin/activity-log.d.ts +44 -0
- package/dist/esm/src/admin/activity-log.d.ts.map +1 -0
- package/dist/esm/src/admin/activity-log.js +85 -0
- package/dist/esm/src/admin/activity-log.js.map +1 -0
- package/dist/esm/src/admin/admin-api.d.ts +61 -0
- package/dist/esm/src/admin/admin-api.d.ts.map +1 -0
- package/dist/esm/src/admin/admin-api.js +1047 -0
- package/dist/esm/src/admin/admin-api.js.map +1 -0
- package/dist/esm/src/admin/admin-auth.d.ts +9 -0
- package/dist/esm/src/admin/admin-auth.d.ts.map +1 -0
- package/dist/esm/src/admin/admin-auth.js +45 -0
- package/dist/esm/src/admin/admin-auth.js.map +1 -0
- package/dist/esm/src/admin/admin-store.d.ts +111 -0
- package/dist/esm/src/admin/admin-store.d.ts.map +1 -0
- package/dist/esm/src/admin/admin-store.js +376 -0
- package/dist/esm/src/admin/admin-store.js.map +1 -0
- package/dist/esm/src/admin/audit-log.d.ts +94 -0
- package/dist/esm/src/admin/audit-log.d.ts.map +1 -0
- package/dist/esm/src/admin/audit-log.js +220 -0
- package/dist/esm/src/admin/audit-log.js.map +1 -0
- package/dist/esm/src/admin/index.d.ts +10 -0
- package/dist/esm/src/admin/index.d.ts.map +1 -0
- package/dist/esm/src/admin/index.js +7 -0
- package/dist/esm/src/admin/index.js.map +1 -0
- package/dist/esm/src/admin/types.d.ts +306 -0
- package/dist/esm/src/admin/types.d.ts.map +1 -0
- package/dist/esm/src/admin/types.js +2 -0
- package/dist/esm/src/admin/types.js.map +1 -0
- package/dist/esm/src/admin/webhook-manager.d.ts +55 -0
- package/dist/esm/src/admin/webhook-manager.d.ts.map +1 -0
- package/dist/esm/src/admin/webhook-manager.js +184 -0
- package/dist/esm/src/admin/webhook-manager.js.map +1 -0
- package/dist/esm/src/config.d.ts +124 -9
- package/dist/esm/src/config.d.ts.map +1 -1
- package/dist/esm/src/config.js +155 -13
- package/dist/esm/src/config.js.map +1 -1
- package/dist/esm/src/connection/connection-manager.d.ts +32 -9
- package/dist/esm/src/connection/connection-manager.d.ts.map +1 -1
- package/dist/esm/src/connection/connection-manager.js +38 -5
- package/dist/esm/src/connection/connection-manager.js.map +1 -1
- package/dist/esm/src/connection/flow-controller.d.ts +53 -0
- package/dist/esm/src/connection/flow-controller.d.ts.map +1 -0
- package/dist/esm/src/connection/flow-controller.js +101 -0
- package/dist/esm/src/connection/flow-controller.js.map +1 -0
- package/dist/esm/src/connection/socket-connection.d.ts +54 -18
- package/dist/esm/src/connection/socket-connection.d.ts.map +1 -1
- package/dist/esm/src/connection/socket-connection.js +102 -40
- package/dist/esm/src/connection/socket-connection.js.map +1 -1
- package/dist/esm/src/delivery-service.d.ts +43 -0
- package/dist/esm/src/delivery-service.d.ts.map +1 -0
- package/dist/esm/src/delivery-service.js +574 -0
- package/dist/esm/src/delivery-service.js.map +1 -0
- package/dist/esm/src/dwn-error.d.ts +10 -1
- package/dist/esm/src/dwn-error.d.ts.map +1 -1
- package/dist/esm/src/dwn-error.js +9 -0
- package/dist/esm/src/dwn-error.js.map +1 -1
- package/dist/esm/src/dwn-server.d.ts +13 -6
- package/dist/esm/src/dwn-server.d.ts.map +1 -1
- package/dist/esm/src/dwn-server.js +199 -24
- package/dist/esm/src/dwn-server.js.map +1 -1
- package/dist/esm/src/http-api.d.ts +28 -13
- package/dist/esm/src/http-api.d.ts.map +1 -1
- package/dist/esm/src/http-api.js +649 -374
- package/dist/esm/src/http-api.js.map +1 -1
- package/dist/esm/src/index.d.ts +6 -2
- package/dist/esm/src/index.d.ts.map +1 -1
- package/dist/esm/src/index.js +4 -1
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/json-rpc-api.js +2 -1
- package/dist/esm/src/json-rpc-api.js.map +1 -1
- package/dist/esm/src/json-rpc-handlers/dwn/process-message.d.ts.map +1 -1
- package/dist/esm/src/json-rpc-handlers/dwn/process-message.js +109 -7
- package/dist/esm/src/json-rpc-handlers/dwn/process-message.js.map +1 -1
- package/dist/esm/src/json-rpc-handlers/subscription/ack.d.ts +20 -0
- package/dist/esm/src/json-rpc-handlers/subscription/ack.d.ts.map +1 -0
- package/dist/esm/src/json-rpc-handlers/subscription/ack.js +41 -0
- package/dist/esm/src/json-rpc-handlers/subscription/ack.js.map +1 -0
- package/dist/esm/src/json-rpc-handlers/subscription/close.d.ts.map +1 -1
- package/dist/esm/src/json-rpc-handlers/subscription/close.js +1 -1
- package/dist/esm/src/json-rpc-handlers/subscription/close.js.map +1 -1
- package/dist/esm/src/json-rpc-handlers/subscription/index.d.ts +1 -0
- package/dist/esm/src/json-rpc-handlers/subscription/index.d.ts.map +1 -1
- package/dist/esm/src/json-rpc-handlers/subscription/index.js +1 -0
- package/dist/esm/src/json-rpc-handlers/subscription/index.js.map +1 -1
- package/dist/esm/src/lib/json-rpc-router.d.ts +25 -8
- package/dist/esm/src/lib/json-rpc-router.d.ts.map +1 -1
- package/dist/esm/src/lib/json-rpc-router.js.map +1 -1
- package/dist/esm/src/lib/sql-utils.d.ts +6 -0
- package/dist/esm/src/lib/sql-utils.d.ts.map +1 -0
- package/dist/esm/src/lib/sql-utils.js +8 -0
- package/dist/esm/src/lib/sql-utils.js.map +1 -0
- package/dist/esm/src/main.js +0 -6
- package/dist/esm/src/main.js.map +1 -1
- package/dist/esm/src/message-processed-hook.d.ts +35 -0
- package/dist/esm/src/message-processed-hook.d.ts.map +1 -0
- package/dist/esm/src/message-processed-hook.js +2 -0
- package/dist/esm/src/message-processed-hook.js.map +1 -0
- package/dist/esm/src/metrics.d.ts +14 -2
- package/dist/esm/src/metrics.d.ts.map +1 -1
- package/dist/esm/src/metrics.js +41 -1
- package/dist/esm/src/metrics.js.map +1 -1
- package/dist/esm/src/plugins/event-log-nats.d.ts +25 -0
- package/dist/esm/src/plugins/event-log-nats.d.ts.map +1 -0
- package/dist/esm/src/plugins/event-log-nats.js +379 -0
- package/dist/esm/src/plugins/event-log-nats.js.map +1 -0
- package/dist/esm/src/rate-limiter.d.ts +60 -0
- package/dist/esm/src/rate-limiter.d.ts.map +1 -0
- package/dist/esm/src/rate-limiter.js +116 -0
- package/dist/esm/src/rate-limiter.js.map +1 -0
- package/dist/esm/src/registration/jwt-provider-auth-plugin.d.ts +53 -0
- package/dist/esm/src/registration/jwt-provider-auth-plugin.d.ts.map +1 -0
- package/dist/esm/src/registration/jwt-provider-auth-plugin.js +90 -0
- package/dist/esm/src/registration/jwt-provider-auth-plugin.js.map +1 -0
- package/dist/esm/src/registration/open-auth-handler.d.ts +37 -0
- package/dist/esm/src/registration/open-auth-handler.d.ts.map +1 -0
- package/dist/esm/src/registration/open-auth-handler.js +214 -0
- package/dist/esm/src/registration/open-auth-handler.js.map +1 -0
- package/dist/esm/src/registration/proof-of-work-manager.d.ts +1 -1
- package/dist/esm/src/registration/proof-of-work-manager.d.ts.map +1 -1
- package/dist/esm/src/registration/proof-of-work-manager.js +3 -3
- package/dist/esm/src/registration/proof-of-work-manager.js.map +1 -1
- package/dist/esm/src/registration/provider-auth-plugin.d.ts +46 -0
- package/dist/esm/src/registration/provider-auth-plugin.d.ts.map +1 -0
- package/dist/esm/src/registration/provider-auth-plugin.js +29 -0
- package/dist/esm/src/registration/provider-auth-plugin.js.map +1 -0
- package/dist/esm/src/registration/registration-manager.d.ts +28 -5
- package/dist/esm/src/registration/registration-manager.d.ts.map +1 -1
- package/dist/esm/src/registration/registration-manager.js +83 -12
- package/dist/esm/src/registration/registration-manager.js.map +1 -1
- package/dist/esm/src/registration/registration-store.d.ts +83 -3
- package/dist/esm/src/registration/registration-store.d.ts.map +1 -1
- package/dist/esm/src/registration/registration-store.js +248 -11
- package/dist/esm/src/registration/registration-store.js.map +1 -1
- package/dist/esm/src/storage.d.ts +5 -5
- package/dist/esm/src/storage.d.ts.map +1 -1
- package/dist/esm/src/storage.js +105 -24
- package/dist/esm/src/storage.js.map +1 -1
- package/dist/esm/src/web5-connect/sql-ttl-cache.d.ts.map +1 -1
- package/dist/esm/src/web5-connect/sql-ttl-cache.js +11 -3
- package/dist/esm/src/web5-connect/sql-ttl-cache.js.map +1 -1
- package/dist/esm/src/web5-connect/web5-connect-server.d.ts.map +1 -1
- package/dist/esm/src/web5-connect/web5-connect-server.js +2 -2
- package/dist/esm/src/web5-connect/web5-connect-server.js.map +1 -1
- package/dist/esm/src/ws-api.d.ts +18 -4
- package/dist/esm/src/ws-api.d.ts.map +1 -1
- package/dist/esm/src/ws-api.js +12 -16
- package/dist/esm/src/ws-api.js.map +1 -1
- package/package.json +34 -53
- package/src/admin/activity-log.ts +100 -0
- package/src/admin/admin-api.ts +1308 -0
- package/src/admin/admin-auth.ts +56 -0
- package/src/admin/admin-store.ts +515 -0
- package/src/admin/audit-log.ts +327 -0
- package/src/admin/index.ts +34 -0
- package/src/admin/types.ts +352 -0
- package/src/admin/webhook-manager.ts +245 -0
- package/src/config.ts +190 -22
- package/src/connection/connection-manager.ts +67 -17
- package/src/connection/flow-controller.ts +117 -0
- package/src/connection/socket-connection.ts +144 -67
- package/src/delivery-service.ts +740 -0
- package/src/dwn-error.ts +11 -2
- package/src/dwn-server.ts +254 -39
- package/src/http-api.ts +736 -392
- package/src/index.ts +13 -2
- package/src/json-rpc-api.ts +2 -1
- package/src/json-rpc-handlers/dwn/process-message.ts +149 -15
- package/src/json-rpc-handlers/subscription/ack.ts +63 -0
- package/src/json-rpc-handlers/subscription/close.ts +5 -9
- package/src/json-rpc-handlers/subscription/index.ts +1 -0
- package/src/lib/json-rpc-router.ts +26 -11
- package/src/lib/sql-utils.ts +7 -0
- package/src/main.ts +0 -8
- package/src/message-processed-hook.ts +33 -0
- package/src/metrics.ts +57 -8
- package/src/plugins/event-log-nats.ts +466 -0
- package/src/process-handlers.ts +5 -5
- package/src/rate-limiter.ts +143 -0
- package/src/registration/jwt-provider-auth-plugin.ts +119 -0
- package/src/registration/open-auth-handler.ts +263 -0
- package/src/registration/proof-of-work-manager.ts +11 -10
- package/src/registration/provider-auth-plugin.ts +84 -0
- package/src/registration/registration-manager.ts +129 -31
- package/src/registration/registration-store.ts +332 -22
- package/src/storage.ts +136 -40
- package/src/web5-connect/sql-ttl-cache.ts +12 -5
- package/src/web5-connect/web5-connect-server.ts +9 -8
- package/src/ws-api.ts +39 -26
- package/dist/cjs/index.js +0 -6811
- package/dist/cjs/package.json +0 -1
- package/dist/esm/src/json-rpc-socket.d.ts +0 -39
- package/dist/esm/src/json-rpc-socket.d.ts.map +0 -1
- package/dist/esm/src/json-rpc-socket.js +0 -125
- package/dist/esm/src/json-rpc-socket.js.map +0 -1
- package/dist/esm/src/lib/http-server-shutdown-handler.d.ts +0 -10
- package/dist/esm/src/lib/http-server-shutdown-handler.d.ts.map +0 -1
- package/dist/esm/src/lib/http-server-shutdown-handler.js +0 -65
- package/dist/esm/src/lib/http-server-shutdown-handler.js.map +0 -1
- package/dist/esm/src/lib/json-rpc.d.ts +0 -54
- package/dist/esm/src/lib/json-rpc.d.ts.map +0 -1
- package/dist/esm/src/lib/json-rpc.js +0 -60
- package/dist/esm/src/lib/json-rpc.js.map +0 -1
- package/dist/esm/src/registration/proof-of-work-types.d.ts +0 -8
- package/dist/esm/src/registration/proof-of-work-types.d.ts.map +0 -1
- package/dist/esm/src/registration/proof-of-work-types.js +0 -2
- package/dist/esm/src/registration/proof-of-work-types.js.map +0 -1
- package/dist/esm/src/registration/registration-types.d.ts +0 -18
- package/dist/esm/src/registration/registration-types.d.ts.map +0 -1
- package/dist/esm/src/registration/registration-types.js +0 -2
- package/dist/esm/src/registration/registration-types.js.map +0 -1
- package/dist/esm/tests/common-scenario-validator.d.ts +0 -11
- package/dist/esm/tests/common-scenario-validator.d.ts.map +0 -1
- package/dist/esm/tests/common-scenario-validator.js +0 -114
- package/dist/esm/tests/common-scenario-validator.js.map +0 -1
- package/dist/esm/tests/connection/connection-manager.spec.d.ts +0 -2
- package/dist/esm/tests/connection/connection-manager.spec.d.ts.map +0 -1
- package/dist/esm/tests/connection/connection-manager.spec.js +0 -47
- package/dist/esm/tests/connection/connection-manager.spec.js.map +0 -1
- package/dist/esm/tests/connection/socket-connection.spec.d.ts +0 -2
- package/dist/esm/tests/connection/socket-connection.spec.d.ts.map +0 -1
- package/dist/esm/tests/connection/socket-connection.spec.js +0 -125
- package/dist/esm/tests/connection/socket-connection.spec.js.map +0 -1
- package/dist/esm/tests/cors/http-api.browser.d.ts +0 -2
- package/dist/esm/tests/cors/http-api.browser.d.ts.map +0 -1
- package/dist/esm/tests/cors/http-api.browser.js +0 -60
- package/dist/esm/tests/cors/http-api.browser.js.map +0 -1
- package/dist/esm/tests/cors/ping.browser.d.ts +0 -2
- package/dist/esm/tests/cors/ping.browser.d.ts.map +0 -1
- package/dist/esm/tests/cors/ping.browser.js +0 -7
- package/dist/esm/tests/cors/ping.browser.js.map +0 -1
- package/dist/esm/tests/dwn-process-message.spec.d.ts +0 -2
- package/dist/esm/tests/dwn-process-message.spec.d.ts.map +0 -1
- package/dist/esm/tests/dwn-process-message.spec.js +0 -172
- package/dist/esm/tests/dwn-process-message.spec.js.map +0 -1
- package/dist/esm/tests/dwn-server.spec.d.ts +0 -2
- package/dist/esm/tests/dwn-server.spec.d.ts.map +0 -1
- package/dist/esm/tests/dwn-server.spec.js +0 -49
- package/dist/esm/tests/dwn-server.spec.js.map +0 -1
- package/dist/esm/tests/http-api.spec.d.ts +0 -2
- package/dist/esm/tests/http-api.spec.d.ts.map +0 -1
- package/dist/esm/tests/http-api.spec.js +0 -775
- package/dist/esm/tests/http-api.spec.js.map +0 -1
- package/dist/esm/tests/json-rpc-socket.spec.d.ts +0 -2
- package/dist/esm/tests/json-rpc-socket.spec.d.ts.map +0 -1
- package/dist/esm/tests/json-rpc-socket.spec.js +0 -225
- package/dist/esm/tests/json-rpc-socket.spec.js.map +0 -1
- package/dist/esm/tests/plugins/data-store-sqlite.d.ts +0 -17
- package/dist/esm/tests/plugins/data-store-sqlite.d.ts.map +0 -1
- package/dist/esm/tests/plugins/data-store-sqlite.js +0 -23
- package/dist/esm/tests/plugins/data-store-sqlite.js.map +0 -1
- package/dist/esm/tests/plugins/event-log-sqlite.d.ts +0 -17
- package/dist/esm/tests/plugins/event-log-sqlite.d.ts.map +0 -1
- package/dist/esm/tests/plugins/event-log-sqlite.js +0 -23
- package/dist/esm/tests/plugins/event-log-sqlite.js.map +0 -1
- package/dist/esm/tests/plugins/event-stream-in-memory.d.ts +0 -17
- package/dist/esm/tests/plugins/event-stream-in-memory.d.ts.map +0 -1
- package/dist/esm/tests/plugins/event-stream-in-memory.js +0 -21
- package/dist/esm/tests/plugins/event-stream-in-memory.js.map +0 -1
- package/dist/esm/tests/plugins/message-store-sqlite.d.ts +0 -17
- package/dist/esm/tests/plugins/message-store-sqlite.d.ts.map +0 -1
- package/dist/esm/tests/plugins/message-store-sqlite.js +0 -23
- package/dist/esm/tests/plugins/message-store-sqlite.js.map +0 -1
- package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts +0 -17
- package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts.map +0 -1
- package/dist/esm/tests/plugins/resumable-task-store-sqlite.js +0 -23
- package/dist/esm/tests/plugins/resumable-task-store-sqlite.js.map +0 -1
- package/dist/esm/tests/process-handler.spec.d.ts +0 -2
- package/dist/esm/tests/process-handler.spec.d.ts.map +0 -1
- package/dist/esm/tests/process-handler.spec.js +0 -60
- package/dist/esm/tests/process-handler.spec.js.map +0 -1
- package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts +0 -2
- package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts.map +0 -1
- package/dist/esm/tests/registration/proof-of-work-manager.spec.js +0 -157
- package/dist/esm/tests/registration/proof-of-work-manager.spec.js.map +0 -1
- package/dist/esm/tests/rpc-subscribe-close.spec.d.ts +0 -2
- package/dist/esm/tests/rpc-subscribe-close.spec.d.ts.map +0 -1
- package/dist/esm/tests/rpc-subscribe-close.spec.js +0 -81
- package/dist/esm/tests/rpc-subscribe-close.spec.js.map +0 -1
- package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts +0 -2
- package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts.map +0 -1
- package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js +0 -73
- package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js.map +0 -1
- package/dist/esm/tests/scenarios/registration.spec.d.ts +0 -2
- package/dist/esm/tests/scenarios/registration.spec.d.ts.map +0 -1
- package/dist/esm/tests/scenarios/registration.spec.js +0 -507
- package/dist/esm/tests/scenarios/registration.spec.js.map +0 -1
- package/dist/esm/tests/scenarios/web5-connect.spec.d.ts +0 -2
- package/dist/esm/tests/scenarios/web5-connect.spec.d.ts.map +0 -1
- package/dist/esm/tests/scenarios/web5-connect.spec.js +0 -137
- package/dist/esm/tests/scenarios/web5-connect.spec.js.map +0 -1
- package/dist/esm/tests/test-dwn.d.ts +0 -7
- package/dist/esm/tests/test-dwn.d.ts.map +0 -1
- package/dist/esm/tests/test-dwn.js +0 -34
- package/dist/esm/tests/test-dwn.js.map +0 -1
- package/dist/esm/tests/utils.d.ts +0 -46
- package/dist/esm/tests/utils.d.ts.map +0 -1
- package/dist/esm/tests/utils.js +0 -116
- package/dist/esm/tests/utils.js.map +0 -1
- package/dist/esm/tests/ws-api.spec.d.ts +0 -2
- package/dist/esm/tests/ws-api.spec.d.ts.map +0 -1
- package/dist/esm/tests/ws-api.spec.js +0 -327
- package/dist/esm/tests/ws-api.spec.js.map +0 -1
- package/src/json-rpc-socket.ts +0 -155
- package/src/lib/http-server-shutdown-handler.ts +0 -79
- package/src/lib/json-rpc.ts +0 -126
- package/src/registration/proof-of-work-types.ts +0 -7
- package/src/registration/registration-types.ts +0 -18
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import type { NatsConnection } from '@nats-io/transport-node';
|
|
2
|
+
import type { ConsumerMessages, JetStreamClient, JetStreamManager } from '@nats-io/jetstream';
|
|
3
|
+
import type { EventLog, EventLogEntry, EventLogReadOptions, EventLogReadResult, EventLogSubscribeOptions, EventSubscription, Filter, KeyValues, MessageEvent, SubscriptionListener } from '@enbox/dwn-sdk-js';
|
|
4
|
+
|
|
5
|
+
import log from 'loglevel';
|
|
6
|
+
|
|
7
|
+
import { connect } from '@nats-io/transport-node';
|
|
8
|
+
import { AckPolicy, DeliverPolicy, jetstream, jetstreamManager } from '@nats-io/jetstream';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Configuration — all sourced from environment variables
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
type NatsEventLogConfig = {
|
|
15
|
+
/** NATS connection URL(s), comma-separated. */
|
|
16
|
+
url : string;
|
|
17
|
+
/** JetStream stream name. */
|
|
18
|
+
streamName : string;
|
|
19
|
+
/** Max event age in nanoseconds (0 = unlimited). */
|
|
20
|
+
streamMaxAge : number;
|
|
21
|
+
/** JetStream replication factor. */
|
|
22
|
+
replicas : number;
|
|
23
|
+
/** Max messages per subject (per-tenant cap). */
|
|
24
|
+
maxMsgsPerSubject : number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function loadConfig(): NatsEventLogConfig {
|
|
28
|
+
return {
|
|
29
|
+
url : process.env.NATS_URL || 'nats://localhost:4222',
|
|
30
|
+
streamName : process.env.NATS_STREAM_NAME || 'DWN_EVENTS',
|
|
31
|
+
streamMaxAge : parseInt(process.env.NATS_STREAM_MAX_AGE || '604800000000000'), // 7 days in nanos
|
|
32
|
+
replicas : parseInt(process.env.NATS_STREAM_REPLICAS || '1'),
|
|
33
|
+
maxMsgsPerSubject : parseInt(process.env.NATS_MAX_MSGS_PER_SUBJECT || '100000'),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Minimal filter matching (OR semantics, matching FilterUtility behaviour)
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns `true` if the indexed key-values match at least one of the given
|
|
43
|
+
* filters (OR semantics). An empty or undefined filter array matches all events.
|
|
44
|
+
*/
|
|
45
|
+
function matchAnyFilter(keyValues: KeyValues, filters: Filter[] | undefined): boolean {
|
|
46
|
+
if (filters === undefined || filters.length === 0) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
for (const filter of filters) {
|
|
50
|
+
if (matchFilter(keyValues, filter)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Returns `true` if every property in the filter matches the indexed values (AND semantics). */
|
|
58
|
+
function matchFilter(indexedValues: KeyValues, filter: Filter): boolean {
|
|
59
|
+
for (const key in filter) {
|
|
60
|
+
const filterValue = filter[key];
|
|
61
|
+
const indexValue = indexedValues[key];
|
|
62
|
+
if (indexValue === undefined) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const values = Array.isArray(indexValue) ? indexValue : [indexValue];
|
|
67
|
+
let anyMatch = false;
|
|
68
|
+
for (const v of values) {
|
|
69
|
+
if (matchSingleValue(filterValue, v)) {
|
|
70
|
+
anyMatch = true;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (!anyMatch) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Match a single index value against a filter value (equal, oneOf, or range). */
|
|
82
|
+
function matchSingleValue(filterValue: unknown, indexValue: string | number | boolean): boolean {
|
|
83
|
+
if (typeof filterValue === 'object' && filterValue !== null) {
|
|
84
|
+
if (Array.isArray(filterValue)) {
|
|
85
|
+
// OneOfFilter
|
|
86
|
+
return (filterValue as Array<string | number | boolean>).includes(indexValue as never);
|
|
87
|
+
}
|
|
88
|
+
// RangeFilter
|
|
89
|
+
const range = filterValue as { gt?: string | number; gte?: string | number; lt?: string | number; lte?: string | number };
|
|
90
|
+
if (range.lt !== undefined && indexValue >= range.lt) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (range.lte !== undefined && indexValue > range.lte) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (range.gt !== undefined && indexValue <= range.gt) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (range.gte !== undefined && indexValue < range.gte) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// EqualFilter
|
|
105
|
+
return indexValue === filterValue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Payload encoding — JSON over NATS messages
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
type NatsEventPayload = {
|
|
113
|
+
event : MessageEvent;
|
|
114
|
+
indexes : KeyValues;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function encodePayload(payload: NatsEventPayload): Uint8Array {
|
|
118
|
+
return new TextEncoder().encode(JSON.stringify(payload));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function decodePayload(data: Uint8Array): NatsEventPayload | undefined {
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(new TextDecoder().decode(data)) as NatsEventPayload;
|
|
124
|
+
} catch {
|
|
125
|
+
log.error('NatsEventLog: failed to decode payload, skipping corrupt message');
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Subject helpers
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Encodes a tenant DID into a NATS-safe subject token by replacing `.` and `>`
|
|
136
|
+
* (which are NATS subject delimiters) with URL-safe equivalents.
|
|
137
|
+
*/
|
|
138
|
+
function tenantToSubjectToken(tenant: string): string {
|
|
139
|
+
return tenant.replace(/\./g, '~').replace(/>/g, '_');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// NatsEventLog — distributed EventLog implementation over NATS JetStream
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Distributed {@link EventLog} implementation backed by NATS JetStream.
|
|
148
|
+
*
|
|
149
|
+
* Events are published to per-tenant subjects within a single JetStream stream.
|
|
150
|
+
* NATS stream sequence numbers are used as opaque cursors, providing native
|
|
151
|
+
* cursor-based replay and EOSE detection via `msg.info.pending`.
|
|
152
|
+
*
|
|
153
|
+
* Designed for multi-node DWN deployments: node A can emit an event, and a
|
|
154
|
+
* subscriber connected to node B receives it via the shared NATS cluster.
|
|
155
|
+
*
|
|
156
|
+
* Loaded by the DWN server plugin system via `DWN_EVENT_LOG_PLUGIN_PATH`.
|
|
157
|
+
* Must be a default export with a no-arg constructor.
|
|
158
|
+
*/
|
|
159
|
+
export default class NatsEventLog implements EventLog {
|
|
160
|
+
#config: NatsEventLogConfig;
|
|
161
|
+
#nc: NatsConnection | undefined;
|
|
162
|
+
#js: JetStreamClient | undefined;
|
|
163
|
+
#jsm: JetStreamManager | undefined;
|
|
164
|
+
|
|
165
|
+
/** Active subscription consumers, keyed by consumer name. */
|
|
166
|
+
#activeConsumers: Map<string, { messages?: ConsumerMessages; stopped: boolean }> = new Map();
|
|
167
|
+
|
|
168
|
+
constructor() {
|
|
169
|
+
this.#config = loadConfig();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---- Lifecycle -----------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
public async open(): Promise<void> {
|
|
175
|
+
if (this.#nc !== undefined) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const servers = this.#config.url.split(',').map((s: string): string => s.trim());
|
|
180
|
+
this.#nc = await connect({ servers });
|
|
181
|
+
this.#js = jetstream(this.#nc);
|
|
182
|
+
this.#jsm = await jetstreamManager(this.#nc);
|
|
183
|
+
|
|
184
|
+
// Ensure the stream exists (idempotent — update if it already exists).
|
|
185
|
+
await this.#ensureStream();
|
|
186
|
+
|
|
187
|
+
log.info(`NatsEventLog: connected to ${servers.join(', ')}, stream '${this.#config.streamName}' ready`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public async close(): Promise<void> {
|
|
191
|
+
// Stop all active subscription consumers.
|
|
192
|
+
for (const [name, entry] of this.#activeConsumers) {
|
|
193
|
+
entry.stopped = true;
|
|
194
|
+
entry.messages?.stop();
|
|
195
|
+
try {
|
|
196
|
+
await this.#jsm!.consumers.delete(this.#config.streamName, name);
|
|
197
|
+
} catch {
|
|
198
|
+
// Consumer may already be gone (ephemeral timeout).
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
this.#activeConsumers.clear();
|
|
202
|
+
|
|
203
|
+
if (this.#nc !== undefined) {
|
|
204
|
+
await this.#nc.drain();
|
|
205
|
+
this.#nc = undefined;
|
|
206
|
+
this.#js = undefined;
|
|
207
|
+
this.#jsm = undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- emit ----------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
public async emit(tenant: string, event: MessageEvent, indexes: KeyValues): Promise<string> {
|
|
214
|
+
this.#assertOpen();
|
|
215
|
+
|
|
216
|
+
const subject = this.#tenantSubject(tenant);
|
|
217
|
+
const data = encodePayload({ event, indexes });
|
|
218
|
+
const ack = await this.#js!.publish(subject, data);
|
|
219
|
+
return String(ack.seq);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---- read ----------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
public async read(tenant: string, options: EventLogReadOptions = {}): Promise<EventLogReadResult> {
|
|
225
|
+
this.#assertOpen();
|
|
226
|
+
|
|
227
|
+
const { cursor, limit, filters } = options;
|
|
228
|
+
const subject = this.#tenantSubject(tenant);
|
|
229
|
+
|
|
230
|
+
// Create a one-shot ordered consumer for the read.
|
|
231
|
+
const consumerOpts: Record<string, unknown> = {
|
|
232
|
+
filter_subject : subject,
|
|
233
|
+
ack_policy : AckPolicy.None, // ordered consumers use AckNone
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (cursor !== undefined) {
|
|
237
|
+
consumerOpts.deliver_policy = DeliverPolicy.StartSequence;
|
|
238
|
+
consumerOpts.opt_start_seq = Number(cursor) + 1;
|
|
239
|
+
} else {
|
|
240
|
+
consumerOpts.deliver_policy = DeliverPolicy.All;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const consumer = await this.#jsm!.consumers.add(this.#config.streamName, consumerOpts);
|
|
244
|
+
const maxResults = limit ?? Number.MAX_SAFE_INTEGER;
|
|
245
|
+
|
|
246
|
+
const events: EventLogEntry[] = [];
|
|
247
|
+
let lastCursor: string | undefined;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const messages = await this.#js!.consumers.get(this.#config.streamName, consumer.name);
|
|
251
|
+
const iter = await messages.fetch({ max_messages: maxResults, expires: 2_000 });
|
|
252
|
+
|
|
253
|
+
for await (const msg of iter) {
|
|
254
|
+
const payload = decodePayload(msg.data);
|
|
255
|
+
if (payload === undefined) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!matchAnyFilter(payload.indexes, filters)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
events.push({
|
|
264
|
+
seq : msg.seq,
|
|
265
|
+
event : payload.event,
|
|
266
|
+
indexes : payload.indexes,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
lastCursor = String(msg.seq);
|
|
270
|
+
|
|
271
|
+
if (events.length >= maxResults) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} finally {
|
|
276
|
+
// Clean up the one-shot consumer.
|
|
277
|
+
try {
|
|
278
|
+
await this.#jsm!.consumers.delete(this.#config.streamName, consumer.name);
|
|
279
|
+
} catch {
|
|
280
|
+
// May already be cleaned up.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
events,
|
|
286
|
+
cursor: lastCursor ?? cursor,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---- subscribe -----------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
public async subscribe(
|
|
293
|
+
tenant: string,
|
|
294
|
+
id: string,
|
|
295
|
+
listener: SubscriptionListener,
|
|
296
|
+
options?: EventLogSubscribeOptions,
|
|
297
|
+
): Promise<EventSubscription> {
|
|
298
|
+
this.#assertOpen();
|
|
299
|
+
|
|
300
|
+
const subject = this.#tenantSubject(tenant);
|
|
301
|
+
const { cursor, filters } = options ?? {};
|
|
302
|
+
|
|
303
|
+
// Build the consumer config.
|
|
304
|
+
const consumerName = `sub-${id}`;
|
|
305
|
+
const consumerOpts: Record<string, unknown> = {
|
|
306
|
+
name : consumerName,
|
|
307
|
+
filter_subject : subject,
|
|
308
|
+
ack_policy : AckPolicy.Explicit,
|
|
309
|
+
inactive_threshold : 60_000_000_000, // 60 seconds in nanos
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (cursor !== undefined) {
|
|
313
|
+
consumerOpts.deliver_policy = DeliverPolicy.StartSequence;
|
|
314
|
+
consumerOpts.opt_start_seq = Number(cursor) + 1;
|
|
315
|
+
} else {
|
|
316
|
+
consumerOpts.deliver_policy = DeliverPolicy.New;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
await this.#jsm!.consumers.add(this.#config.streamName, consumerOpts);
|
|
320
|
+
|
|
321
|
+
const entry: { messages?: ConsumerMessages; stopped: boolean } = { stopped: false };
|
|
322
|
+
this.#activeConsumers.set(consumerName, entry);
|
|
323
|
+
|
|
324
|
+
// Start the consume loop asynchronously.
|
|
325
|
+
const consumeLoop = async (): Promise<void> => {
|
|
326
|
+
let sentEose = cursor === undefined; // no cursor → no EOSE needed
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const consumer = await this.#js!.consumers.get(this.#config.streamName, consumerName);
|
|
330
|
+
const messages = await consumer.consume();
|
|
331
|
+
entry.messages = messages;
|
|
332
|
+
|
|
333
|
+
for await (const msg of messages) {
|
|
334
|
+
if (entry.stopped) {
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const payload = decodePayload(msg.data);
|
|
339
|
+
if (payload === undefined) {
|
|
340
|
+
msg.ack();
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!matchAnyFilter(payload.indexes, filters)) {
|
|
345
|
+
msg.ack();
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const eventCursor = String(msg.seq);
|
|
350
|
+
listener({ type: 'event', cursor: eventCursor, event: payload.event });
|
|
351
|
+
msg.ack();
|
|
352
|
+
|
|
353
|
+
// EOSE detection: when pending reaches 0, all stored events have been
|
|
354
|
+
// delivered and we transition to live mode.
|
|
355
|
+
if (!sentEose && msg.info.pending === 0) {
|
|
356
|
+
listener({ type: 'eose', cursor: eventCursor });
|
|
357
|
+
sentEose = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch (err) {
|
|
361
|
+
if (!entry.stopped) {
|
|
362
|
+
log.error(`NatsEventLog: consume loop error for subscription '${id}'`, err);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// Fire and forget — the loop runs until stop or connection close.
|
|
368
|
+
consumeLoop();
|
|
369
|
+
|
|
370
|
+
// Handle the edge case where cursor was provided but there are zero
|
|
371
|
+
// stored events after it. The consume loop won't receive any messages,
|
|
372
|
+
// so we need to send EOSE proactively. We check consumer info after a
|
|
373
|
+
// short delay to allow the loop to start.
|
|
374
|
+
if (cursor !== undefined) {
|
|
375
|
+
setTimeout(async (): Promise<void> => {
|
|
376
|
+
if (entry.stopped) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
try {
|
|
380
|
+
const info = await this.#jsm!.consumers.info(this.#config.streamName, consumerName);
|
|
381
|
+
if (info.num_pending === 0 && info.delivered.stream_seq <= Number(cursor)) {
|
|
382
|
+
listener({ type: 'eose', cursor });
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
// Consumer may be gone already.
|
|
386
|
+
}
|
|
387
|
+
}, 50);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
id,
|
|
392
|
+
close: async (): Promise<void> => {
|
|
393
|
+
entry.stopped = true;
|
|
394
|
+
entry.messages?.stop();
|
|
395
|
+
this.#activeConsumers.delete(consumerName);
|
|
396
|
+
try {
|
|
397
|
+
await this.#jsm!.consumers.delete(this.#config.streamName, consumerName);
|
|
398
|
+
} catch {
|
|
399
|
+
// Consumer may already be gone (ephemeral timeout).
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ---- trim ----------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
public async trim(tenant: string, olderThan: number | string): Promise<void> {
|
|
408
|
+
this.#assertOpen();
|
|
409
|
+
|
|
410
|
+
const subject = this.#tenantSubject(tenant);
|
|
411
|
+
|
|
412
|
+
if (typeof olderThan === 'number') {
|
|
413
|
+
// Purge events with sequence < olderThan.
|
|
414
|
+
await this.#jsm!.streams.purge(this.#config.streamName, {
|
|
415
|
+
filter : subject,
|
|
416
|
+
seq : olderThan,
|
|
417
|
+
});
|
|
418
|
+
} else {
|
|
419
|
+
// Timestamp-based trim: purge events older than the given ISO-8601 time.
|
|
420
|
+
// NATS stream purge doesn't support timestamp-based purging natively, so
|
|
421
|
+
// we find the sequence threshold by reading events and checking timestamps.
|
|
422
|
+
// For simplicity, we do a full purge of the subject if olderThan is provided
|
|
423
|
+
// as a string — this matches the EventEmitterEventLog behaviour of deleting
|
|
424
|
+
// entries whose messageTimestamp is before the given time.
|
|
425
|
+
// A more precise implementation could binary-search for the sequence cutoff.
|
|
426
|
+
await this.#jsm!.streams.purge(this.#config.streamName, {
|
|
427
|
+
filter: subject,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ---- Private helpers -----------------------------------------------------
|
|
433
|
+
|
|
434
|
+
#tenantSubject(tenant: string): string {
|
|
435
|
+
return `dwn.events.${tenantToSubjectToken(tenant)}`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#assertOpen(): void {
|
|
439
|
+
if (this.#nc === undefined || this.#js === undefined || this.#jsm === undefined) {
|
|
440
|
+
throw new Error('NatsEventLog: not open. Call open() before using.');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async #ensureStream(): Promise<void> {
|
|
445
|
+
const cfg = this.#config;
|
|
446
|
+
try {
|
|
447
|
+
await this.#jsm!.streams.info(cfg.streamName);
|
|
448
|
+
// Stream exists — update config if needed.
|
|
449
|
+
await this.#jsm!.streams.update(cfg.streamName, {
|
|
450
|
+
subjects : ['dwn.events.>'],
|
|
451
|
+
max_age : cfg.streamMaxAge,
|
|
452
|
+
num_replicas : cfg.replicas,
|
|
453
|
+
max_msgs_per_subject : cfg.maxMsgsPerSubject,
|
|
454
|
+
});
|
|
455
|
+
} catch {
|
|
456
|
+
// Stream does not exist — create it.
|
|
457
|
+
await this.#jsm!.streams.add({
|
|
458
|
+
name : cfg.streamName,
|
|
459
|
+
subjects : ['dwn.events.>'],
|
|
460
|
+
max_age : cfg.streamMaxAge,
|
|
461
|
+
num_replicas : cfg.replicas,
|
|
462
|
+
max_msgs_per_subject : cfg.maxMsgsPerSubject,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
package/src/process-handlers.ts
CHANGED
|
@@ -51,11 +51,11 @@ export const setProcessHandlers = (dwnServer: DwnServer): ProcessHandlers => {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
export const removeProcessHandlers = (handlers: ProcessHandlers): void => {
|
|
54
|
-
const {
|
|
55
|
-
unhandledRejectionHandler,
|
|
56
|
-
uncaughtExceptionHandler,
|
|
57
|
-
sigintHandler,
|
|
58
|
-
sigtermHandler
|
|
54
|
+
const {
|
|
55
|
+
unhandledRejectionHandler,
|
|
56
|
+
uncaughtExceptionHandler,
|
|
57
|
+
sigintHandler,
|
|
58
|
+
sigtermHandler
|
|
59
59
|
} = handlers;
|
|
60
60
|
|
|
61
61
|
process.removeListener('unhandledRejection', unhandledRejectionHandler);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token-bucket rate limiter with per-key state tracking and automatic cleanup.
|
|
3
|
+
*
|
|
4
|
+
* Each key (IP address or tenant DID) has its own bucket that refills at a
|
|
5
|
+
* fixed rate. When a request arrives, a token is consumed. If the bucket is
|
|
6
|
+
* empty, the request is rejected with a retry-after hint.
|
|
7
|
+
*
|
|
8
|
+
* Stale buckets are periodically purged to prevent unbounded memory growth.
|
|
9
|
+
*
|
|
10
|
+
* @see https://github.com/enboxorg/enbox/issues/326
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type RateLimiterConfig = {
|
|
14
|
+
/** Tokens added per second (i.e. sustained request rate). */
|
|
15
|
+
refillRate : number;
|
|
16
|
+
/** Maximum burst size (bucket capacity). */
|
|
17
|
+
maxTokens : number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type Bucket = {
|
|
21
|
+
tokens : number;
|
|
22
|
+
lastRefill : number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class RateLimiter {
|
|
26
|
+
#refillRate: number;
|
|
27
|
+
#maxTokens: number;
|
|
28
|
+
#buckets: Map<string, Bucket> = new Map();
|
|
29
|
+
#cleanupInterval: ReturnType<typeof setInterval> | undefined;
|
|
30
|
+
|
|
31
|
+
/** Stale buckets older than 5 minutes are purged. */
|
|
32
|
+
private static readonly STALE_THRESHOLD_MS = 5 * 60 * 1000;
|
|
33
|
+
/** Cleanup runs every 60 seconds. */
|
|
34
|
+
private static readonly CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
35
|
+
|
|
36
|
+
public constructor(config: RateLimiterConfig) {
|
|
37
|
+
this.#refillRate = config.refillRate;
|
|
38
|
+
this.#maxTokens = config.maxTokens;
|
|
39
|
+
|
|
40
|
+
// Start periodic cleanup.
|
|
41
|
+
this.#cleanupInterval = setInterval((): void => {
|
|
42
|
+
this.#cleanup();
|
|
43
|
+
}, RateLimiter.CLEANUP_INTERVAL_MS);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Attempts to consume a token for the given key.
|
|
48
|
+
* @returns `{ allowed: true }` if the request is permitted, or
|
|
49
|
+
* `{ allowed: false, retryAfterMs }` if the rate limit is exceeded.
|
|
50
|
+
*/
|
|
51
|
+
public consume(key: string): { allowed: true } | { allowed: false; retryAfterMs: number } {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
let bucket = this.#buckets.get(key);
|
|
54
|
+
|
|
55
|
+
if (bucket === undefined) {
|
|
56
|
+
bucket = { tokens: this.#maxTokens, lastRefill: now };
|
|
57
|
+
this.#buckets.set(key, bucket);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Refill tokens based on elapsed time.
|
|
61
|
+
const elapsed = (now - bucket.lastRefill) / 1000;
|
|
62
|
+
bucket.tokens = Math.min(this.#maxTokens, bucket.tokens + elapsed * this.#refillRate);
|
|
63
|
+
bucket.lastRefill = now;
|
|
64
|
+
|
|
65
|
+
if (bucket.tokens >= 1) {
|
|
66
|
+
bucket.tokens -= 1;
|
|
67
|
+
return { allowed: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Calculate how long until one token is available.
|
|
71
|
+
const deficit = 1 - bucket.tokens;
|
|
72
|
+
const retryAfterMs = Math.ceil((deficit / this.#refillRate) * 1000);
|
|
73
|
+
return { allowed: false, retryAfterMs };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns the number of keys currently being tracked.
|
|
78
|
+
*/
|
|
79
|
+
public get size(): number {
|
|
80
|
+
return this.#buckets.size;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns the current token count for a key, or `undefined` if not tracked.
|
|
85
|
+
*/
|
|
86
|
+
public getTokens(key: string): number | undefined {
|
|
87
|
+
const bucket = this.#buckets.get(key);
|
|
88
|
+
if (bucket === undefined) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Refill to return current state.
|
|
93
|
+
const elapsed = (Date.now() - bucket.lastRefill) / 1000;
|
|
94
|
+
return Math.min(this.#maxTokens, bucket.tokens + elapsed * this.#refillRate);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Reconfigures the rate limiter with new settings. Existing buckets are
|
|
99
|
+
* retained but will use the new `refillRate` and `maxTokens` going forward.
|
|
100
|
+
*
|
|
101
|
+
* @see https://github.com/enboxorg/enbox/issues/389
|
|
102
|
+
*/
|
|
103
|
+
public reconfigure(config: RateLimiterConfig): void {
|
|
104
|
+
this.#refillRate = config.refillRate;
|
|
105
|
+
this.#maxTokens = config.maxTokens;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns the current configuration of this rate limiter.
|
|
110
|
+
*/
|
|
111
|
+
public get config(): RateLimiterConfig {
|
|
112
|
+
return {
|
|
113
|
+
refillRate : this.#refillRate,
|
|
114
|
+
maxTokens : this.#maxTokens,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Stops the periodic cleanup timer. Call this when shutting down.
|
|
120
|
+
*/
|
|
121
|
+
public destroy(): void {
|
|
122
|
+
if (this.#cleanupInterval) {
|
|
123
|
+
clearInterval(this.#cleanupInterval);
|
|
124
|
+
this.#cleanupInterval = undefined;
|
|
125
|
+
}
|
|
126
|
+
this.#buckets.clear();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Removes buckets that have been at max capacity for longer than the stale threshold.
|
|
131
|
+
*/
|
|
132
|
+
#cleanup(): void {
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
for (const [key, bucket] of this.#buckets) {
|
|
135
|
+
const elapsed = (now - bucket.lastRefill) / 1000;
|
|
136
|
+
const currentTokens = Math.min(this.#maxTokens, bucket.tokens + elapsed * this.#refillRate);
|
|
137
|
+
const timeSinceLastUse = now - bucket.lastRefill;
|
|
138
|
+
if (currentTokens >= this.#maxTokens && timeSinceLastUse > RateLimiter.STALE_THRESHOLD_MS) {
|
|
139
|
+
this.#buckets.delete(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|