@lenso/limits 0.0.0-stage → 0.2.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LioRael
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,353 @@
1
- # Temporary Holding Version
1
+ # @lenso/limits
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Request rate, periodic numeric quota, and weighted concurrency leases. These are
4
+ admission controls, not balances, money, billing, or an exactly-once system.
5
+
6
+ ## Ordinary service
7
+
8
+ ```ts
9
+ import { createLimits, createMemoryLimitStore } from "@lenso/limits";
10
+
11
+ const limits = createLimits({
12
+ store: createMemoryLimitStore(),
13
+ config: { failurePolicy: "throw" }, // required: "throw" | "deny" | "allow"
14
+ });
15
+
16
+ // Trusted application code derives tenant and subject after authentication.
17
+ const scope = { instance: "reports", tenant: tenantId, key: `run:${subjectId}` };
18
+ const rate = await limits.consumeRate({
19
+ scope,
20
+ capacity: 100,
21
+ quantity: 1,
22
+ periodMs: 60_000,
23
+ });
24
+ const quota = await limits.consumeQuota({
25
+ scope,
26
+ capacity: 1000,
27
+ quantity: 5,
28
+ periodMs: 86_400_000,
29
+ });
30
+ await limits.close(); // releases this service's leases, never closes its store
31
+ ```
32
+
33
+ Rate and quota occupy separate namespaces even with identical scopes. Both use
34
+ **fixed UTC epoch-aligned windows**:
35
+ `windowStart = floor(now / periodMs) * periodMs`. This is intentionally not a
36
+ sliding-window or token-bucket limiter: traffic can burst on both sides of a
37
+ window boundary. Periods are fixed elapsed durations, not calendar months,
38
+ customer billing cycles, or a caller-supplied reset timestamp.
39
+
40
+ `capacity` and `quantity` must be integers in `1..2147483647`; `periodMs` and
41
+ `ttlMs` must be integers in `1..31622400000` (366 days). Fractional, zero,
42
+ negative, nonfinite and out-of-range values fail. A valid quantity larger than
43
+ capacity is denied without consuming anything.
44
+
45
+ All admission results include `allowed`, `remaining`, `retryAfter`, and `reason`.
46
+ `retryAfter` is **milliseconds**, not an HTTP header:
47
+
48
+ - allowed: `0`;
49
+ - exhausted counter: time to the next window;
50
+ - exhausted lease capacity: earliest expiry that could fit this quantity,
51
+ assuming no renewals or earlier releases;
52
+ - quantity larger than capacity or unknown backend outcome: `null`.
53
+
54
+ For HTTP, the application can turn a finite value into Retry-After seconds with
55
+ `Math.ceil(retryAfter / 1000)`. It must not turn `null` into zero.
56
+
57
+ Policy is pinned per namespace/scope: changing counter capacity/period or lease
58
+ capacity returns `LimitError("policy-conflict")`. All workers must agree on
59
+ policies. Version a policy's application key deliberately when changing it;
60
+ doing so creates a new allowance, not a migration of already consumed quota.
61
+
62
+ ## Modes and shared storage
63
+
64
+ `createMemoryLimitStore()` belongs to one store object in one process. Separate
65
+ objects, CLI invocations and processes have separate limits. There is no hidden
66
+ singleton, background timer or cluster coordination.
67
+
68
+ The optional `@lenso/limits/sqlite` entry accepts **native Bun SQLite Drizzle**.
69
+ Multiple processes on the **same host**, opening the same local database file,
70
+ share limits. The root entry does not import Drizzle, `bun:sqlite`, Auth, Web,
71
+ Manage or Tasks. Install `drizzle-orm@0.45.3` for a SQL adapter, not memory mode.
72
+
73
+ Apply `migrations/0001_sqlite.sql` through your explicit migration workflow
74
+ before starting consumers. Exported `limitSchema` can join the application's
75
+ Drizzle schema. Setup never creates tables or changes PRAGMAs.
76
+ The SQL file is also exported at
77
+ `@lenso/limits/migrations/0001_sqlite.sql`.
78
+
79
+ ```ts
80
+ import { createBunSqlitePlugin } from "@lenso/db/bun-sqlite";
81
+ import { createLimitsPlugin } from "@lenso/limits";
82
+ import { createSqliteLimitStore, limitSchema } from "@lenso/limits/sqlite";
83
+
84
+ const db = createBunSqlitePlugin({
85
+ id: "limit-db",
86
+ filename: "./state/limits.sqlite",
87
+ schema: limitSchema,
88
+ });
89
+ const limits = createLimitsPlugin({
90
+ id: "limits",
91
+ requires: [db],
92
+ config: { failurePolicy: "deny" },
93
+ connect: (context) => createSqliteLimitStore(context.get(db)),
94
+ });
95
+ // Install these exact objects. A consumer declares requires: [limits] and
96
+ // obtains context.get(limits); creating another object with the same ID is not DI.
97
+ ```
98
+
99
+ The factory also accepts existing Config sources as `config: [source, ...]`;
100
+ `limitConfig` is its public Standard Schema/JSON Schema contract. The final
101
+ Config-bound plugin is the installed instance. Resource/plugin handles never
102
+ belong in serialized config. `scope.instance` names the logical application
103
+ policy namespace, **not** a per-process `context.instanceId`: giving each replica
104
+ a different scope intentionally isolates its allowance.
105
+
106
+ The application/DB owner should configure WAL and a suitable `busy_timeout`
107
+ on each connection through its existing DB setup. The adapter borrows the DB
108
+ and never closes it. SQLite lock contention remains a backend error after busy
109
+ handling, not an exhausted-limit result. The synchronous adapter blocks the
110
+ Bun event loop while waiting on locks; do not nest calls inside a deferred
111
+ application read transaction or long-running DB transaction.
112
+
113
+ Each mutation uses Drizzle's real `BEGIN IMMEDIATE` transaction: acquire the
114
+ SQLite writer lock, sample DB time, read/check/prune/update, then commit. The
115
+ callback is synchronous and never yields. Atomicity comes from that database
116
+ write transaction, not from JavaScript read/modify/write or an in-process lock.
117
+ The lease list is persisted as JSON per scope; this is intended for moderate
118
+ concurrency, not millions of concurrent leases. All writers must use the store
119
+ contract; direct table edits bypass its invariants.
120
+
121
+ Time is sampled once **after lock acquisition** from SQLite's UTC VFS clock.
122
+ Each bucket persists its highest observed time; backward jumps freeze progress
123
+ until the clock catches up rather than reopening consumed windows or reviving
124
+ expired leases. Forward jumps reset windows/expire leases sooner. Memory mode
125
+ uses `Date.now()` with the same per-bucket clamp; injected `now` is for tests.
126
+
127
+ SQLite remains local-file only: no cross-host/network-filesystem coordination.
128
+ PG and D1 use the separate adapters below, not casts of SQLite's database type.
129
+ No Redis dependency or universal database execution layer is introduced.
130
+
131
+ ### PostgreSQL shared mode
132
+
133
+ `@lenso/limits/postgres` exports `createPostgresLimitStore` and
134
+ `postgresLimitSchema`, accepting native Drizzle `PgDatabase` types. The public
135
+ Bun-first resource pairs with it directly:
136
+
137
+ ```ts
138
+ import { createBunSqlPlugin } from "@lenso/db/bun-sql";
139
+ import { createLimitsPlugin } from "@lenso/limits";
140
+ import { createPostgresLimitStore, postgresLimitSchema } from "@lenso/limits/postgres";
141
+
142
+ const db = createBunSqlPlugin({
143
+ id: "limit-pg",
144
+ connection: databaseUrl, // Trusted application configuration, not request JSON.
145
+ schema: postgresLimitSchema,
146
+ });
147
+ const limits = createLimitsPlugin({
148
+ id: "limits",
149
+ requires: [db],
150
+ config: { failurePolicy: "deny" },
151
+ connect: (context) => createPostgresLimitStore(context.get(db)),
152
+ });
153
+ ```
154
+
155
+ Apply `@lenso/limits/migrations/0001_postgres.sql` explicitly. Counters use
156
+ BIGINT and concurrency buckets use JSONB; all values remain within the common
157
+ JavaScript integer bounds. There is no startup migration or new pool owned by
158
+ the store.
159
+
160
+ Each operation runs in a database transaction. Bucket creation uses
161
+ `INSERT ... ON CONFLICT DO NOTHING`, then `SELECT ... FOR UPDATE` serializes
162
+ changes to the same namespace/scope. Only **after acquiring that row lock**, a
163
+ fresh statement samples `clock_timestamp()`; `now()`/transaction-start time
164
+ would wrongly include time spent waiting. Shared state rules then update the
165
+ locked row before commit, including expiry pruning and weighted admission.
166
+ Different scopes are not held behind a single application/global lock.
167
+
168
+ Processes or hosts connected to the **same authoritative PG database** with the
169
+ same namespace/policies share admission state. The application's DB owner
170
+ configures connection TLS, timeouts and availability; the store borrows the
171
+ existing resource and does not change them. Lock/statement timeouts and
172
+ serialization errors reach the explicit failure policy; the package does not
173
+ automatically retry unknown writes. Use the primary database, not a read
174
+ replica. JSON lease buckets target moderate concurrency.
175
+
176
+ Tests run real PostgreSQL on loopback with independent clients and owned
177
+ temporary clusters. They verify row-lock waits, post-lock expiry checks,
178
+ scope isolation, timeout faults and borrowed-resource cleanup. They do not
179
+ simulate a production failover or claim tested multi-host networking.
180
+
181
+ ### D1 and Workers shared mode
182
+
183
+ `@lenso/limits/d1` exports `createD1LimitStore` and `d1LimitSchema`, accepting
184
+ native `DrizzleD1Database`. Install the exact DB and limits instances inside the
185
+ application's existing `@lenso/workers` request assembly:
186
+
187
+ ```ts
188
+ import type { D1Database } from "@cloudflare/workers-types";
189
+ import { createD1Plugin } from "@lenso/db/d1";
190
+ import { createLimitsPlugin } from "@lenso/limits";
191
+ import { createD1LimitStore, d1LimitSchema } from "@lenso/limits/d1";
192
+
193
+ function limitsForRequest(bindings: { DB: D1Database }) {
194
+ const db = createD1Plugin({ id: "limit-d1", binding: bindings.DB, schema: d1LimitSchema });
195
+ const limits = createLimitsPlugin({
196
+ id: "limits",
197
+ requires: [db],
198
+ config: { failurePolicy: "deny" },
199
+ connect: (context) => createD1LimitStore(context.get(db)),
200
+ });
201
+ return { plugins: [db, limits], limits };
202
+ }
203
+ ```
204
+
205
+ Apply `@lenso/limits/migrations/0001_d1.sql` with the existing D1 migration
206
+ workflow before serving requests. Its counter/bucket/lease tables are
207
+ D1-specific and are **not interchangeable** with the local SQLite migration.
208
+ The platform binding stays borrowed. The runtime graph imports no Bun database,
209
+ filesystem or PG driver, and no listener is started by the adapter.
210
+
211
+ D1 has no interactive transaction callback here. Each method sends a fixed
212
+ `db.batch` transaction: advance the bucket's persisted database time,
213
+ reset/prune as needed, perform guarded SQL admission, and return its snapshot.
214
+ Counter consumption is a conditional `UPDATE`; lease admission is
215
+ `INSERT ... SELECT` conditioned on the live quantity sum. JavaScript never
216
+ reads an allowance and writes back a proposed replacement. A zero-row
217
+ mutation means denial or policy mismatch, **not** a batch error, so every
218
+ mutating statement carries the relevant scope/policy predicate.
219
+
220
+ These batches start with a write and keep result reads in that same primary
221
+ transaction. Do not replace them with replica observations, separate
222
+ `SELECT`/mutation calls, or `db.transaction`. The adapter never uses a session
223
+ bookmark as a lock. Initial window calculation explicitly casts the bound
224
+ period to INTEGER because D1 binds JS numbers as REAL; otherwise integer
225
+ window alignment is lost.
226
+
227
+ Database time is sampled by the batch's first bucket mutation and reused via
228
+ `last_now` for the rest of the operation. Persistent high-water semantics match
229
+ the other modes. Leases use separate indexed rows so SQL can prune and count
230
+ them atomically; explaining weighted retry times reads the holders within the
231
+ same batch. Intended for moderate concurrency; D1 query/response limits and
232
+ network faults remain backend errors.
233
+
234
+ Tests execute local **workerd's native D1 binding**, not a hand-written SQLite
235
+ double: concurrent batches, whole-batch rollback, expiry/old tokens, failure
236
+ policies and real `createWorkerHandler` + `createD1Plugin` request assembly.
237
+ Requests can share the same D1 database independently of per-request app
238
+ instances. Cloudflare-hosted D1 network, replication/failover and geographic
239
+ behavior are **not validated by local workerd tests**.
240
+
241
+ ## Leases and execution lifetime
242
+
243
+ ```ts
244
+ await limits.withLease(
245
+ { scope, capacity: 4, quantity: 1, ttlMs: 30_000 },
246
+ async ({ signal, lease }) => {
247
+ // Use the same application-owned Auth/Tasks service and honor signal.
248
+ await doWork({ signal });
249
+ },
250
+ { signal: requestSignal, renewEveryMs: 10_000 },
251
+ );
252
+ ```
253
+
254
+ Manual `acquire` returns `lease` with an unguessable UUID token, scope, weight and
255
+ epoch-millisecond `expiresAt`. Use `renew(lease, ttlMs)` and `release(lease)`.
256
+ Renewal never shortens a valid lease; expired/missing tokens return `null`.
257
+ Duplicate release is successful. Only the matching token is changed, so a stale
258
+ holder cannot renew or release a replacement. Tokens are capabilities, not
259
+ monotonic fencing numbers; do not publish them to untrusted clients or logs.
260
+ Raw `LimitStore.acquire(input, token)` is a trusted provider boundary, not the
261
+ application lease API: its caller must supply a fresh, never-reused token.
262
+ Applications use `createLimits(...).acquire` to get that behavior.
263
+
264
+ Manual leases have **no automatic renewal**. In `withLease`, renewal starts only
265
+ with an explicit `renewEveryMs` integer below TTL (and at most 2147483647 ms).
266
+ The wrapper owns its timer, prevents overlapping renewals, aborts on lost/failed
267
+ renewal, awaits in-flight renewal, and releases in `finally`. Body and release
268
+ errors both survive. Long stalls can still miss expiry.
269
+
270
+ Request cancellation signals the callback cooperatively and stops renewal;
271
+ `finally` releases after the callback settles. `close()` stops new admission,
272
+ aborts and drains owned wrappers and pending store calls, then releases manual
273
+ leases. A callback ignoring cancellation can delay close indefinitely: the
274
+ library does not pretend it has stopped execution. Do not await `limits.close()`
275
+ from inside its own running callback. The host must drain business work using
276
+ manual leases before closing their owning service/DB.
277
+
278
+ **TTL recovers valid lease weight, not necessarily running work.** An expired
279
+ task may still execute while a replacement runs. No strict execution bound,
280
+ fencing, revocation acknowledgement or exactly-once guarantee is claimed.
281
+ Stronger guarantees require the protected execution/effect endpoint to enforce
282
+ fencing or confirm cancellation itself.
283
+
284
+ ## Identity, faults and optional exposure
285
+
286
+ These services are trusted internal APIs, not public authorization endpoints.
287
+ Obtain an actor using the application's existing Auth audience, call its
288
+ `access.enforce` policy, derive tenant/subject from verified identity/membership,
289
+ and only then construct scope and consume. Do not copy `tenant`, `instance`,
290
+ `key`, `capacity` or `quantity` from arbitrary request JSON. An application may
291
+ derive a business key from an authorized resource, but a client-selected key must
292
+ not partition away that subject's limit.
293
+
294
+ `failurePolicy` has no default:
295
+
296
+ | Policy | Backend failure on consume/acquire |
297
+ | ------- | --------------------------------------------------------------------------------- |
298
+ | `throw` | Reject with `LimitError("backend-failure")`, retaining the cause in-process |
299
+ | `deny` | Denied, `reason: "backend-failure"`, unknown remaining/retry |
300
+ | `allow` | Allowed, visibly degraded, unknown remaining/retry; acquire returns `lease: null` |
301
+
302
+ Choose `allow` only when executing without a confirmed quota/lease is acceptable.
303
+ It also applies to `withLease`, whose callback then receives `lease: null`.
304
+ An unknown write outcome may already have consumed quota or created a lease:
305
+ there is **no automatic retry, refund or fabricated token**. Unknown leases
306
+ recover via TTL. Renewal and release failures always reject regardless of policy;
307
+ there is no meaningful fail-open renewal or confirmation of an unknown release.
308
+ Shutdown retries still-owned releases after a wrapper failure and aggregates
309
+ remaining cleanup failures.
310
+
311
+ The plugin reuses the existing contextual logger for a fixed backend-failure
312
+ warning without scope, token or backend error text. It does not bootstrap Log,
313
+ OTel, workers or a provider. Existing application telemetry can wrap calls.
314
+
315
+ No Web, CLI, MCP or Manage operation is exposed automatically, and no Manage
316
+ adapter is shipped in this first version. If needed, application-owned adapters
317
+ should declare only an explicit Operation/Manage subset over an already
318
+ authorized business service, with trusted identity binding and tenant/object
319
+ policy; never expose the raw scope-taking service as an unauthenticated tool.
320
+ Do not treat a Manage declaration or CLI input as an identity or permission.
321
+
322
+ ## Evidence and current limits
323
+
324
+ Focused tests cover memory boundaries, clock rollback, scope/namespace isolation,
325
+ quantity overflow, policy conflicts, stale/duplicate/expired tokens, wrapper
326
+ renewal/cancellation/shutdown, failure policies, real SQLite writer contention,
327
+ and existing public Config/Auth/DB lifecycle integration. A gated four-process
328
+ file-backed SQLite test proves shared admission rather than same-loop mocks.
329
+ Auth credentials in that test are local fixtures, not a production provider.
330
+ PostgreSQL tests use owned local clusters; D1/Workers tests use local Miniflare
331
+ and workerd with a disposable native D1 binding. No production connection,
332
+ Cloudflare credential or deployment is needed.
333
+
334
+ Bucket policy rows are retained, including empty buckets, to preserve policy
335
+ and clock history. Scope cardinality and DB maintenance are application-owned;
336
+ there is no built-in sweep/reset/admin permission. Deleting a bucket explicitly
337
+ resets its allowance/history and must not be exposed to untrusted callers.
338
+
339
+ Backend semantics checked against current official docs:
340
+ [Bun transactions](https://bun.sh/docs/api/sqlite#transactions),
341
+ [SQLite transactions](https://sqlite.org/lang_transaction.html),
342
+ [SQLite time](https://sqlite.org/lang_datefunc.html),
343
+ [PG row locks](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS),
344
+ [PG time](https://www.postgresql.org/docs/current/functions-datetime.html#FUNCTIONS-DATETIME-CURRENT),
345
+ [D1 batch and sessions](https://developers.cloudflare.com/d1/worker-api/d1-database/).
346
+
347
+ Development checks: build `@lenso/core`, `@lenso/db`, `@lenso/auth` and
348
+ `@lenso/workers` before this package, then run its `build`, `typecheck` and `test`
349
+ scripts. Set `LENSO_REQUIRE_POSTGRES=1` to require the local PG binaries rather
350
+ than skip PG tests. Miniflare/workerd are test-only dependencies; no provider
351
+ credentials are read. Install the registered workspace dependencies from the
352
+ single root Bun lockfile with `bun install --frozen-lockfile`; dependency changes
353
+ and lockfile updates remain the integration owner's responsibility.
@@ -0,0 +1,5 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import { type LimitConfig } from "./contracts";
3
+ export declare function validateConfig(value: unknown): LimitConfig;
4
+ export declare const limitConfigSchema: StandardSchemaV1<LimitConfig, LimitConfig>;
5
+ export declare const limitConfig: import("@lenso/core/config").ConfigContract<StandardSchemaV1<LimitConfig, LimitConfig>>;
@@ -0,0 +1,60 @@
1
+ /** Trusted application scope, never a client-selected identity or authorization grant. */
2
+ export interface LimitScope {
3
+ readonly instance: string;
4
+ readonly tenant: string;
5
+ readonly key: string;
6
+ }
7
+ export interface ConsumeInput {
8
+ readonly scope: LimitScope;
9
+ readonly capacity: number;
10
+ readonly quantity: number;
11
+ readonly periodMs: number;
12
+ }
13
+ export interface AcquireInput {
14
+ readonly scope: LimitScope;
15
+ readonly capacity: number;
16
+ readonly quantity: number;
17
+ readonly ttlMs: number;
18
+ }
19
+ export type CounterKind = "rate" | "quota";
20
+ export type FailurePolicy = "throw" | "deny" | "allow";
21
+ export interface LimitConfig {
22
+ readonly failurePolicy: FailurePolicy;
23
+ }
24
+ export interface Decision {
25
+ readonly allowed: boolean;
26
+ /** Null when the backend outcome is unknown. */
27
+ readonly remaining: number | null;
28
+ /** Milliseconds until this quantity can fit; null means no finite estimate. */
29
+ readonly retryAfter: number | null;
30
+ readonly reason: "allowed" | "exhausted" | "too-large" | "backend-failure";
31
+ }
32
+ export interface Lease {
33
+ readonly scope: LimitScope;
34
+ readonly token: string;
35
+ readonly quantity: number;
36
+ readonly expiresAt: number;
37
+ }
38
+ export interface Acquisition extends Decision {
39
+ /** Fail-open never fabricates a lease. */
40
+ readonly lease: Lease | null;
41
+ }
42
+ /** Each method must be atomic for its scope; this interface is not a transaction emulator. */
43
+ export interface LimitStore {
44
+ consume(kind: CounterKind, input: ConsumeInput): Promise<Decision>;
45
+ /** Trusted provider boundary: the service supplies a fresh UUID; callers must never reuse tokens. */
46
+ acquire(input: AcquireInput, token: string): Promise<Acquisition>;
47
+ renew(scope: LimitScope, token: string, ttlMs: number): Promise<Lease | null>;
48
+ release(scope: LimitScope, token: string): Promise<void>;
49
+ }
50
+ export declare class LimitError extends Error {
51
+ readonly code: "invalid-input" | "policy-conflict" | "backend-failure" | "closed" | "denied" | "lease-lost";
52
+ constructor(code: "invalid-input" | "policy-conflict" | "backend-failure" | "closed" | "denied" | "lease-lost", options?: ErrorOptions);
53
+ }
54
+ export declare const maxDurationMs = 31622400000;
55
+ export declare function positiveInteger(value: number, maximum?: number): void;
56
+ export declare function scopeKey(scope: LimitScope): string;
57
+ export declare function validateConsume(input: ConsumeInput): void;
58
+ export declare function validateAcquire(input: AcquireInput): void;
59
+ export declare function validateToken(token: string): void;
60
+ export declare function validateKind(kind: CounterKind): void;