@valentinkolb/sync 2.2.0 → 3.0.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.
@@ -0,0 +1,165 @@
1
+ // src/browser/store.ts
2
+ class MemoryStore {
3
+ data = new Map;
4
+ timers = new Map;
5
+ get(key) {
6
+ const entry = this.data.get(key);
7
+ if (!entry)
8
+ return;
9
+ if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {
10
+ this.del(key);
11
+ return;
12
+ }
13
+ return entry.value;
14
+ }
15
+ set(key, value, ttlMs) {
16
+ const existingTimer = this.timers.get(key);
17
+ if (existingTimer) {
18
+ clearTimeout(existingTimer);
19
+ this.timers.delete(key);
20
+ }
21
+ const expiresAt = ttlMs != null && ttlMs > 0 ? Date.now() + ttlMs : null;
22
+ this.data.set(key, { value, expiresAt });
23
+ if (ttlMs != null && ttlMs > 0) {
24
+ this.timers.set(key, setTimeout(() => this.del(key), ttlMs));
25
+ }
26
+ }
27
+ del(key) {
28
+ this.data.delete(key);
29
+ const timer = this.timers.get(key);
30
+ if (timer) {
31
+ clearTimeout(timer);
32
+ this.timers.delete(key);
33
+ }
34
+ }
35
+ keys(prefix) {
36
+ const now = Date.now();
37
+ const result = [];
38
+ for (const [key, entry] of this.data) {
39
+ if (entry.expiresAt !== null && now >= entry.expiresAt) {
40
+ this.del(key);
41
+ continue;
42
+ }
43
+ if (prefix === undefined || key.startsWith(prefix)) {
44
+ result.push(key);
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+ clear() {
50
+ for (const timer of this.timers.values()) {
51
+ clearTimeout(timer);
52
+ }
53
+ this.timers.clear();
54
+ this.data.clear();
55
+ }
56
+ }
57
+ var createMemoryStore = () => new MemoryStore;
58
+
59
+ // src/browser/internal/sleep.ts
60
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
61
+
62
+ // src/browser/internal/id.ts
63
+ var randomHex = (bytes) => {
64
+ const buf = new Uint8Array(bytes);
65
+ crypto.getRandomValues(buf);
66
+ return Array.from(buf, (b) => b.toString(16).padStart(2, "0")).join("");
67
+ };
68
+ var simpleHash = (str) => {
69
+ let hash = 5381;
70
+ for (let i = 0;i < str.length; i++) {
71
+ hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
72
+ }
73
+ return `hash:${(hash >>> 0).toString(16)}`;
74
+ };
75
+
76
+ // src/browser/mutex.ts
77
+ var DEFAULT_PREFIX = "sync:mutex";
78
+ var DEFAULT_RETRY_COUNT = 10;
79
+ var DEFAULT_RETRY_DELAY = 200;
80
+ var DEFAULT_TTL = 1e4;
81
+ var MAX_RESOURCE_LENGTH = 128;
82
+ var normalizeResource = (resource) => {
83
+ if (resource.length <= MAX_RESOURCE_LENGTH)
84
+ return resource;
85
+ return simpleHash(resource);
86
+ };
87
+
88
+ class LockError extends Error {
89
+ resource;
90
+ constructor(resource) {
91
+ super(`Failed to acquire lock on resource: ${resource}`);
92
+ this.name = "LockError";
93
+ this.resource = resource;
94
+ }
95
+ }
96
+ var mutex = (config) => {
97
+ const prefix = config.prefix ?? DEFAULT_PREFIX;
98
+ const retryCount = config.retryCount ?? DEFAULT_RETRY_COUNT;
99
+ const retryDelay = config.retryDelay ?? DEFAULT_RETRY_DELAY;
100
+ const defaultTtl = config.defaultTtl ?? DEFAULT_TTL;
101
+ const store = config.store ?? createMemoryStore();
102
+ const acquire = async (resource, ttl = defaultTtl) => {
103
+ const safeResource = normalizeResource(resource);
104
+ const key = `${prefix}:${config.id}:${safeResource}`;
105
+ const value = randomHex(16);
106
+ for (let attempt = 0;attempt <= retryCount; attempt++) {
107
+ const existing = store.get(key);
108
+ if (existing === undefined) {
109
+ store.set(key, value, ttl);
110
+ return {
111
+ resource: key,
112
+ value,
113
+ ttl,
114
+ expiration: Date.now() + ttl
115
+ };
116
+ }
117
+ if (attempt < retryCount) {
118
+ await sleep(retryDelay + Math.random() * 100);
119
+ }
120
+ }
121
+ return null;
122
+ };
123
+ const release = async (lock) => {
124
+ const current = store.get(lock.resource);
125
+ if (current === lock.value) {
126
+ store.del(lock.resource);
127
+ }
128
+ };
129
+ const extend = async (lock, ttl = defaultTtl) => {
130
+ const current = store.get(lock.resource);
131
+ if (current === lock.value) {
132
+ store.set(lock.resource, lock.value, ttl);
133
+ lock.ttl = ttl;
134
+ lock.expiration = Date.now() + ttl;
135
+ return true;
136
+ }
137
+ return false;
138
+ };
139
+ const withLock = async (resource, fn, ttl) => {
140
+ const lock = await acquire(resource, ttl);
141
+ if (!lock)
142
+ return null;
143
+ try {
144
+ return await fn(lock);
145
+ } finally {
146
+ await release(lock);
147
+ }
148
+ };
149
+ const withLockOrThrow = async (resource, fn, ttl) => {
150
+ const lock = await acquire(resource, ttl);
151
+ if (!lock) {
152
+ throw new LockError(resource);
153
+ }
154
+ try {
155
+ return await fn(lock);
156
+ } finally {
157
+ await release(lock);
158
+ }
159
+ };
160
+ return { id: config.id, acquire, release, withLock, withLockOrThrow, extend };
161
+ };
162
+ export {
163
+ mutex,
164
+ LockError
165
+ };
@@ -0,0 +1,342 @@
1
+ // src/browser/internal/emitter.ts
2
+ class Emitter {
3
+ listeners = new Set;
4
+ on(fn) {
5
+ this.listeners.add(fn);
6
+ return () => {
7
+ this.listeners.delete(fn);
8
+ };
9
+ }
10
+ emit(value) {
11
+ for (const fn of this.listeners) {
12
+ fn(value);
13
+ }
14
+ }
15
+ once() {
16
+ return new Promise((resolve) => {
17
+ const unsub = this.on((value) => {
18
+ unsub();
19
+ resolve(value);
20
+ });
21
+ });
22
+ }
23
+ onceWithSignal(signal) {
24
+ if (!signal)
25
+ return this.once();
26
+ if (signal.aborted)
27
+ return Promise.reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
28
+ return new Promise((resolve, reject) => {
29
+ const unsub = this.on((value) => {
30
+ unsub();
31
+ signal.removeEventListener("abort", onAbort);
32
+ resolve(value);
33
+ });
34
+ const onAbort = () => {
35
+ unsub();
36
+ signal.removeEventListener("abort", onAbort);
37
+ reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
38
+ };
39
+ signal.addEventListener("abort", onAbort, { once: true });
40
+ });
41
+ }
42
+ }
43
+
44
+ // src/browser/internal/id.ts
45
+ var randomId = () => crypto.randomUUID();
46
+
47
+ // src/browser/internal/sleep.ts
48
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
49
+
50
+ // src/browser/queue.ts
51
+ var DAY_MS = 24 * 60 * 60 * 1000;
52
+ var DEFAULT_PREFIX = "sync:queue";
53
+ var DEFAULT_TENANT = "default";
54
+ var DEFAULT_LEASE_MS = 30000;
55
+ var DEFAULT_WAIT_TIMEOUT_MS = 30000;
56
+ var DEFAULT_MAX_DELIVERIES = 10;
57
+ var DEFAULT_MAX_NACK_DELAY_MS = 7 * DAY_MS;
58
+ var DEFAULT_MAX_MESSAGE_AGE_MS = 7 * DAY_MS;
59
+ var DEFAULT_DLQ_RETENTION_MS = 7 * DAY_MS;
60
+ var DEFAULT_IDEMPOTENCY_TTL_MS = 7 * DAY_MS;
61
+ var DEFAULT_PAYLOAD_BYTES = 128 * 1024;
62
+ var DEFAULT_MAINTENANCE_INTERVAL_MS = 1000;
63
+ var textEncoder = new TextEncoder;
64
+ var queue = (config) => {
65
+ const prefix = config.prefix ?? DEFAULT_PREFIX;
66
+ const defaultTenant = config.tenantId ?? DEFAULT_TENANT;
67
+ const defaultLeaseMs = config.delivery?.defaultLeaseMs ?? DEFAULT_LEASE_MS;
68
+ const maxDeliveries = config.delivery?.maxDeliveries ?? DEFAULT_MAX_DELIVERIES;
69
+ const maxPayloadBytes = config.limits?.payloadBytes ?? DEFAULT_PAYLOAD_BYTES;
70
+ const maxNackDelayMs = config.limits?.maxNackDelayMs ?? DEFAULT_MAX_NACK_DELAY_MS;
71
+ const maxMessageAgeMs = config.limits?.maxMessageAgeMs ?? DEFAULT_MAX_MESSAGE_AGE_MS;
72
+ const dlqRetentionMs = config.limits?.dlqRetentionMs ?? DEFAULT_DLQ_RETENTION_MS;
73
+ const resolveTenant = (tenantId) => tenantId ?? defaultTenant;
74
+ const states = new Map;
75
+ const getState = (tenantId) => {
76
+ const key = `${prefix}:${tenantId}:${config.id}`;
77
+ let state = states.get(key);
78
+ if (!state) {
79
+ state = {
80
+ seq: 0,
81
+ ready: [],
82
+ delayed: new Map,
83
+ messages: new Map,
84
+ deliveries: new Map,
85
+ leases: new Map,
86
+ dlq: new Map,
87
+ idempotency: new Map,
88
+ emitter: new Emitter,
89
+ lastMaintenance: 0
90
+ };
91
+ states.set(key, state);
92
+ }
93
+ return state;
94
+ };
95
+ const runMaintenance = (state) => {
96
+ const now = Date.now();
97
+ if (now - state.lastMaintenance < DEFAULT_MAINTENANCE_INTERVAL_MS)
98
+ return;
99
+ state.lastMaintenance = now;
100
+ for (const [messageId, deliverAt] of state.delayed) {
101
+ if (deliverAt <= now) {
102
+ state.delayed.delete(messageId);
103
+ const msg = state.messages.get(messageId);
104
+ if (msg) {
105
+ if (now - msg.enqueuedAt > maxMessageAgeMs) {
106
+ moveToDlq(state, messageId, msg, "expired");
107
+ } else {
108
+ state.ready.push(messageId);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ for (const [deliveryId, leaseUntil] of state.leases) {
114
+ if (leaseUntil <= now) {
115
+ const delivery = state.deliveries.get(deliveryId);
116
+ state.leases.delete(deliveryId);
117
+ state.deliveries.delete(deliveryId);
118
+ if (delivery) {
119
+ const msg = state.messages.get(delivery.messageId);
120
+ if (msg) {
121
+ if (msg.attempt >= maxDeliveries) {
122
+ moveToDlq(state, delivery.messageId, msg, "max_deliveries");
123
+ } else {
124
+ state.ready.push(delivery.messageId);
125
+ state.emitter.emit();
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ for (const [messageId, dlq] of state.dlq) {
132
+ if (now - dlq.movedAt > dlqRetentionMs) {
133
+ state.dlq.delete(messageId);
134
+ }
135
+ }
136
+ for (const [key, entry] of state.idempotency) {
137
+ if (now >= entry.expiresAt) {
138
+ state.idempotency.delete(key);
139
+ }
140
+ }
141
+ };
142
+ const moveToDlq = (state, messageId, msg, reason) => {
143
+ state.dlq.set(messageId, {
144
+ messageId,
145
+ data: msg.data,
146
+ orderingKey: msg.orderingKey,
147
+ meta: msg.meta,
148
+ attempts: msg.attempt,
149
+ movedAt: Date.now(),
150
+ reason
151
+ });
152
+ state.messages.delete(messageId);
153
+ };
154
+ const send = async (sendCfg) => {
155
+ const tenantId = resolveTenant(sendCfg.tenantId);
156
+ const state = getState(tenantId);
157
+ const parsed = config.schema.safeParse(sendCfg.data);
158
+ if (!parsed.success)
159
+ throw parsed.error;
160
+ const payloadRaw = JSON.stringify(parsed.data);
161
+ const payloadBytes = textEncoder.encode(payloadRaw).byteLength;
162
+ if (payloadBytes > maxPayloadBytes) {
163
+ throw new Error(`payload exceeds limit (${maxPayloadBytes} bytes)`);
164
+ }
165
+ if (sendCfg.idempotencyKey) {
166
+ const existing = state.idempotency.get(sendCfg.idempotencyKey);
167
+ if (existing && Date.now() < existing.expiresAt) {
168
+ return { messageId: existing.messageId };
169
+ }
170
+ }
171
+ const messageId = String(++state.seq);
172
+ const msg = {
173
+ data: parsed.data,
174
+ orderingKey: sendCfg.orderingKey,
175
+ meta: sendCfg.meta,
176
+ enqueuedAt: Date.now(),
177
+ attempt: 0
178
+ };
179
+ state.messages.set(messageId, msg);
180
+ if (sendCfg.idempotencyKey) {
181
+ state.idempotency.set(sendCfg.idempotencyKey, {
182
+ messageId,
183
+ expiresAt: Date.now() + (sendCfg.idempotencyTtlMs ?? DEFAULT_IDEMPOTENCY_TTL_MS)
184
+ });
185
+ }
186
+ const delayMs = sendCfg.delayMs ?? 0;
187
+ if (delayMs > 0) {
188
+ state.delayed.set(messageId, Date.now() + delayMs);
189
+ } else {
190
+ state.ready.push(messageId);
191
+ state.emitter.emit();
192
+ }
193
+ return { messageId };
194
+ };
195
+ const createReader = () => {
196
+ const recv = async (recvCfg = {}) => {
197
+ const tenantId = resolveTenant(recvCfg.tenantId);
198
+ const state = getState(tenantId);
199
+ const wait = recvCfg.wait ?? true;
200
+ const timeoutMs = recvCfg.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
201
+ const leaseMs = recvCfg.leaseMs ?? defaultLeaseMs;
202
+ runMaintenance(state);
203
+ const claimed = claimNext(state, leaseMs);
204
+ if (claimed)
205
+ return claimed;
206
+ if (!wait)
207
+ return null;
208
+ const deadline = Date.now() + timeoutMs;
209
+ while (!recvCfg.signal?.aborted) {
210
+ const remaining = deadline - Date.now();
211
+ if (remaining <= 0)
212
+ break;
213
+ const raceTimeout = Math.min(remaining, 1000);
214
+ await Promise.race([
215
+ state.emitter.onceWithSignal(recvCfg.signal),
216
+ sleep(raceTimeout)
217
+ ]).catch(() => {});
218
+ runMaintenance(state);
219
+ const claimed2 = claimNext(state, leaseMs);
220
+ if (claimed2)
221
+ return claimed2;
222
+ }
223
+ return null;
224
+ };
225
+ const claimNext = (state, leaseMs) => {
226
+ while (state.ready.length > 0) {
227
+ const messageId = state.ready.shift();
228
+ const msg = state.messages.get(messageId);
229
+ if (!msg)
230
+ continue;
231
+ if (Date.now() - msg.enqueuedAt > maxMessageAgeMs) {
232
+ moveToDlq(state, messageId, msg, "expired");
233
+ continue;
234
+ }
235
+ msg.attempt++;
236
+ const deliveryId = `${messageId}:${randomId()}`;
237
+ const leaseUntil = Date.now() + leaseMs;
238
+ const delivery = {
239
+ messageId,
240
+ deliveryId,
241
+ leaseUntil,
242
+ attempt: msg.attempt
243
+ };
244
+ state.deliveries.set(deliveryId, delivery);
245
+ state.leases.set(deliveryId, leaseUntil);
246
+ return buildReceived(state, messageId, deliveryId, msg, delivery);
247
+ }
248
+ return null;
249
+ };
250
+ const buildReceived = (state, messageId, deliveryId, msg, delivery) => {
251
+ const parsed = config.schema.safeParse(msg.data);
252
+ if (!parsed.success) {
253
+ state.deliveries.delete(deliveryId);
254
+ state.leases.delete(deliveryId);
255
+ state.messages.delete(messageId);
256
+ return null;
257
+ }
258
+ let settled = false;
259
+ const ack = async () => {
260
+ if (settled)
261
+ return false;
262
+ if (!state.deliveries.has(deliveryId))
263
+ return false;
264
+ settled = true;
265
+ state.deliveries.delete(deliveryId);
266
+ state.leases.delete(deliveryId);
267
+ state.messages.delete(messageId);
268
+ return true;
269
+ };
270
+ const nack = async (cfg) => {
271
+ if (settled)
272
+ return false;
273
+ if (!state.deliveries.has(deliveryId))
274
+ return false;
275
+ settled = true;
276
+ state.deliveries.delete(deliveryId);
277
+ state.leases.delete(deliveryId);
278
+ if (msg.attempt >= maxDeliveries) {
279
+ moveToDlq(state, messageId, msg, cfg?.reason ?? "max_deliveries");
280
+ return true;
281
+ }
282
+ const delayMs = cfg?.delayMs ?? 0;
283
+ if (delayMs > maxNackDelayMs) {
284
+ throw new Error(`nack delayMs (${delayMs}) exceeds maxNackDelayMs (${maxNackDelayMs})`);
285
+ }
286
+ if (delayMs > 0) {
287
+ state.delayed.set(messageId, Date.now() + delayMs);
288
+ } else {
289
+ state.ready.push(messageId);
290
+ state.emitter.emit();
291
+ }
292
+ return true;
293
+ };
294
+ const touch = async (cfg) => {
295
+ if (settled)
296
+ return false;
297
+ if (!state.deliveries.has(deliveryId))
298
+ return false;
299
+ const newLeaseMs = cfg?.leaseMs ?? defaultLeaseMs;
300
+ const newLeaseUntil = Date.now() + newLeaseMs;
301
+ delivery.leaseUntil = newLeaseUntil;
302
+ state.leases.set(deliveryId, newLeaseUntil);
303
+ return true;
304
+ };
305
+ return {
306
+ data: parsed.data,
307
+ messageId,
308
+ deliveryId,
309
+ attempt: msg.attempt,
310
+ leaseUntil: delivery.leaseUntil,
311
+ orderingKey: msg.orderingKey,
312
+ meta: msg.meta,
313
+ ack,
314
+ nack,
315
+ touch
316
+ };
317
+ };
318
+ const stream = async function* (streamCfg = {}) {
319
+ const wait = streamCfg.wait ?? true;
320
+ while (!streamCfg.signal?.aborted) {
321
+ const message = await recv(streamCfg);
322
+ if (message) {
323
+ yield message;
324
+ continue;
325
+ }
326
+ if (!wait)
327
+ break;
328
+ }
329
+ };
330
+ return { recv, stream };
331
+ };
332
+ const defaultReader = createReader();
333
+ return {
334
+ send,
335
+ recv: defaultReader.recv,
336
+ stream: defaultReader.stream,
337
+ reader: () => createReader()
338
+ };
339
+ };
340
+ export {
341
+ queue
342
+ };
@@ -0,0 +1,124 @@
1
+ // src/browser/store.ts
2
+ class MemoryStore {
3
+ data = new Map;
4
+ timers = new Map;
5
+ get(key) {
6
+ const entry = this.data.get(key);
7
+ if (!entry)
8
+ return;
9
+ if (entry.expiresAt !== null && Date.now() >= entry.expiresAt) {
10
+ this.del(key);
11
+ return;
12
+ }
13
+ return entry.value;
14
+ }
15
+ set(key, value, ttlMs) {
16
+ const existingTimer = this.timers.get(key);
17
+ if (existingTimer) {
18
+ clearTimeout(existingTimer);
19
+ this.timers.delete(key);
20
+ }
21
+ const expiresAt = ttlMs != null && ttlMs > 0 ? Date.now() + ttlMs : null;
22
+ this.data.set(key, { value, expiresAt });
23
+ if (ttlMs != null && ttlMs > 0) {
24
+ this.timers.set(key, setTimeout(() => this.del(key), ttlMs));
25
+ }
26
+ }
27
+ del(key) {
28
+ this.data.delete(key);
29
+ const timer = this.timers.get(key);
30
+ if (timer) {
31
+ clearTimeout(timer);
32
+ this.timers.delete(key);
33
+ }
34
+ }
35
+ keys(prefix) {
36
+ const now = Date.now();
37
+ const result = [];
38
+ for (const [key, entry] of this.data) {
39
+ if (entry.expiresAt !== null && now >= entry.expiresAt) {
40
+ this.del(key);
41
+ continue;
42
+ }
43
+ if (prefix === undefined || key.startsWith(prefix)) {
44
+ result.push(key);
45
+ }
46
+ }
47
+ return result;
48
+ }
49
+ clear() {
50
+ for (const timer of this.timers.values()) {
51
+ clearTimeout(timer);
52
+ }
53
+ this.timers.clear();
54
+ this.data.clear();
55
+ }
56
+ }
57
+ var createMemoryStore = () => new MemoryStore;
58
+
59
+ // src/browser/internal/id.ts
60
+ var simpleHash = (str) => {
61
+ let hash = 5381;
62
+ for (let i = 0;i < str.length; i++) {
63
+ hash = (hash << 5) + hash + str.charCodeAt(i) | 0;
64
+ }
65
+ return `hash:${(hash >>> 0).toString(16)}`;
66
+ };
67
+
68
+ // src/browser/ratelimit.ts
69
+ var DEFAULT_PREFIX = "sync:ratelimit";
70
+ var DEFAULT_WINDOW_SECS = 1;
71
+ var MAX_IDENTIFIER_LENGTH = 128;
72
+ var normalizeIdentifier = (identifier) => {
73
+ if (identifier.length <= MAX_IDENTIFIER_LENGTH)
74
+ return identifier;
75
+ return simpleHash(identifier);
76
+ };
77
+
78
+ class RateLimitError extends Error {
79
+ remaining;
80
+ resetIn;
81
+ constructor(result) {
82
+ super("Rate limit exceeded");
83
+ this.name = "RateLimitError";
84
+ this.remaining = result.remaining;
85
+ this.resetIn = result.resetIn;
86
+ }
87
+ }
88
+ var ratelimit = (config) => {
89
+ const prefix = config.prefix ?? DEFAULT_PREFIX;
90
+ const windowSecs = config.windowSecs ?? DEFAULT_WINDOW_SECS;
91
+ const { limit } = config;
92
+ const store = config.store ?? createMemoryStore();
93
+ const check = async (identifier) => {
94
+ const safeIdentifier = normalizeIdentifier(identifier);
95
+ const now = Date.now();
96
+ const windowMs = windowSecs * 1000;
97
+ const currentWindow = Math.floor(now / windowMs);
98
+ const previousWindow = currentWindow - 1;
99
+ const elapsedInWindow = now % windowMs;
100
+ const elapsedRatio = elapsedInWindow / windowMs;
101
+ const currentKey = `${prefix}:${config.id}:${safeIdentifier}:${currentWindow}`;
102
+ const previousKey = `${prefix}:${config.id}:${safeIdentifier}:${previousWindow}`;
103
+ const previousCount = store.get(previousKey) ?? 0;
104
+ const currentCount = (store.get(currentKey) ?? 0) + 1;
105
+ store.set(currentKey, currentCount, windowSecs * 2000);
106
+ const weightedCount = previousCount * (1 - elapsedRatio) + currentCount;
107
+ const limited = weightedCount > limit;
108
+ const remaining = Math.max(0, Math.floor(limit - weightedCount));
109
+ const resetIn = windowMs - elapsedInWindow;
110
+ return { limited, remaining, resetIn };
111
+ };
112
+ const checkOrThrow = async (identifier) => {
113
+ const result = await check(identifier);
114
+ if (result.limited) {
115
+ throw new RateLimitError(result);
116
+ }
117
+ return result;
118
+ };
119
+ return { id: config.id, check, checkOrThrow };
120
+ };
121
+ export {
122
+ ratelimit,
123
+ RateLimitError
124
+ };