with-bottleneck 0.0.0 → 0.1.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.
Files changed (32) hide show
  1. package/dist/domain.objects/Bottleneck.d.ts +60 -0
  2. package/dist/domain.objects/Bottleneck.js +3 -0
  3. package/dist/domain.objects/Bottleneck.js.map +1 -0
  4. package/dist/domain.operations/genBottleneck/asConcurrencyLimit.d.ts +7 -0
  5. package/dist/domain.operations/genBottleneck/asConcurrencyLimit.js +10 -0
  6. package/dist/domain.operations/genBottleneck/asConcurrencyLimit.js.map +1 -0
  7. package/dist/domain.operations/genBottleneck/assertLimitsValid.d.ts +8 -0
  8. package/dist/domain.operations/genBottleneck/assertLimitsValid.js +60 -0
  9. package/dist/domain.operations/genBottleneck/assertLimitsValid.js.map +1 -0
  10. package/dist/domain.operations/genBottleneck/scheduleWithLimits.d.ts +30 -0
  11. package/dist/domain.operations/genBottleneck/scheduleWithLimits.js +45 -0
  12. package/dist/domain.operations/genBottleneck/scheduleWithLimits.js.map +1 -0
  13. package/dist/domain.operations/genBottleneck.d.ts +6 -0
  14. package/dist/domain.operations/genBottleneck.js +29 -0
  15. package/dist/domain.operations/genBottleneck.js.map +1 -0
  16. package/dist/domain.operations/semaphore/genSemaphore.d.ts +34 -0
  17. package/dist/domain.operations/semaphore/genSemaphore.js +84 -0
  18. package/dist/domain.operations/semaphore/genSemaphore.js.map +1 -0
  19. package/dist/domain.operations/throttle/genThrottle.d.ts +48 -0
  20. package/dist/domain.operations/throttle/genThrottle.js +108 -0
  21. package/dist/domain.operations/throttle/genThrottle.js.map +1 -0
  22. package/dist/domain.operations/withBottleneck/getBottleneckFromSupplier.d.ts +9 -0
  23. package/dist/domain.operations/withBottleneck/getBottleneckFromSupplier.js +17 -0
  24. package/dist/domain.operations/withBottleneck/getBottleneckFromSupplier.js.map +1 -0
  25. package/dist/domain.operations/withBottleneck.d.ts +8 -0
  26. package/dist/domain.operations/withBottleneck.js +43 -0
  27. package/dist/domain.operations/withBottleneck.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.js +7 -1
  30. package/dist/index.js.map +1 -1
  31. package/package.json +43 -39
  32. 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,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=Bottleneck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Bottleneck.js","sourceRoot":"","sources":["../../src/domain.objects/Bottleneck.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * .what = derives concurrency limit from optional input
3
+ * .why = extracts default logic for unlimited concurrency
4
+ */
5
+ export declare const asConcurrencyLimit: (input: {
6
+ concurrency: number | undefined;
7
+ }) => number;
@@ -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":";;;AAIA,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":";;;AACA,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
@@ -0,0 +1,3 @@
1
+ export type { Bottleneck, BottleneckLimits, BottleneckSemaphore, BottleneckSupplier, } from './domain.objects/Bottleneck';
2
+ export { genBottleneck } from './domain.operations/genBottleneck';
3
+ export { withBottleneck } from './domain.operations/withBottleneck';
package/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
1
  "use strict";
2
- // todo
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":";AAAA,OAAO"}
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.0.0",
5
+ "version": "0.1.0",
6
6
  "repository": "ehmpathy/with-bottleneck",
7
7
  "homepage": "https://github.com/ehmpathy/with-bottleneck",
8
8
  "keywords": [
@@ -21,7 +21,46 @@
21
21
  "files": [
22
22
  "/dist"
23
23
  ],
24
- "dependencies": {},
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:deps",
46
+ "test:unit": "jest -c ./jest.unit.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
47
+ "test:integration": "jest -c ./jest.integration.config.ts --forceExit --verbose --passWithNoTests $([ -z $THOROUGH ] && echo '--changedSince=main') $([ -n $RESNAP ] && echo '--updateSnapshot')",
48
+ "test:acceptance:locally": "npm run build && LOCALLY=true jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n $RESNAP ] && echo '--updateSnapshot')",
49
+ "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",
50
+ "test:acceptance": "npm run build && jest -c ./jest.acceptance.config.ts --forceExit --verbose --runInBand --passWithNoTests $([ -n $RESNAP ] && 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 behaver mechanic reviewer",
57
+ "prepare": "if [ -e .git ] && [ -z $CI ]; then npm run prepare:husky && npm run prepare:rhachet; fi",
58
+ "upgrade:rhachet": "rhachet upgrade"
59
+ },
60
+ "dependencies": {
61
+ "helpful-errors": "1.5.3",
62
+ "iso-time": "1.11.7"
63
+ },
25
64
  "devDependencies": {
26
65
  "@biomejs/biome": "2.3.8",
27
66
  "@commitlint/cli": "19.5.0",
@@ -42,7 +81,6 @@
42
81
  "depcheck": "1.4.3",
43
82
  "domain-objects": "0.31.9",
44
83
  "esbuild-register": "3.6.0",
45
- "helpful-errors": "1.5.3",
46
84
  "husky": "8.0.3",
47
85
  "jest": "30.2.0",
48
86
  "rhachet": "1.41.9",
@@ -64,39 +102,5 @@
64
102
  "path": "./node_modules/cz-conventional-changelog"
65
103
  }
66
104
  },
67
- "scripts": {
68
- "build:ts": "tsc -p ./tsconfig.build.json",
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
- }
105
+ "packageManager": "pnpm@10.24.0"
106
+ }
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
+ ```