@zukhruf/lease 0.0.0-stage → 0.3.17

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 ezzabuzaid
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,92 @@
1
- # Temporary Holding Version
1
+ # @zukhruf/lease
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
+ A lease gives one holder the right to act for a subject, for example a key of a mutex or the leadership of an election. The holder must know when it lost that right. This package gives the holder a signal that aborts when another holder may have the right. `@zukhruf/mutex`, `@zukhruf/single-flight`, and `@zukhruf/election` give out their leases with it.
4
+
5
+ The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
6
+
7
+ ## A lease is a signal
8
+
9
+ A holder gets a `Lease`. A lease has one property: `signal`, an `AbortSignal`.
10
+
11
+ - The signal aborts one time, with a `LeaseLostError`, when another holder may have the right.
12
+ - The signal never aborts when the holder releases the lease.
13
+
14
+ The holder stops its work when the signal aborts. Give the signal to each call that takes a signal, and check it before each write:
15
+
16
+ ```ts
17
+ import type { Lease } from '@zukhruf/lease';
18
+
19
+ async function copyRows(lease: Lease, batches: Row[][]) {
20
+ for (const batch of batches) {
21
+ lease.signal.throwIfAborted();
22
+ await fetch('https://warehouse.example/rows', {
23
+ method: 'POST',
24
+ body: JSON.stringify(batch),
25
+ signal: lease.signal,
26
+ });
27
+ }
28
+ }
29
+ ```
30
+
31
+ ## Tell a lost lease from other errors
32
+
33
+ The reason of the signal is a `LeaseLostError`. Its `subject` names what the lease gave the right to. Its `cause`, when there is one, is what the issuer saw: for example, the error of a coordinator that refused the claim.
34
+
35
+ ```ts
36
+ import { LeaseLostError } from '@zukhruf/lease';
37
+
38
+ try {
39
+ await copyRows(lease, batches);
40
+ } catch (error) {
41
+ if (error instanceof LeaseLostError) {
42
+ console.log(`Another holder may have ${error.subject} now.`, error.cause);
43
+ }
44
+ throw error;
45
+ }
46
+ ```
47
+
48
+ A catch with `instanceof LeaseLostError` works for the leases of each package that uses this package, because they all give the same class.
49
+
50
+ ## Give out a lease
51
+
52
+ An issuer makes one `LeaseController` for each lease. It keeps the controller, and it gives the holder only the signal. `AbortController` and `AbortSignal` use the same split: only the code that has the controller can abort the signal.
53
+
54
+ ```ts
55
+ import { type Lease, LeaseController } from '@zukhruf/lease';
56
+
57
+ const controller = new LeaseController('orders');
58
+ const lease: Lease = { signal: controller.signal };
59
+
60
+ // The coordinator refused the claim, so another holder may have it:
61
+ controller.lose(new Error('The coordinator refused the claim.'));
62
+
63
+ // The holder released the lease:
64
+ controller.end();
65
+ ```
66
+
67
+ - `lose(cause?)` aborts the signal with `new LeaseLostError(subject, { cause })`. When you give no cause, the error has no `cause` property.
68
+ - A lease is lost one time. A second `lose` keeps the reason of the first.
69
+ - `end()` never aborts the signal. After `end()`, `lose` does nothing: the holder gave the right up, so it cannot lose it. A loss that the issuer sees late, after the release, does not reach the holder.
70
+ - `end()` can be called again. It keeps the reason of an earlier loss.
71
+
72
+ ## A lease lasts for a session, not for a time
73
+
74
+ In many systems, a lease ends after a time unless the holder renews it. A lease of zukhruf has no timer. It ends when the holder releases it, or when the holder's process stops: the operating system then frees the claim of that process. It is lost when the issuer decides that another holder may have the right, for example when a coordinator refuses the claim after a failover. Thus this package has no timer and no renew.
75
+
76
+ ## A lease warns the holder. It does not protect a resource
77
+
78
+ A holder can stop for some time, for example in a long garbage collection, and then write. In that time, the issuer can see the loss and give the right to another holder. The holder writes before it checks its signal. Thus the signal is a warning, not a protection.
79
+
80
+ To protect a resource, use a fencing token. The resource keeps the highest token that it accepted, and it refuses a write with a lower token. Fencing tokens are in [`@zukhruf/fencing`](../fencing). A lease and a fencing token are two concepts: some systems use a lease with no token, and others use a token with no lease. See [ADR 0001](./docs/adr/0001-a-lease-is-its-own-package-apart-from-fencing.md).
81
+
82
+ ## The same concepts in other systems
83
+
84
+ | This package | Other systems |
85
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
86
+ | Lease | Lease: Gray and Cheriton, "Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency" (1989); Kubernetes `coordination.k8s.io/v1` `Lease`; etcd `concurrency.Session` |
87
+ | `lease.signal` aborts | etcd `Session.Done()` closes its channel; Go `ctx.Done()` closes |
88
+ | `LeaseLostError` | Hazelcast `LockOwnershipLostException`; Go `context.Cause(ctx)` |
89
+ | `LeaseController` and `Lease` | Web `AbortController` and `AbortSignal`; Go `context.WithCancelCause`, which returns the context and its cancel function |
90
+ | `lose(cause)` keeps the first reason | Go `cancel(cause)`: a second call does nothing; `AbortController.abort(reason)`: a second call does nothing |
91
+
92
+ The leases of Gray and Cheriton, Kubernetes, and etcd last for a time, and the holder renews them. The leases of zukhruf last for a session.
@@ -0,0 +1,4 @@
1
+ export type { Lease } from './lease.ts';
2
+ export { LeaseController } from './lease-controller.ts';
3
+ export { LeaseLostError } from './lease-lost-error.ts';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { LeaseController } from "./lease-controller.js";
2
+ export { LeaseLostError } from "./lease-lost-error.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The issuer's side of one lease. The issuer keeps the controller and gives
3
+ * the holder only `signal`, as an `AbortController` keeps its abort and
4
+ * gives out its `AbortSignal`.
5
+ */
6
+ export declare class LeaseController {
7
+ #private;
8
+ constructor(subject: string);
9
+ get signal(): AbortSignal;
10
+ /**
11
+ * Another holder may have the right now. Aborts `signal` with a
12
+ * `LeaseLostError` that carries `cause`, when one is given. A lease is lost
13
+ * at most once, and never after it ended: a released right is not this
14
+ * holder's to lose.
15
+ */
16
+ lose(cause?: unknown): void;
17
+ /** The holder released the lease. `signal` keeps its state, and a later `lose` does nothing. */
18
+ end(): void;
19
+ }
20
+ //# sourceMappingURL=lease-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease-controller.d.ts","sourceRoot":"","sources":["../src/lease-controller.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,qBAAa,eAAe;;gBAMd,OAAO,EAAE,MAAM;IAI3B,IAAI,MAAM,IAAI,WAAW,CAExB;IAED;;;;;OAKG;IACH,IAAI,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IAW3B,gGAAgG;IAChG,GAAG,IAAI,IAAI;CAGZ"}
@@ -0,0 +1,35 @@
1
+ import { LeaseLostError } from "./lease-lost-error.js";
2
+ /**
3
+ * The issuer's side of one lease. The issuer keeps the controller and gives
4
+ * the holder only `signal`, as an `AbortController` keeps its abort and
5
+ * gives out its `AbortSignal`.
6
+ */
7
+ export class LeaseController {
8
+ #subject;
9
+ #lost = new AbortController();
10
+ /** Aborts once the lease ends, by a loss or by `end()`. */
11
+ #ended = new AbortController();
12
+ constructor(subject) {
13
+ this.#subject = subject;
14
+ }
15
+ get signal() {
16
+ return this.#lost.signal;
17
+ }
18
+ /**
19
+ * Another holder may have the right now. Aborts `signal` with a
20
+ * `LeaseLostError` that carries `cause`, when one is given. A lease is lost
21
+ * at most once, and never after it ended: a released right is not this
22
+ * holder's to lose.
23
+ */
24
+ lose(cause) {
25
+ if (this.#ended.signal.aborted)
26
+ return;
27
+ this.#ended.abort();
28
+ this.#lost.abort(new LeaseLostError(this.#subject, cause === undefined ? undefined : { cause }));
29
+ }
30
+ /** The holder released the lease. `signal` keeps its state, and a later `lose` does nothing. */
31
+ end() {
32
+ this.#ended.abort();
33
+ }
34
+ }
35
+ //# sourceMappingURL=lease-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease-controller.js","sourceRoot":"","sources":["../src/lease-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD;;;;GAIG;AACH,MAAM,OAAO,eAAe;IACjB,QAAQ,CAAS;IACjB,KAAK,GAAG,IAAI,eAAe,EAAE,CAAC;IACvC,2DAA2D;IAClD,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;IAExC,YAAY,OAAe;QACzB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC1B,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC;IAC3B,CAAC;IAED;;;;;OAKG;IACH,IAAI,CAAC,KAAe;QAClB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO;QACvC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACpB,IAAI,CAAC,KAAK,CAAC,KAAK,CACd,IAAI,cAAc,CAChB,IAAI,CAAC,QAAQ,EACb,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAC5C,CACF,CAAC;IACJ,CAAC;IAED,gGAAgG;IAChG,GAAG;QACD,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ /** The holder may have lost the right to its subject: another holder may have it now. */
2
+ export declare class LeaseLostError extends Error {
3
+ readonly subject: string;
4
+ constructor(subject: string, options?: ErrorOptions);
5
+ }
6
+ //# sourceMappingURL=lease-lost-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease-lost-error.d.ts","sourceRoot":"","sources":["../src/lease-lost-error.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAQpD"}
@@ -0,0 +1,10 @@
1
+ /** The holder may have lost the right to its subject: another holder may have it now. */
2
+ export class LeaseLostError extends Error {
3
+ subject;
4
+ constructor(subject, options) {
5
+ super(`Lost the lease on ${JSON.stringify(subject)}: another holder may have it now.`, options);
6
+ this.name = 'LeaseLostError';
7
+ this.subject = subject;
8
+ }
9
+ }
10
+ //# sourceMappingURL=lease-lost-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease-lost-error.js","sourceRoot":"","sources":["../src/lease-lost-error.ts"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,OAAO,CAAS;IAEzB,YAAY,OAAe,EAAE,OAAsB;QACjD,KAAK,CACH,qBAAqB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,mCAAmC,EAC/E,OAAO,CACR,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ /** The holder's view of a lease: act on the subject until `signal` aborts. */
2
+ export interface Lease {
3
+ /**
4
+ * Aborts once, with a `LeaseLostError`, when another holder may have the
5
+ * right. It never aborts when the holder releases the lease.
6
+ */
7
+ readonly signal: AbortSignal;
8
+ }
9
+ //# sourceMappingURL=lease.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.d.ts","sourceRoot":"","sources":["../src/lease.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,MAAM,WAAW,KAAK;IACpB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B"}
package/dist/lease.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=lease.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.js","sourceRoot":"","sources":["../src/lease.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "@zukhruf/lease",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.17",
4
+ "description": "A lease and its loss: the holder gets an AbortSignal that aborts once, with LeaseLostError, when another holder may have the right. The issuer keeps a LeaseController.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/JanuaryLabs/zukhruf",
9
+ "directory": "packages/lease"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/JanuaryLabs/zukhruf/issues"
13
+ },
14
+ "homepage": "https://github.com/JanuaryLabs/zukhruf/tree/main/packages/lease#readme",
15
+ "keywords": [
16
+ "lease",
17
+ "lock",
18
+ "leader-election",
19
+ "abortsignal",
20
+ "abortcontroller",
21
+ "distributed"
22
+ ],
23
+ "type": "module",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ "./package.json": "./package.json",
27
+ ".": {
28
+ "@zukhruf/source": "./src/index.ts",
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "default": "./dist/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "!dist/spec/**",
37
+ "!**/*.tsbuildinfo",
38
+ "!**/*.test.*"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }