with-bottleneck 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/domain.objects/Bottleneck.d.ts +60 -0
- package/dist/domain.objects/Bottleneck.js +3 -0
- package/dist/domain.objects/Bottleneck.js.map +1 -0
- package/dist/domain.operations/genBottleneck/asConcurrencyLimit.d.ts +7 -0
- package/dist/domain.operations/genBottleneck/asConcurrencyLimit.js +10 -0
- package/dist/domain.operations/genBottleneck/asConcurrencyLimit.js.map +1 -0
- package/dist/domain.operations/genBottleneck/assertLimitsValid.d.ts +8 -0
- package/dist/domain.operations/genBottleneck/assertLimitsValid.js +60 -0
- package/dist/domain.operations/genBottleneck/assertLimitsValid.js.map +1 -0
- package/dist/domain.operations/genBottleneck/scheduleWithLimits.d.ts +30 -0
- package/dist/domain.operations/genBottleneck/scheduleWithLimits.js +45 -0
- package/dist/domain.operations/genBottleneck/scheduleWithLimits.js.map +1 -0
- package/dist/domain.operations/genBottleneck.d.ts +6 -0
- package/dist/domain.operations/genBottleneck.js +29 -0
- package/dist/domain.operations/genBottleneck.js.map +1 -0
- package/dist/domain.operations/semaphore/genSemaphore.d.ts +34 -0
- package/dist/domain.operations/semaphore/genSemaphore.js +84 -0
- package/dist/domain.operations/semaphore/genSemaphore.js.map +1 -0
- package/dist/domain.operations/throttle/genThrottle.d.ts +48 -0
- package/dist/domain.operations/throttle/genThrottle.js +108 -0
- package/dist/domain.operations/throttle/genThrottle.js.map +1 -0
- package/dist/domain.operations/withBottleneck/getBottleneckFromSupplier.d.ts +9 -0
- package/dist/domain.operations/withBottleneck/getBottleneckFromSupplier.js +17 -0
- package/dist/domain.operations/withBottleneck/getBottleneckFromSupplier.js.map +1 -0
- package/dist/domain.operations/withBottleneck.d.ts +8 -0
- package/dist/domain.operations/withBottleneck.js +43 -0
- package/dist/domain.operations/withBottleneck.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +57 -50
- package/readme.md +22 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { IsoDuration } from 'iso-time';
|
|
2
|
+
/**
|
|
3
|
+
* .what = semaphore interface for concurrency control
|
|
4
|
+
* .why = limits simultaneous operations via slot-based acquire/release
|
|
5
|
+
*/
|
|
6
|
+
export interface BottleneckSemaphore {
|
|
7
|
+
/** acquire a slot, blocks until available */
|
|
8
|
+
acquire: () => Promise<void>;
|
|
9
|
+
/** release a slot */
|
|
10
|
+
release: () => void;
|
|
11
|
+
/** count of queued waiters */
|
|
12
|
+
readonly queued: number;
|
|
13
|
+
/** count of active slots */
|
|
14
|
+
readonly active: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* .what = throttle interface for velocity control
|
|
18
|
+
* .why = limits operations per time via token bucket
|
|
19
|
+
*/
|
|
20
|
+
export interface BottleneckThrottle {
|
|
21
|
+
/** acquire a token, blocks until available */
|
|
22
|
+
acquire: () => Promise<void>;
|
|
23
|
+
/** count of available tokens */
|
|
24
|
+
readonly tokens: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* .what = bottleneck instance with semaphore and schedule
|
|
28
|
+
* .why = provides unified concurrency and velocity control
|
|
29
|
+
*/
|
|
30
|
+
export interface Bottleneck {
|
|
31
|
+
/** the inner semaphore for concurrency control */
|
|
32
|
+
readonly semaphore: BottleneckSemaphore;
|
|
33
|
+
/**
|
|
34
|
+
* schedule fn for execution with bottleneck control
|
|
35
|
+
*
|
|
36
|
+
* acquires semaphore slot, then throttle token if configured,
|
|
37
|
+
* runs function, then releases semaphore slot (always, even on error)
|
|
38
|
+
*/
|
|
39
|
+
schedule: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* .what = limits configuration for genBottleneck
|
|
43
|
+
* .why = specifies concurrency and/or velocity constraints
|
|
44
|
+
*/
|
|
45
|
+
export interface BottleneckLimits {
|
|
46
|
+
/** max concurrent operations (how many at once) */
|
|
47
|
+
concurrency?: number;
|
|
48
|
+
/** rate limit (how many per time) */
|
|
49
|
+
velocity?: {
|
|
50
|
+
/** how many operations */
|
|
51
|
+
quantity: number;
|
|
52
|
+
/** per duration */
|
|
53
|
+
duration: IsoDuration;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* .what = supplier type for bottleneck resolution
|
|
58
|
+
* .why = enables static instance or per-call derivation from context
|
|
59
|
+
*/
|
|
60
|
+
export type BottleneckSupplier<TInput, TContext> = Bottleneck | ((input: TInput, context: TContext) => Bottleneck);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Bottleneck.js","sourceRoot":"","sources":["../../src/domain.objects/Bottleneck.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.asConcurrencyLimit = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* .what = derives concurrency limit from optional input
|
|
6
|
+
* .why = extracts default logic for unlimited concurrency
|
|
7
|
+
*/
|
|
8
|
+
const asConcurrencyLimit = (input) => input.concurrency ?? Infinity;
|
|
9
|
+
exports.asConcurrencyLimit = asConcurrencyLimit;
|
|
10
|
+
//# sourceMappingURL=asConcurrencyLimit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asConcurrencyLimit.js","sourceRoot":"","sources":["../../../src/domain.operations/genBottleneck/asConcurrencyLimit.ts"],"names":[],"mappings":";;;AAAA;;;GAGG;AACI,MAAM,kBAAkB,GAAG,CAAC,KAElC,EAAU,EAAE,CAAC,KAAK,CAAC,WAAW,IAAI,QAAQ,CAAC;AAF/B,QAAA,kBAAkB,sBAEa"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BottleneckLimits } from '../../domain.objects/Bottleneck';
|
|
2
|
+
/**
|
|
3
|
+
* .what = validates bottleneck limits configuration
|
|
4
|
+
* .why = fails fast on invalid limits with actionable error
|
|
5
|
+
*/
|
|
6
|
+
export declare const assertLimitsValid: (input: {
|
|
7
|
+
limits: BottleneckLimits | null;
|
|
8
|
+
}) => void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertLimitsValid = void 0;
|
|
4
|
+
const helpful_errors_1 = require("helpful-errors");
|
|
5
|
+
const iso_time_1 = require("iso-time");
|
|
6
|
+
/**
|
|
7
|
+
* .what = validates bottleneck limits configuration
|
|
8
|
+
* .why = fails fast on invalid limits with actionable error
|
|
9
|
+
*/
|
|
10
|
+
const assertLimitsValid = (input) => {
|
|
11
|
+
const { limits } = input;
|
|
12
|
+
// null limits are valid (defaults apply)
|
|
13
|
+
if (limits === null)
|
|
14
|
+
return;
|
|
15
|
+
// validate concurrency: must be positive integer or Infinity
|
|
16
|
+
if (limits.concurrency !== undefined && limits.concurrency <= 0)
|
|
17
|
+
throw new helpful_errors_1.BadRequestError('concurrency must be positive', {
|
|
18
|
+
field: 'concurrency',
|
|
19
|
+
value: limits.concurrency,
|
|
20
|
+
hint: 'use a positive integer for concurrency limit, or omit for unlimited',
|
|
21
|
+
});
|
|
22
|
+
if (limits.concurrency !== undefined &&
|
|
23
|
+
limits.concurrency !== Infinity &&
|
|
24
|
+
!Number.isInteger(limits.concurrency))
|
|
25
|
+
throw new helpful_errors_1.BadRequestError('concurrency must be an integer', {
|
|
26
|
+
field: 'concurrency',
|
|
27
|
+
value: limits.concurrency,
|
|
28
|
+
hint: 'use a positive integer for concurrency limit, or Infinity for unlimited',
|
|
29
|
+
});
|
|
30
|
+
// validate velocity.quantity: must be positive integer
|
|
31
|
+
if (limits.velocity !== undefined && limits.velocity.quantity <= 0)
|
|
32
|
+
throw new helpful_errors_1.BadRequestError('velocity.quantity must be positive', {
|
|
33
|
+
field: 'velocity.quantity',
|
|
34
|
+
value: limits.velocity.quantity,
|
|
35
|
+
hint: 'use a positive integer for velocity quantity',
|
|
36
|
+
});
|
|
37
|
+
if (limits.velocity !== undefined &&
|
|
38
|
+
!Number.isInteger(limits.velocity.quantity))
|
|
39
|
+
throw new helpful_errors_1.BadRequestError('velocity.quantity must be an integer', {
|
|
40
|
+
field: 'velocity.quantity',
|
|
41
|
+
value: limits.velocity.quantity,
|
|
42
|
+
hint: 'use a positive integer for velocity quantity',
|
|
43
|
+
});
|
|
44
|
+
// validate velocity.duration: must be present and positive
|
|
45
|
+
if (limits.velocity !== undefined && limits.velocity.duration === undefined)
|
|
46
|
+
throw new helpful_errors_1.BadRequestError('velocity.duration is required', {
|
|
47
|
+
field: 'velocity.duration',
|
|
48
|
+
value: limits.velocity.duration,
|
|
49
|
+
hint: 'specify duration as { seconds: 1 } or ISO 8601 string',
|
|
50
|
+
});
|
|
51
|
+
if (limits.velocity !== undefined &&
|
|
52
|
+
(0, iso_time_1.toMilliseconds)(limits.velocity.duration) <= 0)
|
|
53
|
+
throw new helpful_errors_1.BadRequestError('velocity.duration must be positive', {
|
|
54
|
+
field: 'velocity.duration',
|
|
55
|
+
value: limits.velocity.duration,
|
|
56
|
+
hint: 'specify duration as { seconds: 1 } or ISO 8601 string',
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
exports.assertLimitsValid = assertLimitsValid;
|
|
60
|
+
//# sourceMappingURL=assertLimitsValid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assertLimitsValid.js","sourceRoot":"","sources":["../../../src/domain.operations/genBottleneck/assertLimitsValid.ts"],"names":[],"mappings":";;;AAAA,mDAAiD;AACjD,uCAA0C;AAI1C;;;GAGG;AACI,MAAM,iBAAiB,GAAG,CAAC,KAEjC,EAAQ,EAAE;IACT,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAEzB,yCAAyC;IACzC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO;IAE5B,6DAA6D;IAC7D,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,MAAM,CAAC,WAAW,IAAI,CAAC;QAC7D,MAAM,IAAI,gCAAe,CAAC,8BAA8B,EAAE;YACxD,KAAK,EAAE,aAAa;YACpB,KAAK,EAAE,MAAM,CAAC,WAAW;YACzB,IAAI,EAAE,qEAAqE;SAC5E,CAAC,CAAC;IAEL,IACE,MAAM,CAAC,WAAW,KAAK,SAAS;QAChC,MAAM,CAAC,WAAW,KAAK,QAAQ;QAC/B,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC;QAErC,MAAM,IAAI,gCAAe,CAAC,gCAAgC,EAAE;YAC1D,KAAK,EAAE,aAAa;YACpB,KAAK,EAAE,MAAM,CAAC,WAAW;YACzB,IAAI,EAAE,yEAAyE;SAChF,CAAC,CAAC;IAEL,uDAAuD;IACvD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,IAAI,CAAC;QAChE,MAAM,IAAI,gCAAe,CAAC,oCAAoC,EAAE;YAC9D,KAAK,EAAE,mBAAmB;YAC1B,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ;YAC/B,IAAI,EAAE,8CAA8C;SACrD,CAAC,CAAC;IAEL,IACE,MAAM,CAAC,QAAQ,KAAK,SAAS;QAC7B,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAE3C,MAAM,IAAI,gCAAe,CAAC,sCAAsC,EAAE;YAChE,KAAK,EAAE,mBAAmB;YAC1B,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ;YAC/B,IAAI,EAAE,8CAA8C;SACrD,CAAC,CAAC;IAEL,2DAA2D;IAC3D,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS;QACzE,MAAM,IAAI,gCAAe,CAAC,+BAA+B,EAAE;YACzD,KAAK,EAAE,mBAAmB;YAC1B,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ;YAC/B,IAAI,EAAE,uDAAuD;SAC9D,CAAC,CAAC;IAEL,IACE,MAAM,CAAC,QAAQ,KAAK,SAAS;QAC7B,IAAA,yBAAc,EAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAE7C,MAAM,IAAI,gCAAe,CAAC,oCAAoC,EAAE;YAC9D,KAAK,EAAE,mBAAmB;YAC1B,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ;YAC/B,IAAI,EAAE,uDAAuD;SAC9D,CAAC,CAAC;AACP,CAAC,CAAC;AA9DW,QAAA,iBAAiB,qBA8D5B"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { BottleneckSemaphore, BottleneckThrottle } from '../../domain.objects/Bottleneck';
|
|
2
|
+
/**
|
|
3
|
+
* .what = schedules fn with semaphore and optional throttle control
|
|
4
|
+
* .why = encapsulates acquire/run/release pattern with guaranteed cleanup
|
|
5
|
+
*
|
|
6
|
+
* .note = order-dependence (intentional architecture)
|
|
7
|
+
*
|
|
8
|
+
* semaphore is acquired before throttle. when both are configured,
|
|
9
|
+
* a slot is held while blocked for velocity tokens. this is intentional:
|
|
10
|
+
* it prevents more than N operations from queued at the velocity limit.
|
|
11
|
+
*
|
|
12
|
+
* without this order, unlimited operations could pile up blocked for
|
|
13
|
+
* tokens, which leads to memory pressure. the order-dependence is a
|
|
14
|
+
* deliberate trade-off:
|
|
15
|
+
*
|
|
16
|
+
* | order | memory | behavior |
|
|
17
|
+
* |-----------------|------------------|-------------------------------|
|
|
18
|
+
* | semaphore first | bounded (N) | velocity wait holds slot |
|
|
19
|
+
* | throttle first | unbounded | unlimited queue at throttle |
|
|
20
|
+
*
|
|
21
|
+
* if you need independent limits, compose two separate bottlenecks.
|
|
22
|
+
*
|
|
23
|
+
* .mitigation = the order is documented here and in genBottleneck.
|
|
24
|
+
* tests verify the bounded memory behavior explicitly.
|
|
25
|
+
*/
|
|
26
|
+
export declare const scheduleWithLimits: <T>(input: {
|
|
27
|
+
fn: () => Promise<T>;
|
|
28
|
+
semaphore: BottleneckSemaphore;
|
|
29
|
+
throttle: BottleneckThrottle | null;
|
|
30
|
+
}) => Promise<T>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.scheduleWithLimits = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* .what = schedules fn with semaphore and optional throttle control
|
|
6
|
+
* .why = encapsulates acquire/run/release pattern with guaranteed cleanup
|
|
7
|
+
*
|
|
8
|
+
* .note = order-dependence (intentional architecture)
|
|
9
|
+
*
|
|
10
|
+
* semaphore is acquired before throttle. when both are configured,
|
|
11
|
+
* a slot is held while blocked for velocity tokens. this is intentional:
|
|
12
|
+
* it prevents more than N operations from queued at the velocity limit.
|
|
13
|
+
*
|
|
14
|
+
* without this order, unlimited operations could pile up blocked for
|
|
15
|
+
* tokens, which leads to memory pressure. the order-dependence is a
|
|
16
|
+
* deliberate trade-off:
|
|
17
|
+
*
|
|
18
|
+
* | order | memory | behavior |
|
|
19
|
+
* |-----------------|------------------|-------------------------------|
|
|
20
|
+
* | semaphore first | bounded (N) | velocity wait holds slot |
|
|
21
|
+
* | throttle first | unbounded | unlimited queue at throttle |
|
|
22
|
+
*
|
|
23
|
+
* if you need independent limits, compose two separate bottlenecks.
|
|
24
|
+
*
|
|
25
|
+
* .mitigation = the order is documented here and in genBottleneck.
|
|
26
|
+
* tests verify the bounded memory behavior explicitly.
|
|
27
|
+
*/
|
|
28
|
+
const scheduleWithLimits = async (input) => {
|
|
29
|
+
// acquire semaphore (blocks if at concurrency limit)
|
|
30
|
+
await input.semaphore.acquire();
|
|
31
|
+
try {
|
|
32
|
+
// acquire throttle token if configured (blocks if at velocity limit)
|
|
33
|
+
if (input.throttle) {
|
|
34
|
+
await input.throttle.acquire();
|
|
35
|
+
}
|
|
36
|
+
// run function
|
|
37
|
+
return await input.fn();
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
// always release semaphore slot
|
|
41
|
+
input.semaphore.release();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
exports.scheduleWithLimits = scheduleWithLimits;
|
|
45
|
+
//# sourceMappingURL=scheduleWithLimits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduleWithLimits.js","sourceRoot":"","sources":["../../../src/domain.operations/genBottleneck/scheduleWithLimits.ts"],"names":[],"mappings":";;;AAKA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACI,MAAM,kBAAkB,GAAG,KAAK,EAAK,KAI3C,EAAc,EAAE;IACf,qDAAqD;IACrD,MAAM,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;IAEhC,IAAI,CAAC;QACH,qEAAqE;QACrE,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;YACnB,MAAM,KAAK,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;QACjC,CAAC;QAED,eAAe;QACf,OAAO,MAAM,KAAK,CAAC,EAAE,EAAE,CAAC;IAC1B,CAAC;YAAS,CAAC;QACT,gCAAgC;QAChC,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;IAC5B,CAAC;AACH,CAAC,CAAC;AApBW,QAAA,kBAAkB,sBAoB7B"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Bottleneck, BottleneckLimits } from '../domain.objects/Bottleneck';
|
|
2
|
+
/**
|
|
3
|
+
* .what = creates a bottleneck instance with concurrency and/or velocity limits
|
|
4
|
+
* .why = provides unified rate limit and concurrency control
|
|
5
|
+
*/
|
|
6
|
+
export declare const genBottleneck: (input?: BottleneckLimits | null) => Bottleneck;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.genBottleneck = void 0;
|
|
4
|
+
const asConcurrencyLimit_1 = require("./genBottleneck/asConcurrencyLimit");
|
|
5
|
+
const assertLimitsValid_1 = require("./genBottleneck/assertLimitsValid");
|
|
6
|
+
const scheduleWithLimits_1 = require("./genBottleneck/scheduleWithLimits");
|
|
7
|
+
const genSemaphore_1 = require("./semaphore/genSemaphore");
|
|
8
|
+
const genThrottle_1 = require("./throttle/genThrottle");
|
|
9
|
+
/**
|
|
10
|
+
* .what = creates a bottleneck instance with concurrency and/or velocity limits
|
|
11
|
+
* .why = provides unified rate limit and concurrency control
|
|
12
|
+
*/
|
|
13
|
+
const genBottleneck = (input) => {
|
|
14
|
+
// validate limits
|
|
15
|
+
(0, assertLimitsValid_1.assertLimitsValid)({ limits: input ?? null });
|
|
16
|
+
// derive concurrency limit (Infinity if not configured)
|
|
17
|
+
const concurrency = (0, asConcurrencyLimit_1.asConcurrencyLimit)({ concurrency: input?.concurrency });
|
|
18
|
+
const semaphore = (0, genSemaphore_1.genSemaphore)({ concurrency });
|
|
19
|
+
// derive throttle (null if velocity not configured)
|
|
20
|
+
const throttle = (0, genThrottle_1.genThrottle)({ velocity: input?.velocity ?? null });
|
|
21
|
+
// compose schedule with limits
|
|
22
|
+
const schedule = async (fn) => (0, scheduleWithLimits_1.scheduleWithLimits)({ fn, semaphore, throttle });
|
|
23
|
+
return {
|
|
24
|
+
semaphore,
|
|
25
|
+
schedule,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
exports.genBottleneck = genBottleneck;
|
|
29
|
+
//# sourceMappingURL=genBottleneck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"genBottleneck.js","sourceRoot":"","sources":["../../src/domain.operations/genBottleneck.ts"],"names":[],"mappings":";;;AAKA,2EAAwE;AACxE,yEAAsE;AACtE,2EAAwE;AACxE,2DAAwD;AACxD,wDAAqD;AAErD;;;GAGG;AACI,MAAM,aAAa,GAAG,CAAC,KAA+B,EAAc,EAAE;IAC3E,kBAAkB;IAClB,IAAA,qCAAiB,EAAC,EAAE,MAAM,EAAE,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC;IAE7C,wDAAwD;IACxD,MAAM,WAAW,GAAG,IAAA,uCAAkB,EAAC,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAC;IAC5E,MAAM,SAAS,GAAG,IAAA,2BAAY,EAAC,EAAE,WAAW,EAAE,CAAC,CAAC;IAEhD,oDAAoD;IACpD,MAAM,QAAQ,GAAG,IAAA,yBAAW,EAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,IAAI,IAAI,EAAE,CAAC,CAAC;IAEpE,+BAA+B;IAC/B,MAAM,QAAQ,GAAG,KAAK,EAAK,EAAoB,EAAc,EAAE,CAC7D,IAAA,uCAAkB,EAAC,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;IAElD,OAAO;QACL,SAAS;QACT,QAAQ;KACT,CAAC;AACJ,CAAC,CAAC;AAnBW,QAAA,aAAa,iBAmBxB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BottleneckSemaphore } from '../../domain.objects/Bottleneck';
|
|
2
|
+
/**
|
|
3
|
+
* .what = creates a semaphore for concurrency control
|
|
4
|
+
* .why = limits simultaneous operations via slot-based acquire/release
|
|
5
|
+
*
|
|
6
|
+
* .exemption = rule.require.immutable-vars (scoped mutation zone)
|
|
7
|
+
*
|
|
8
|
+
* semaphores are fundamentally stateful — they track available resources
|
|
9
|
+
* and a queue of waiters. there is no immutable alternative:
|
|
10
|
+
*
|
|
11
|
+
* | alternative considered | why it fails |
|
|
12
|
+
* |------------------------|-------------------------------------|
|
|
13
|
+
* | immutable counter | cannot share state across acquire() |
|
|
14
|
+
* | functional reduce | no lock/wake mechanism |
|
|
15
|
+
* | external store | same mutation, different location |
|
|
16
|
+
*
|
|
17
|
+
* this matches the canonical semaphore (Dijkstra, 1965) and all major
|
|
18
|
+
* implementations (async-sema, p-queue, bottleneck npm).
|
|
19
|
+
*
|
|
20
|
+
* .safety = single-threaded JS event loop
|
|
21
|
+
*
|
|
22
|
+
* javascript guarantees synchronous blocks complete atomically.
|
|
23
|
+
* the read-modify-write (if < limit then count += 1) is safe because:
|
|
24
|
+
* - no preemption mid-block
|
|
25
|
+
* - async boundaries (await) are explicit
|
|
26
|
+
* - queue mutations are synchronous
|
|
27
|
+
*
|
|
28
|
+
* .boundaries = unsafe in worker threads or shared memory
|
|
29
|
+
*
|
|
30
|
+
* must not share via SharedArrayBuffer. for distributed, use redis/etcd.
|
|
31
|
+
*/
|
|
32
|
+
export declare const genSemaphore: (input: {
|
|
33
|
+
concurrency: number;
|
|
34
|
+
}) => BottleneckSemaphore;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.genSemaphore = void 0;
|
|
4
|
+
const helpful_errors_1 = require("helpful-errors");
|
|
5
|
+
/**
|
|
6
|
+
* .what = creates a semaphore for concurrency control
|
|
7
|
+
* .why = limits simultaneous operations via slot-based acquire/release
|
|
8
|
+
*
|
|
9
|
+
* .exemption = rule.require.immutable-vars (scoped mutation zone)
|
|
10
|
+
*
|
|
11
|
+
* semaphores are fundamentally stateful — they track available resources
|
|
12
|
+
* and a queue of waiters. there is no immutable alternative:
|
|
13
|
+
*
|
|
14
|
+
* | alternative considered | why it fails |
|
|
15
|
+
* |------------------------|-------------------------------------|
|
|
16
|
+
* | immutable counter | cannot share state across acquire() |
|
|
17
|
+
* | functional reduce | no lock/wake mechanism |
|
|
18
|
+
* | external store | same mutation, different location |
|
|
19
|
+
*
|
|
20
|
+
* this matches the canonical semaphore (Dijkstra, 1965) and all major
|
|
21
|
+
* implementations (async-sema, p-queue, bottleneck npm).
|
|
22
|
+
*
|
|
23
|
+
* .safety = single-threaded JS event loop
|
|
24
|
+
*
|
|
25
|
+
* javascript guarantees synchronous blocks complete atomically.
|
|
26
|
+
* the read-modify-write (if < limit then count += 1) is safe because:
|
|
27
|
+
* - no preemption mid-block
|
|
28
|
+
* - async boundaries (await) are explicit
|
|
29
|
+
* - queue mutations are synchronous
|
|
30
|
+
*
|
|
31
|
+
* .boundaries = unsafe in worker threads or shared memory
|
|
32
|
+
*
|
|
33
|
+
* must not share via SharedArrayBuffer. for distributed, use redis/etcd.
|
|
34
|
+
*/
|
|
35
|
+
const genSemaphore = (input) => {
|
|
36
|
+
// .mutation-zone = semaphore state (scoped, const reference with mutable properties)
|
|
37
|
+
const state = {
|
|
38
|
+
active: 0,
|
|
39
|
+
queue: [],
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* acquire a slot, blocks until available
|
|
43
|
+
*/
|
|
44
|
+
const acquire = () => {
|
|
45
|
+
// slot available: proceed immediately
|
|
46
|
+
if (state.active < input.concurrency) {
|
|
47
|
+
state.active += 1;
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
}
|
|
50
|
+
// at capacity: queue and wait
|
|
51
|
+
return new Promise((onSlotAvailable) => {
|
|
52
|
+
state.queue.push(onSlotAvailable);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* release a slot, allows next waiter to proceed
|
|
57
|
+
*/
|
|
58
|
+
const release = () => {
|
|
59
|
+
// guard: release without acquire is a bug
|
|
60
|
+
if (state.active === 0) {
|
|
61
|
+
throw new helpful_errors_1.UnexpectedCodePathError('release called without prior acquire', { active: 0, hint: 'ensure acquire() is awaited before release()' });
|
|
62
|
+
}
|
|
63
|
+
// release slot
|
|
64
|
+
state.active -= 1;
|
|
65
|
+
// wake next waiter if any
|
|
66
|
+
const next = state.queue.shift();
|
|
67
|
+
if (next) {
|
|
68
|
+
state.active += 1;
|
|
69
|
+
next();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
acquire,
|
|
74
|
+
release,
|
|
75
|
+
get queued() {
|
|
76
|
+
return state.queue.length;
|
|
77
|
+
},
|
|
78
|
+
get active() {
|
|
79
|
+
return state.active;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
exports.genSemaphore = genSemaphore;
|
|
84
|
+
//# sourceMappingURL=genSemaphore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"genSemaphore.js","sourceRoot":"","sources":["../../../src/domain.operations/semaphore/genSemaphore.ts"],"names":[],"mappings":";;;AAAA,mDAAyD;AAIzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACI,MAAM,YAAY,GAAG,CAAC,KAE5B,EAAuB,EAAE;IACxB,qFAAqF;IACrF,MAAM,KAAK,GAAG;QACZ,MAAM,EAAE,CAAC;QACT,KAAK,EAAE,EAAuB;KAC/B,CAAC;IAEF;;OAEG;IACH,MAAM,OAAO,GAAG,GAAkB,EAAE;QAClC,sCAAsC;QACtC,IAAI,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;YACrC,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;YAClB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;QAED,8BAA8B;QAC9B,OAAO,IAAI,OAAO,CAAC,CAAC,eAAe,EAAE,EAAE;YACrC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF;;OAEG;IACH,MAAM,OAAO,GAAG,GAAS,EAAE;QACzB,0CAA0C;QAC1C,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,wCAAuB,CAC/B,sCAAsC,EACtC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,8CAA8C,EAAE,CACpE,CAAC;QACJ,CAAC;QAED,eAAe;QACf,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;QAElB,0BAA0B;QAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACjC,IAAI,IAAI,EAAE,CAAC;YACT,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;YAClB,IAAI,EAAE,CAAC;QACT,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,OAAO;QACP,OAAO;QACP,IAAI,MAAM;YACR,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC;QAC5B,CAAC;QACD,IAAI,MAAM;YACR,OAAO,KAAK,CAAC,MAAM,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AA1DW,QAAA,YAAY,gBA0DvB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { IsoDuration } from 'iso-time';
|
|
2
|
+
import type { BottleneckThrottle } from '../../domain.objects/Bottleneck';
|
|
3
|
+
/**
|
|
4
|
+
* .what = creates a throttle for velocity control
|
|
5
|
+
* .why = limits operations per time via token bucket algorithm
|
|
6
|
+
*
|
|
7
|
+
* .exemption = rule.require.immutable-vars (scoped mutation zone)
|
|
8
|
+
*
|
|
9
|
+
* token bucket requires mutable state: token count, waiter queue, refill
|
|
10
|
+
* timer. there is no immutable alternative for rate limits:
|
|
11
|
+
*
|
|
12
|
+
* | alternative considered | why it fails |
|
|
13
|
+
* |------------------------|--------------------------------------|
|
|
14
|
+
* | immutable counter | cannot share token state over time |
|
|
15
|
+
* | functional timestamp | no queue/wake mechanism |
|
|
16
|
+
* | external store | same mutation, different location |
|
|
17
|
+
*
|
|
18
|
+
* this matches the canonical token bucket (Leaky Bucket, 1986) and
|
|
19
|
+
* all major implementations (bottleneck npm, limiter, p-ratelimit).
|
|
20
|
+
*
|
|
21
|
+
* .safety = single-threaded JS event loop
|
|
22
|
+
*
|
|
23
|
+
* javascript guarantees synchronous blocks complete atomically:
|
|
24
|
+
* - tokens -= 1 and queue.shift() execute in same sync block
|
|
25
|
+
* - setTimeout callback runs in fresh event loop turn
|
|
26
|
+
* - no preemption between read and write
|
|
27
|
+
*
|
|
28
|
+
* .timer = setTimeout precision (intentional trade-off)
|
|
29
|
+
*
|
|
30
|
+
* setTimeout is not precise — delays may exceed requested duration under
|
|
31
|
+
* CPU load. this means actual rate may be lower than configured (more
|
|
32
|
+
* time between refills). this is safe: we never exceed the configured
|
|
33
|
+
* rate, only potentially undershoot.
|
|
34
|
+
*
|
|
35
|
+
* | precision need | solution |
|
|
36
|
+
* |--------------------|------------------------------------|
|
|
37
|
+
* | approximate (here) | setTimeout, safe undershoot |
|
|
38
|
+
* | high precision | setImmediate + hrtime (not needed) |
|
|
39
|
+
* | distributed | redis-based (rate-limiter-flexible)|
|
|
40
|
+
*
|
|
41
|
+
* .boundaries = unsafe in worker threads or shared memory
|
|
42
|
+
*/
|
|
43
|
+
export declare const genThrottle: (input: {
|
|
44
|
+
velocity: {
|
|
45
|
+
quantity: number;
|
|
46
|
+
duration: IsoDuration;
|
|
47
|
+
} | null;
|
|
48
|
+
}) => BottleneckThrottle | null;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.genThrottle = void 0;
|
|
4
|
+
const iso_time_1 = require("iso-time");
|
|
5
|
+
/**
|
|
6
|
+
* .what = creates a throttle for velocity control
|
|
7
|
+
* .why = limits operations per time via token bucket algorithm
|
|
8
|
+
*
|
|
9
|
+
* .exemption = rule.require.immutable-vars (scoped mutation zone)
|
|
10
|
+
*
|
|
11
|
+
* token bucket requires mutable state: token count, waiter queue, refill
|
|
12
|
+
* timer. there is no immutable alternative for rate limits:
|
|
13
|
+
*
|
|
14
|
+
* | alternative considered | why it fails |
|
|
15
|
+
* |------------------------|--------------------------------------|
|
|
16
|
+
* | immutable counter | cannot share token state over time |
|
|
17
|
+
* | functional timestamp | no queue/wake mechanism |
|
|
18
|
+
* | external store | same mutation, different location |
|
|
19
|
+
*
|
|
20
|
+
* this matches the canonical token bucket (Leaky Bucket, 1986) and
|
|
21
|
+
* all major implementations (bottleneck npm, limiter, p-ratelimit).
|
|
22
|
+
*
|
|
23
|
+
* .safety = single-threaded JS event loop
|
|
24
|
+
*
|
|
25
|
+
* javascript guarantees synchronous blocks complete atomically:
|
|
26
|
+
* - tokens -= 1 and queue.shift() execute in same sync block
|
|
27
|
+
* - setTimeout callback runs in fresh event loop turn
|
|
28
|
+
* - no preemption between read and write
|
|
29
|
+
*
|
|
30
|
+
* .timer = setTimeout precision (intentional trade-off)
|
|
31
|
+
*
|
|
32
|
+
* setTimeout is not precise — delays may exceed requested duration under
|
|
33
|
+
* CPU load. this means actual rate may be lower than configured (more
|
|
34
|
+
* time between refills). this is safe: we never exceed the configured
|
|
35
|
+
* rate, only potentially undershoot.
|
|
36
|
+
*
|
|
37
|
+
* | precision need | solution |
|
|
38
|
+
* |--------------------|------------------------------------|
|
|
39
|
+
* | approximate (here) | setTimeout, safe undershoot |
|
|
40
|
+
* | high precision | setImmediate + hrtime (not needed) |
|
|
41
|
+
* | distributed | redis-based (rate-limiter-flexible)|
|
|
42
|
+
*
|
|
43
|
+
* .boundaries = unsafe in worker threads or shared memory
|
|
44
|
+
*/
|
|
45
|
+
const genThrottle = (input) => {
|
|
46
|
+
// guard: no velocity = no throttle
|
|
47
|
+
if (!input.velocity)
|
|
48
|
+
return null;
|
|
49
|
+
const { quantity, duration } = input.velocity;
|
|
50
|
+
const durationMs = (0, iso_time_1.toMilliseconds)(duration);
|
|
51
|
+
// .mutation-zone = token bucket state (scoped, const reference with mutable properties)
|
|
52
|
+
const state = {
|
|
53
|
+
tokens: quantity,
|
|
54
|
+
queue: [],
|
|
55
|
+
refillTimer: null,
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* schedule refill after duration passes
|
|
59
|
+
*/
|
|
60
|
+
const scheduleRefill = () => {
|
|
61
|
+
// guard: only one refill timer at a time
|
|
62
|
+
if (state.refillTimer !== null)
|
|
63
|
+
return;
|
|
64
|
+
state.refillTimer = setTimeout(() => {
|
|
65
|
+
// refill tokens
|
|
66
|
+
state.tokens = quantity;
|
|
67
|
+
state.refillTimer = null;
|
|
68
|
+
// wake queued waiters (up to available tokens)
|
|
69
|
+
while (state.queue.length > 0 && state.tokens > 0) {
|
|
70
|
+
const next = state.queue.shift();
|
|
71
|
+
if (next) {
|
|
72
|
+
state.tokens -= 1;
|
|
73
|
+
next();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// if still waiters, schedule another refill
|
|
77
|
+
if (state.queue.length > 0) {
|
|
78
|
+
scheduleRefill();
|
|
79
|
+
}
|
|
80
|
+
}, durationMs);
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* acquire a token, blocks until available
|
|
84
|
+
*/
|
|
85
|
+
const acquire = () => {
|
|
86
|
+
// token available: proceed immediately
|
|
87
|
+
if (state.tokens > 0) {
|
|
88
|
+
state.tokens -= 1;
|
|
89
|
+
// start refill timer if this is first token consumed
|
|
90
|
+
if (state.tokens < quantity && state.refillTimer === null) {
|
|
91
|
+
scheduleRefill();
|
|
92
|
+
}
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
}
|
|
95
|
+
// no tokens: queue and wait for refill
|
|
96
|
+
return new Promise((onTokenAvailable) => {
|
|
97
|
+
state.queue.push(onTokenAvailable);
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
return {
|
|
101
|
+
acquire,
|
|
102
|
+
get tokens() {
|
|
103
|
+
return state.tokens;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
exports.genThrottle = genThrottle;
|
|
108
|
+
//# sourceMappingURL=genThrottle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"genThrottle.js","sourceRoot":"","sources":["../../../src/domain.operations/throttle/genThrottle.ts"],"names":[],"mappings":";;;AACA,uCAA0C;AAI1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACI,MAAM,WAAW,GAAG,CAAC,KAK3B,EAA6B,EAAE;IAC9B,mCAAmC;IACnC,IAAI,CAAC,KAAK,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEjC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC;IAC9C,MAAM,UAAU,GAAG,IAAA,yBAAc,EAAC,QAAQ,CAAC,CAAC;IAE5C,wFAAwF;IACxF,MAAM,KAAK,GAAG;QACZ,MAAM,EAAE,QAAQ;QAChB,KAAK,EAAE,EAAuB;QAC9B,WAAW,EAAE,IAA4C;KAC1D,CAAC;IAEF;;OAEG;IACH,MAAM,cAAc,GAAG,GAAS,EAAE;QAChC,yCAAyC;QACzC,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI;YAAE,OAAO;QAEvC,KAAK,CAAC,WAAW,GAAG,UAAU,CAAC,GAAG,EAAE;YAClC,gBAAgB;YAChB,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC;YACxB,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC;YAEzB,+CAA+C;YAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAClD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;gBACjC,IAAI,IAAI,EAAE,CAAC;oBACT,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;oBAClB,IAAI,EAAE,CAAC;gBACT,CAAC;YACH,CAAC;YAED,4CAA4C;YAC5C,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,cAAc,EAAE,CAAC;YACnB,CAAC;QACH,CAAC,EAAE,UAAU,CAAC,CAAC;IACjB,CAAC,CAAC;IAEF;;OAEG;IACH,MAAM,OAAO,GAAG,GAAkB,EAAE;QAClC,uCAAuC;QACvC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;YAElB,qDAAqD;YACrD,IAAI,KAAK,CAAC,MAAM,GAAG,QAAQ,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,EAAE,CAAC;gBAC1D,cAAc,EAAE,CAAC;YACnB,CAAC;YAED,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;QAED,uCAAuC;QACvC,OAAO,IAAI,OAAO,CAAC,CAAC,gBAAgB,EAAE,EAAE;YACtC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACrC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,OAAO;QACL,OAAO;QACP,IAAI,MAAM;YACR,OAAO,KAAK,CAAC,MAAM,CAAC;QACtB,CAAC;KACF,CAAC;AACJ,CAAC,CAAC;AA3EW,QAAA,WAAW,eA2EtB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Bottleneck } from '../../domain.objects/Bottleneck';
|
|
2
|
+
/**
|
|
3
|
+
* .what = derives bottleneck from supplier or static instance
|
|
4
|
+
* .why = transforms polymorphic bottleneck option to concrete instance
|
|
5
|
+
*/
|
|
6
|
+
export declare const getBottleneckFromSupplier: <TArgs extends unknown[]>(input: {
|
|
7
|
+
bottleneck: Bottleneck | ((...args: TArgs) => Bottleneck);
|
|
8
|
+
args: TArgs;
|
|
9
|
+
}) => Bottleneck;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getBottleneckFromSupplier = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* .what = derives bottleneck from supplier or static instance
|
|
6
|
+
* .why = transforms polymorphic bottleneck option to concrete instance
|
|
7
|
+
*/
|
|
8
|
+
const getBottleneckFromSupplier = (input) => {
|
|
9
|
+
// static instance: return directly
|
|
10
|
+
if (typeof input.bottleneck !== 'function') {
|
|
11
|
+
return input.bottleneck;
|
|
12
|
+
}
|
|
13
|
+
// supplier function: invoke with args
|
|
14
|
+
return input.bottleneck(...input.args);
|
|
15
|
+
};
|
|
16
|
+
exports.getBottleneckFromSupplier = getBottleneckFromSupplier;
|
|
17
|
+
//# sourceMappingURL=getBottleneckFromSupplier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getBottleneckFromSupplier.js","sourceRoot":"","sources":["../../../src/domain.operations/withBottleneck/getBottleneckFromSupplier.ts"],"names":[],"mappings":";;;AAEA;;;GAGG;AACI,MAAM,yBAAyB,GAAG,CAA0B,KAGlE,EAAc,EAAE;IACf,mCAAmC;IACnC,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC3C,OAAO,KAAK,CAAC,UAAU,CAAC;IAC1B,CAAC;IAED,sCAAsC;IACtC,OAAO,KAAK,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC,CAAC;AAXW,QAAA,yBAAyB,6BAWpC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Bottleneck } from '../domain.objects/Bottleneck';
|
|
2
|
+
/**
|
|
3
|
+
* .what = wraps function with bottleneck control
|
|
4
|
+
* .why = enables declarative rate limit and concurrency control via HOF
|
|
5
|
+
*/
|
|
6
|
+
export declare const withBottleneck: <TLogic extends (...args: Parameters<TLogic>) => Promise<Awaited<ReturnType<TLogic>>>>(logic: TLogic, options: {
|
|
7
|
+
bottleneck: Bottleneck | ((...args: Parameters<TLogic>) => Bottleneck);
|
|
8
|
+
}) => TLogic;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withBottleneck = void 0;
|
|
4
|
+
const getBottleneckFromSupplier_1 = require("./withBottleneck/getBottleneckFromSupplier");
|
|
5
|
+
/**
|
|
6
|
+
* .what = wraps function with bottleneck control
|
|
7
|
+
* .why = enables declarative rate limit and concurrency control via HOF
|
|
8
|
+
*/
|
|
9
|
+
const withBottleneck = (logic, options) => {
|
|
10
|
+
const wrapped = async (...args) => {
|
|
11
|
+
// derive bottleneck from static instance or supplier function
|
|
12
|
+
const bottleneck = (0, getBottleneckFromSupplier_1.getBottleneckFromSupplier)({
|
|
13
|
+
bottleneck: options.bottleneck,
|
|
14
|
+
args,
|
|
15
|
+
});
|
|
16
|
+
// delegate to bottleneck.schedule
|
|
17
|
+
/**
|
|
18
|
+
* .cast = sdk boundary (external org code boundary)
|
|
19
|
+
* .why = TypeScript cannot infer that Promise<T> from schedule matches Awaited<ReturnType<TLogic>>
|
|
20
|
+
* because schedule is generic over T while TLogic is the outer constraint
|
|
21
|
+
* .safe = schedule returns Promise<T> where T is the return type of logic(...args)
|
|
22
|
+
* which is exactly Awaited<ReturnType<TLogic>> by construction
|
|
23
|
+
* .removal = requires higher-kinded types or const type parameters
|
|
24
|
+
* to flow TLogic's return type through schedule's generic
|
|
25
|
+
* blocked on microsoft/TypeScript#1213
|
|
26
|
+
*/
|
|
27
|
+
return bottleneck.schedule(() => logic(...args));
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* .cast = sdk boundary (external org code boundary)
|
|
31
|
+
* .why = preserves exact TLogic type for consumers
|
|
32
|
+
* TypeScript cannot infer that wrapped satisfies TLogic because:
|
|
33
|
+
* - TLogic may have additional properties (e.g., displayName)
|
|
34
|
+
* - the wrapped signature is derived via Parameters<>/ReturnType<>
|
|
35
|
+
* .safe = wrapped has identical call signature to TLogic
|
|
36
|
+
* .removal = requires TypeScript to support exact function type inference
|
|
37
|
+
* blocked on microsoft/TypeScript#34319 (exactOptionalPropertyTypes)
|
|
38
|
+
* until then, this cast is the standard HOF pattern
|
|
39
|
+
*/
|
|
40
|
+
return wrapped;
|
|
41
|
+
};
|
|
42
|
+
exports.withBottleneck = withBottleneck;
|
|
43
|
+
//# sourceMappingURL=withBottleneck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"withBottleneck.js","sourceRoot":"","sources":["../../src/domain.operations/withBottleneck.ts"],"names":[],"mappings":";;;AAEA,0FAAuF;AAEvF;;;GAGG;AACI,MAAM,cAAc,GAAG,CAK5B,KAAa,EACb,OAEC,EACO,EAAE;IACV,MAAM,OAAO,GAAG,KAAK,EACnB,GAAG,IAAwB,EACW,EAAE;QACxC,8DAA8D;QAC9D,MAAM,UAAU,GAAG,IAAA,qDAAyB,EAAC;YAC3C,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,IAAI;SACL,CAAC,CAAC;QAEH,kCAAkC;QAClC;;;;;;;;;WASG;QACH,OAAO,UAAU,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAE9C,CAAC;IACJ,CAAC,CAAC;IAEF;;;;;;;;;;OAUG;IACH,OAAO,OAAiB,CAAC;AAC3B,CAAC,CAAC;AA/CW,QAAA,cAAc,kBA+CzB"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.withBottleneck = exports.genBottleneck = void 0;
|
|
4
|
+
// domain operations
|
|
5
|
+
var genBottleneck_1 = require("./domain.operations/genBottleneck");
|
|
6
|
+
Object.defineProperty(exports, "genBottleneck", { enumerable: true, get: function () { return genBottleneck_1.genBottleneck; } });
|
|
7
|
+
var withBottleneck_1 = require("./domain.operations/withBottleneck");
|
|
8
|
+
Object.defineProperty(exports, "withBottleneck", { enumerable: true, get: function () { return withBottleneck_1.withBottleneck; } });
|
|
3
9
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAOA,oBAAoB;AACpB,mEAAkE;AAAzD,8GAAA,aAAa,OAAA;AACtB,qEAAoE;AAA3D,gHAAA,cAAc,OAAA"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "with-bottleneck",
|
|
3
3
|
"author": "ehmpathy",
|
|
4
4
|
"description": "rate limit and concurrency control utilities",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.1.1",
|
|
6
6
|
"repository": "ehmpathy/with-bottleneck",
|
|
7
7
|
"homepage": "https://github.com/ehmpathy/with-bottleneck",
|
|
8
8
|
"keywords": [
|
|
@@ -21,38 +21,79 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"/dist"
|
|
23
23
|
],
|
|
24
|
-
"
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build:ts": "tsc -p ./tsconfig.build.json",
|
|
26
|
+
"commit:with-cli": "npx cz",
|
|
27
|
+
"fix:format:biome": "biome check --write",
|
|
28
|
+
"fix:format": "npm run fix:format:biome",
|
|
29
|
+
"fix:lint": "biome check --write",
|
|
30
|
+
"fix": "npm run fix:format && npm run fix:lint",
|
|
31
|
+
"build:clean:bun": "rm -f ./bin/*.bc",
|
|
32
|
+
"build:clean:tsc": "(chmod -R u+w dist 2>/dev/null || true) && rm -rf dist/",
|
|
33
|
+
"build:clean": "npm run build:clean:tsc && npm run build:clean:bun",
|
|
34
|
+
"build:compile:tsc": "tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
|
|
35
|
+
"build:compile": "npm run build:compile:tsc && npm run build:compile:bun --if-present",
|
|
36
|
+
"build": "npm run build:clean && npm run build:compile && npm run build:complete --if-present",
|
|
37
|
+
"test:auth": "[ \"$ECHO\" = 'true' ] && echo '. .agent/repo=.this/role=any/skills/use.apikeys.sh' || . .agent/repo=.this/role=any/skills/use.apikeys.sh",
|
|
38
|
+
"test:commits": "LAST_TAG=$(git describe --tags --abbrev=0 @^ 2> /dev/null || git rev-list --max-parents=0 HEAD) && npx commitlint --from $LAST_TAG --to HEAD --verbose",
|
|
39
|
+
"test:types": "tsc -p ./tsconfig.json --noEmit",
|
|
40
|
+
"test:format:biome": "biome format",
|
|
41
|
+
"test:format": "npm run test:format:biome",
|
|
42
|
+
"test:lint:deps": "npx depcheck -c ./.depcheckrc.yml",
|
|
43
|
+
"test:lint:biome": "biome check --diagnostic-level=error",
|
|
44
|
+
"test:lint:biome:all": "biome check",
|
|
45
|
+
"test:lint": "npm run test:lint:biome && npm run test:lint:cycles && npm run test:lint:deps",
|
|
46
|
+
"test:unit": "set -eu && jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ \"${THOROUGH:-}\" != \"true\" ] && echo '--changedSince=main') $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
47
|
+
"test:integration": "set -eu && jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ \"${THOROUGH:-}\" != \"true\" ] && echo '--changedSince=main') $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
48
|
+
"test:acceptance:locally": "set -eu && npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
49
|
+
"test": "set -eu && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
|
|
50
|
+
"test:acceptance": "set -eu && npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n \"${CI:-}\" ] && echo '--ci') $([ \"${RESNAP:-}\" = \"true\" ] && echo '--updateSnapshot')",
|
|
51
|
+
"prepush": "npm run test && npm run build",
|
|
52
|
+
"prepublish": "npm run build",
|
|
53
|
+
"preversion": "npm run prepush",
|
|
54
|
+
"postversion": "git push origin HEAD --tags --no-verify",
|
|
55
|
+
"prepare:husky": "husky install && chmod ug+x .husky/*",
|
|
56
|
+
"prepare:rhachet": "rhachet init --hooks --roles mechanic behaver driver architect ergonomist reviewer dreamer dispatcher",
|
|
57
|
+
"prepare": "if [ -e .git ] && [ -z $CI ]; then npm run prepare:husky && npm run prepare:rhachet; fi",
|
|
58
|
+
"upgrade:rhachet": "rhachet upgrade",
|
|
59
|
+
"test:lint:cycles": "dpdm --no-warning --no-tree --exit-code circular:1 --exclude \"$(yq -r '.exclude | join(\"|\") // \"^$\"' .dpdmrc.yaml)\" 'src/**/*.ts'"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"domain-objects": "0.31.9",
|
|
63
|
+
"helpful-errors": "1.7.3",
|
|
64
|
+
"iso-time": "1.11.7"
|
|
65
|
+
},
|
|
25
66
|
"devDependencies": {
|
|
26
67
|
"@biomejs/biome": "2.3.8",
|
|
27
68
|
"@commitlint/cli": "19.5.0",
|
|
28
69
|
"@commitlint/config-conventional": "19.5.0",
|
|
70
|
+
"@jest/globals": "30.2.0",
|
|
29
71
|
"@swc/core": "1.15.3",
|
|
30
72
|
"@swc/jest": "0.2.39",
|
|
31
|
-
"@tsconfig/node-lts-strictest": "18.12.1",
|
|
32
73
|
"@tsconfig/node20": "20.1.5",
|
|
33
74
|
"@tsconfig/strictest": "2.0.5",
|
|
34
75
|
"@types/jest": "30.0.0",
|
|
35
76
|
"@types/node": "22.15.21",
|
|
36
77
|
"core-js": "3.26.1",
|
|
37
78
|
"cz-conventional-changelog": "3.3.0",
|
|
38
|
-
"declapract": "0.13.
|
|
39
|
-
"declapract-typescript-ehmpathy": "0.47.
|
|
40
|
-
"declastruct": "1.
|
|
41
|
-
"declastruct-github": "1.
|
|
79
|
+
"declapract": "0.13.21",
|
|
80
|
+
"declapract-typescript-ehmpathy": "0.47.74",
|
|
81
|
+
"declastruct": "1.9.1",
|
|
82
|
+
"declastruct-github": "1.4.0",
|
|
42
83
|
"depcheck": "1.4.3",
|
|
43
84
|
"domain-objects": "0.31.9",
|
|
85
|
+
"dpdm": "4.0.1",
|
|
44
86
|
"esbuild-register": "3.6.0",
|
|
45
|
-
"helpful-errors": "1.5.3",
|
|
46
87
|
"husky": "8.0.3",
|
|
47
88
|
"jest": "30.2.0",
|
|
48
|
-
"rhachet": "1.41.
|
|
89
|
+
"rhachet": "1.41.16",
|
|
49
90
|
"rhachet-brains-anthropic": "0.4.1",
|
|
50
91
|
"rhachet-brains-xai": "0.3.3",
|
|
51
|
-
"rhachet-roles-bhrain": "0.
|
|
52
|
-
"rhachet-roles-bhuild": "0.21.
|
|
53
|
-
"rhachet-roles-ehmpathy": "1.35.
|
|
54
|
-
"
|
|
55
|
-
"
|
|
92
|
+
"rhachet-roles-bhrain": "0.28.0",
|
|
93
|
+
"rhachet-roles-bhuild": "0.21.13",
|
|
94
|
+
"rhachet-roles-ehmpathy": "1.35.12",
|
|
95
|
+
"rhachet-roles-rhachet": "0.1.7",
|
|
96
|
+
"test-fns": "1.15.8",
|
|
56
97
|
"ts-node": "10.9.2",
|
|
57
98
|
"tsc-alias": "1.8.10",
|
|
58
99
|
"tsx": "4.20.6",
|
|
@@ -64,39 +105,5 @@
|
|
|
64
105
|
"path": "./node_modules/cz-conventional-changelog"
|
|
65
106
|
}
|
|
66
107
|
},
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
"commit:with-cli": "npx cz",
|
|
70
|
-
"fix:format:biome": "biome check --write",
|
|
71
|
-
"fix:format": "npm run fix:format:biome",
|
|
72
|
-
"fix:lint": "biome check --write",
|
|
73
|
-
"fix": "npm run fix:format && npm run fix:lint",
|
|
74
|
-
"build:clean:bun": "rm -f ./bin/*.bc",
|
|
75
|
-
"build:clean:tsc": "(chmod -R u+w dist 2>/dev/null || true) && rm -rf dist/",
|
|
76
|
-
"build:clean": "npm run build:clean:tsc && npm run build:clean:bun",
|
|
77
|
-
"build:compile:tsc": "tsc -p ./tsconfig.build.json && tsc-alias -p ./tsconfig.build.json",
|
|
78
|
-
"build:compile": "npm run build:compile:tsc && npm run build:compile:bun --if-present",
|
|
79
|
-
"build": "npm run build:clean && npm run build:compile && npm run build:complete --if-present",
|
|
80
|
-
"test:auth": "[ \"$ECHO\" = 'true' ] && echo '. .agent/repo=.this/role=any/skills/use.apikeys.sh' || . .agent/repo=.this/role=any/skills/use.apikeys.sh",
|
|
81
|
-
"test:commits": "LAST_TAG=$(git describe --tags --abbrev=0 @^ 2> /dev/null || git rev-list --max-parents=0 HEAD) && npx commitlint --from $LAST_TAG --to HEAD --verbose",
|
|
82
|
-
"test:types": "tsc -p ./tsconfig.json --noEmit",
|
|
83
|
-
"test:format:biome": "biome format",
|
|
84
|
-
"test:format": "npm run test:format:biome",
|
|
85
|
-
"test:lint:deps": "npx depcheck -c ./.depcheckrc.yml",
|
|
86
|
-
"test:lint:biome": "biome check --diagnostic-level=error",
|
|
87
|
-
"test:lint:biome:all": "biome check",
|
|
88
|
-
"test:lint": "npm run test:lint:biome && npm run test:lint:deps",
|
|
89
|
-
"test:unit": "jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
90
|
-
"test:integration": "jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
91
|
-
"test:acceptance:locally": "npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
92
|
-
"test": "eval $(ECHO=true npm run --silent test:auth) && npm run test:commits && npm run test:types && npm run test:format && npm run test:lint && npm run test:unit && npm run test:integration && npm run test:acceptance:locally",
|
|
93
|
-
"test:acceptance": "npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n $RESNAP ] && echo '--updateSnapshot')",
|
|
94
|
-
"prepush": "npm run test && npm run build",
|
|
95
|
-
"prepublish": "npm run build",
|
|
96
|
-
"preversion": "npm run prepush",
|
|
97
|
-
"postversion": "git push origin HEAD --tags --no-verify",
|
|
98
|
-
"prepare:husky": "husky install && chmod ug+x .husky/*",
|
|
99
|
-
"prepare:rhachet": "rhachet init --hooks --roles behaver mechanic reviewer",
|
|
100
|
-
"upgrade:rhachet": "rhachet upgrade"
|
|
101
|
-
}
|
|
102
|
-
}
|
|
108
|
+
"packageManager": "pnpm@10.24.0"
|
|
109
|
+
}
|
package/readme.md
CHANGED
|
@@ -176,3 +176,25 @@ same here: we *want* a bottleneck to control flow.
|
|
|
176
176
|
pour one out to the og, [`bottleneck`](https://www.npmjs.com/package/bottleneck), who paved the intuitive frame — abandoned since 2020.
|
|
177
177
|
|
|
178
178
|
we pick up where bottleneck left off; now, with wrappers and dependency injection.
|
|
179
|
+
|
|
180
|
+
# defaults
|
|
181
|
+
|
|
182
|
+
`genBottleneck()` with no config returns an unlimited bottleneck:
|
|
183
|
+
- `concurrency: Infinity` — no concurrency limit
|
|
184
|
+
- `velocity: null` — no rate limit
|
|
185
|
+
|
|
186
|
+
this is intentional:
|
|
187
|
+
- tests run instantly without delays
|
|
188
|
+
- inject unlimited bottleneck for test mocks
|
|
189
|
+
- only configure limits where you need them
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// no config = unlimited = fast tests
|
|
193
|
+
const testBottleneck = genBottleneck();
|
|
194
|
+
|
|
195
|
+
await Promise.all([
|
|
196
|
+
wrapped({ n: 1 }, { bottleneck: testBottleneck }),
|
|
197
|
+
wrapped({ n: 2 }, { bottleneck: testBottleneck }),
|
|
198
|
+
wrapped({ n: 3 }, { bottleneck: testBottleneck }),
|
|
199
|
+
]);
|
|
200
|
+
```
|