@alpamayo-solutions/colca-client 0.0.0 → 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/README.md +143 -3
- package/dist/door.d.ts +155 -0
- package/dist/door.d.ts.map +1 -0
- package/dist/door.js +212 -0
- package/dist/door.js.map +1 -0
- package/dist/generated/contracts.d.ts +627 -0
- package/dist/generated/contracts.d.ts.map +1 -0
- package/dist/generated/contracts.js +39 -0
- package/dist/generated/contracts.js.map +1 -0
- package/dist/ids.d.ts +36 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +64 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/live.d.ts +148 -0
- package/dist/live.d.ts.map +1 -0
- package/dist/live.js +500 -0
- package/dist/live.js.map +1 -0
- package/dist/stream.d.ts +54 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +85 -0
- package/dist/stream.js.map +1 -0
- package/dist/topics.d.ts +45 -0
- package/dist/topics.d.ts.map +1 -0
- package/dist/topics.js +70 -0
- package/dist/topics.js.map +1 -0
- package/package.json +66 -3
- package/LICENSE.md +0 -105
package/README.md
CHANGED
|
@@ -1,5 +1,145 @@
|
|
|
1
1
|
# @alpamayo-solutions/colca-client
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A Colca node's door, in TypeScript: records over HTTP — `fetch`, `ack`,
|
|
4
|
+
`publish`, `kv`, `self` — and live values over MQTT. No runtime dependencies
|
|
5
|
+
for the door, no framework, no opinion about how your service is built.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @alpamayo-solutions/colca-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## A consumer
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Door, Stream } from "@alpamayo-solutions/colca-client";
|
|
15
|
+
|
|
16
|
+
const door = new Door({ baseUrl: "http://colca", service: "my-app" });
|
|
17
|
+
const panels = new Stream(door, "annotations", door.cursorName("panels"), {
|
|
18
|
+
prefix: "wisewoods/line1",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
for await (const record of panels.follow()) {
|
|
22
|
+
// Acked page by page: a handler that throws sees its page again.
|
|
23
|
+
console.log(record.topic, record.payload);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## A producer
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
await door.publishTo(
|
|
31
|
+
{ root: "steine", contract: "_Metric", node: "n-technikum", path: "wisewoods/line1/mas2/grit" },
|
|
32
|
+
{ signal_id: "01M2AB…", timestamp: Date.now() / 1000, value: 60 },
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Live values
|
|
37
|
+
|
|
38
|
+
`@alpamayo-solutions/colca-client/live` keeps one MQTT connection to the node's
|
|
39
|
+
WebSocket door for people and applications, and hands out values as they change.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm install mqtt
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Live } from "@alpamayo-solutions/colca-client/live";
|
|
47
|
+
|
|
48
|
+
const live = new Live({
|
|
49
|
+
url: "wss://node:8885",
|
|
50
|
+
// Asked before every connection, so hand back a token that is valid now.
|
|
51
|
+
token: () => auth.freshToken(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const stop = live.subscribe("steine/v1/_Metric/n-technikum/wisewoods/#", (value) => {
|
|
55
|
+
show(value.topic, value.payload);
|
|
56
|
+
});
|
|
57
|
+
live.onState((state) => showOffline(state !== "online"));
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Data and entity paths are retained at the node, so a subscription starts with
|
|
61
|
+
the current values and continues with the changes; nothing has to be fetched
|
|
62
|
+
first. Around that the client does what a page left open all day needs:
|
|
63
|
+
|
|
64
|
+
- **One connection for the whole page.** `subscribe` returns the function that
|
|
65
|
+
ends that subscription and no other; the node's subscription goes when the
|
|
66
|
+
last listener on a filter does.
|
|
67
|
+
- **A second subscriber gets the value at once.** The client keeps the last value
|
|
68
|
+
of every topic, and `latest()` and `values()` read it.
|
|
69
|
+
- **A fresh token before the old one runs out.** The node ends a session when its
|
|
70
|
+
token expires. The client reconnects shortly before, with a new token and every
|
|
71
|
+
subscription sent again, and stays `online` while doing so.
|
|
72
|
+
- **Waits that grow after a drop**, jittered, each attempt with a fresh token.
|
|
73
|
+
|
|
74
|
+
These are values, not a log. A change during a reconnect is superseded by the
|
|
75
|
+
retained value that follows it. Whatever must see every record reads a stream
|
|
76
|
+
through the door.
|
|
77
|
+
|
|
78
|
+
### Commands
|
|
79
|
+
|
|
80
|
+
A person's session sends commands, not values. `command()` sends one and waits
|
|
81
|
+
for the executor's `_Ack`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const ack = await live.command("steine/v1/_CmdParam/n-technikum/wisewoods/line1/mas2/sta1/aggos/setGrit", {
|
|
85
|
+
params: { signal: "grit", value: 120 },
|
|
86
|
+
});
|
|
87
|
+
if (ack.result_code !== 200) showRefusal(ack.message);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
It adds the correlation id and the expiry, subscribes to the acknowledgements
|
|
91
|
+
before it sends, so an executor that answers at once is not missed, and matches
|
|
92
|
+
the answer by its id wherever in the tree it arrives. The promise settles with
|
|
93
|
+
the `_Ack` whatever its result code, and rejects with `CommandTimeout` when
|
|
94
|
+
nobody answers in time (30 s by default, which is also the command's expiry).
|
|
95
|
+
Offline, nothing is queued: a setpoint sent minutes late is a different
|
|
96
|
+
setpoint.
|
|
97
|
+
|
|
98
|
+
`publish()` sends a single record without waiting, and `newUlid()` makes ids
|
|
99
|
+
that sort by the time they were made, as the node's own do.
|
|
100
|
+
|
|
101
|
+
## Which door, which credential
|
|
102
|
+
|
|
103
|
+
| Door | URL | Credential |
|
|
104
|
+
| --------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
105
|
+
| local | `http://colca` (port 80, inside the deployment network) | none — reachability is the credential; `service` names the caller |
|
|
106
|
+
| published | `https://node:443` | a person's bearer token, or a machine's pinned client certificate |
|
|
107
|
+
|
|
108
|
+
A client certificate is a TLS matter, so it belongs to the runtime rather than
|
|
109
|
+
to this package: hand in a `fetch` that carries it (`options.fetch`).
|
|
110
|
+
|
|
111
|
+
## Things the node insists on
|
|
112
|
+
|
|
113
|
+
- **Cursors belong to their caller.** They are named `c/{service}/…`, which is
|
|
114
|
+
what `door.cursorName()` builds; the node refuses any other name.
|
|
115
|
+
- **Fetching never moves a cursor.** Only `ack` does, and only forward. `tail`
|
|
116
|
+
reads the end of a stream without touching it, which is what a view wants.
|
|
117
|
+
- **A gap is not an error.** When records were pruned below the cursor, the page
|
|
118
|
+
says so. `Stream` passes it to `onGap` and acks past it, so it is reported
|
|
119
|
+
once rather than forever.
|
|
120
|
+
- **The node decides what you see.** `/fetch` and `/kv` filter by the caller's
|
|
121
|
+
read grants; a publish is judged against its zone and grants. A 403 carries
|
|
122
|
+
the node's reason.
|
|
123
|
+
|
|
124
|
+
## Ids
|
|
125
|
+
|
|
126
|
+
`deriveAnnotationId()` computes an annotation's id the way the node's own
|
|
127
|
+
contracts package does. It is derived rather than drawn so that create, update
|
|
128
|
+
and delete of one annotation are appends under the same id — a producer that
|
|
129
|
+
runs twice overwrites its own record instead of doubling it.
|
|
130
|
+
|
|
131
|
+
The agreement is checked against `clients/spec/vectors.json`, generated from
|
|
132
|
+
`colca-data-contracts`. If a rule changes there, the tests here fail.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
npm install
|
|
138
|
+
npm run lint
|
|
139
|
+
npm run typecheck
|
|
140
|
+
npm test
|
|
141
|
+
npm run build
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The wire format itself is documented in
|
|
145
|
+
[HTTP API](https://alpamayo-solutions.github.io/colca/http-api/).
|
package/dist/door.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The door's HTTP client: `GET /fetch`, `POST /ack`, `GET /kv`, `GET /self`
|
|
3
|
+
* and `POST /publish`. One class, no runtime dependencies — the platform's
|
|
4
|
+
* `fetch` does the work.
|
|
5
|
+
*
|
|
6
|
+
* Which door a client talks to is decided by the URL and the credential:
|
|
7
|
+
*
|
|
8
|
+
* - the local door (`http://colca`, port 80 inside the deployment network) has
|
|
9
|
+
* no credential. The caller names itself with `X-Colca-Service` and the door
|
|
10
|
+
* registers it on first sight;
|
|
11
|
+
* - the published door (`https://node:443`) wants a credential: a person's
|
|
12
|
+
* bearer token, or a machine's pinned client certificate — the latter is a
|
|
13
|
+
* TLS matter and therefore the runtime's, not this client's (see `agent`).
|
|
14
|
+
*
|
|
15
|
+
* Wire fields arrive in snake_case and are handed on in camelCase; the payload
|
|
16
|
+
* itself is passed through untouched, because decoding it into a contract type
|
|
17
|
+
* is the caller's business.
|
|
18
|
+
*/
|
|
19
|
+
import { type TopicOptions } from "./topics.js";
|
|
20
|
+
export interface DoorRecord {
|
|
21
|
+
offset: number;
|
|
22
|
+
originOffset: number;
|
|
23
|
+
topic: string;
|
|
24
|
+
payload: unknown;
|
|
25
|
+
/** Colca's record timestamp, in unix **milliseconds** (payload times are seconds). */
|
|
26
|
+
ts: number;
|
|
27
|
+
writtenBy: string;
|
|
28
|
+
actorId: string;
|
|
29
|
+
actorLabel: string;
|
|
30
|
+
actorKind: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A pruned range. Present only when the cursor sits below the stream's
|
|
34
|
+
* low-water mark, and reported again until the consumer acks `toOffset`.
|
|
35
|
+
*/
|
|
36
|
+
export interface Gap {
|
|
37
|
+
stream: string;
|
|
38
|
+
fromOffset: number;
|
|
39
|
+
toOffset: number;
|
|
40
|
+
firstTs?: number;
|
|
41
|
+
lastTs?: number;
|
|
42
|
+
approx: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface Page {
|
|
45
|
+
records: DoorRecord[];
|
|
46
|
+
/** Where a following read starts. Fetching never moves the cursor; acking does. */
|
|
47
|
+
next: number;
|
|
48
|
+
gap?: Gap;
|
|
49
|
+
}
|
|
50
|
+
export interface KvEntry {
|
|
51
|
+
path: string;
|
|
52
|
+
nodeId: string;
|
|
53
|
+
topic: string;
|
|
54
|
+
payload: unknown;
|
|
55
|
+
ts: number;
|
|
56
|
+
offset: number;
|
|
57
|
+
}
|
|
58
|
+
export interface SelfInfo {
|
|
59
|
+
ulid: string;
|
|
60
|
+
name: string;
|
|
61
|
+
node: string;
|
|
62
|
+
element: string;
|
|
63
|
+
mount: string;
|
|
64
|
+
limits: {
|
|
65
|
+
maxRecordBytes: number;
|
|
66
|
+
maxBlobBytes: number;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export interface PublishResult {
|
|
70
|
+
stream: string;
|
|
71
|
+
offset: number;
|
|
72
|
+
topic: string;
|
|
73
|
+
command?: unknown;
|
|
74
|
+
}
|
|
75
|
+
export interface FetchOptions {
|
|
76
|
+
/** Records per page, 1…1000. */
|
|
77
|
+
max?: number;
|
|
78
|
+
/** Narrows to a subtree of the UNS path — the part after the node, not the raw topic. */
|
|
79
|
+
prefix?: string;
|
|
80
|
+
/** Only for the `metrics` stream, at most 1000 ids. */
|
|
81
|
+
signalIds?: readonly string[];
|
|
82
|
+
/**
|
|
83
|
+
* Read the *end* of the stream instead of the cursor's position, without
|
|
84
|
+
* moving it. What a view wants when it opens: the last `max` records.
|
|
85
|
+
*/
|
|
86
|
+
tail?: boolean;
|
|
87
|
+
signal?: AbortSignal;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Who a record is attributed to, when the publisher acts for someone else.
|
|
91
|
+
* A service that cannot forward a person's token names their groups instead
|
|
92
|
+
* and must say why — the node logs every such use.
|
|
93
|
+
*/
|
|
94
|
+
export interface Attribution {
|
|
95
|
+
actorId?: string;
|
|
96
|
+
actorLabel?: string;
|
|
97
|
+
actorKind?: string;
|
|
98
|
+
actorGroups?: readonly string[];
|
|
99
|
+
fallbackReason?: string;
|
|
100
|
+
writtenBy?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface DoorOptions {
|
|
103
|
+
/** The door's origin, e.g. `http://colca` or `https://node:443`. */
|
|
104
|
+
baseUrl: string;
|
|
105
|
+
/** This caller's name on the local door. Also owns the `c/{service}/…` cursors. */
|
|
106
|
+
service?: string;
|
|
107
|
+
/** A person's token for the published door. */
|
|
108
|
+
token?: string;
|
|
109
|
+
/** The node's admin token (`X-Colca-Token`), for administrative callers. */
|
|
110
|
+
adminToken?: string;
|
|
111
|
+
timeoutMs?: number;
|
|
112
|
+
/** For tests, proxies, or a Node `Agent` that carries a client certificate. */
|
|
113
|
+
fetch?: typeof globalThis.fetch;
|
|
114
|
+
}
|
|
115
|
+
/** A non-2xx answer from the door, with the node's own words kept. */
|
|
116
|
+
export declare class DoorError extends Error {
|
|
117
|
+
readonly status: number;
|
|
118
|
+
readonly route: string;
|
|
119
|
+
/** The node's denial reason, on a 403. */
|
|
120
|
+
readonly reason?: string | undefined;
|
|
121
|
+
constructor(status: number, route: string, message: string,
|
|
122
|
+
/** The node's denial reason, on a 403. */
|
|
123
|
+
reason?: string | undefined);
|
|
124
|
+
}
|
|
125
|
+
export declare class Door {
|
|
126
|
+
#private;
|
|
127
|
+
readonly service?: string;
|
|
128
|
+
constructor(options: DoorOptions);
|
|
129
|
+
/** The cursor namespace this caller owns. The door refuses any other. */
|
|
130
|
+
cursorName(suffix: string): string;
|
|
131
|
+
/** `GET /fetch` — read forward from the cursor's stored position. */
|
|
132
|
+
fetchPage(stream: string, cursor: string, options?: FetchOptions): Promise<Page>;
|
|
133
|
+
/**
|
|
134
|
+
* `POST /ack` — ack the last **processed** offset. Monotonic: the door never
|
|
135
|
+
* moves a cursor backwards, and answers whether this one moved.
|
|
136
|
+
*/
|
|
137
|
+
ack(stream: string, cursor: string, offset: number): Promise<boolean>;
|
|
138
|
+
/** `POST /ack` with `delete` — retire a cursor. Fine when it never existed. */
|
|
139
|
+
deleteCursor(stream: string, cursor: string): Promise<void>;
|
|
140
|
+
/** `POST /publish` — one record under this caller's identity. */
|
|
141
|
+
publish(topic: string, payload: unknown, attribution?: Attribution): Promise<PublishResult>;
|
|
142
|
+
/** The same, with the topic built from its parts. */
|
|
143
|
+
publishTo(parts: TopicOptions, payload: unknown, attribution?: Attribution): Promise<PublishResult>;
|
|
144
|
+
/**
|
|
145
|
+
* `GET /kv` — the retained entries under `prefix`, every page followed.
|
|
146
|
+
* `contract` narrows the scan at the node, before payloads are decoded.
|
|
147
|
+
*/
|
|
148
|
+
kv(prefix?: string, options?: {
|
|
149
|
+
contract?: string | readonly string[];
|
|
150
|
+
signal?: AbortSignal;
|
|
151
|
+
}): Promise<KvEntry[]>;
|
|
152
|
+
/** `GET /self` — this caller's minted identity and the limits it must respect. */
|
|
153
|
+
self(): Promise<SelfInfo>;
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=door.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"door.d.ts","sourceRoot":"","sources":["../src/door.ts"],"names":[],"mappings":"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"}
|
package/dist/door.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The door's HTTP client: `GET /fetch`, `POST /ack`, `GET /kv`, `GET /self`
|
|
3
|
+
* and `POST /publish`. One class, no runtime dependencies — the platform's
|
|
4
|
+
* `fetch` does the work.
|
|
5
|
+
*
|
|
6
|
+
* Which door a client talks to is decided by the URL and the credential:
|
|
7
|
+
*
|
|
8
|
+
* - the local door (`http://colca`, port 80 inside the deployment network) has
|
|
9
|
+
* no credential. The caller names itself with `X-Colca-Service` and the door
|
|
10
|
+
* registers it on first sight;
|
|
11
|
+
* - the published door (`https://node:443`) wants a credential: a person's
|
|
12
|
+
* bearer token, or a machine's pinned client certificate — the latter is a
|
|
13
|
+
* TLS matter and therefore the runtime's, not this client's (see `agent`).
|
|
14
|
+
*
|
|
15
|
+
* Wire fields arrive in snake_case and are handed on in camelCase; the payload
|
|
16
|
+
* itself is passed through untouched, because decoding it into a contract type
|
|
17
|
+
* is the caller's business.
|
|
18
|
+
*/
|
|
19
|
+
import { topic as buildTopic } from "./topics.js";
|
|
20
|
+
/** A non-2xx answer from the door, with the node's own words kept. */
|
|
21
|
+
export class DoorError extends Error {
|
|
22
|
+
status;
|
|
23
|
+
route;
|
|
24
|
+
reason;
|
|
25
|
+
constructor(status, route, message,
|
|
26
|
+
/** The node's denial reason, on a 403. */
|
|
27
|
+
reason) {
|
|
28
|
+
super(`${route}: ${String(status)} ${message}`);
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.route = route;
|
|
31
|
+
this.reason = reason;
|
|
32
|
+
this.name = "DoorError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export class Door {
|
|
36
|
+
#base;
|
|
37
|
+
#headers;
|
|
38
|
+
#timeout;
|
|
39
|
+
#fetch;
|
|
40
|
+
service;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.#base = options.baseUrl.replace(/\/+$/, "");
|
|
43
|
+
this.service = options.service;
|
|
44
|
+
this.#timeout = options.timeoutMs ?? 10_000;
|
|
45
|
+
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
46
|
+
this.#headers = { "content-type": "application/json" };
|
|
47
|
+
if (options.service)
|
|
48
|
+
this.#headers["x-colca-service"] = options.service;
|
|
49
|
+
if (options.token)
|
|
50
|
+
this.#headers.authorization = `Bearer ${options.token}`;
|
|
51
|
+
if (options.adminToken)
|
|
52
|
+
this.#headers["x-colca-token"] = options.adminToken;
|
|
53
|
+
}
|
|
54
|
+
/** The cursor namespace this caller owns. The door refuses any other. */
|
|
55
|
+
cursorName(suffix) {
|
|
56
|
+
if (!this.service) {
|
|
57
|
+
throw new Error("cursorName() needs the service name the cursors are namespaced by");
|
|
58
|
+
}
|
|
59
|
+
return `c/${this.service}/${suffix}`;
|
|
60
|
+
}
|
|
61
|
+
/** `GET /fetch` — read forward from the cursor's stored position. */
|
|
62
|
+
async fetchPage(stream, cursor, options = {}) {
|
|
63
|
+
const query = new URLSearchParams({ stream, cursor });
|
|
64
|
+
if (options.max !== undefined)
|
|
65
|
+
query.set("max", String(options.max));
|
|
66
|
+
if (options.prefix)
|
|
67
|
+
query.set("prefix", options.prefix);
|
|
68
|
+
if (options.tail)
|
|
69
|
+
query.set("tail", "1");
|
|
70
|
+
for (const id of options.signalIds ?? [])
|
|
71
|
+
query.append("signal_id", id);
|
|
72
|
+
const body = await this.#call("GET", `/fetch?${query.toString()}`, undefined, options.signal);
|
|
73
|
+
return {
|
|
74
|
+
records: (body.records ?? []).map(toRecord),
|
|
75
|
+
next: body.next,
|
|
76
|
+
gap: body.gap ? toGap(body.gap) : undefined,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `POST /ack` — ack the last **processed** offset. Monotonic: the door never
|
|
81
|
+
* moves a cursor backwards, and answers whether this one moved.
|
|
82
|
+
*/
|
|
83
|
+
async ack(stream, cursor, offset) {
|
|
84
|
+
const body = await this.#call("POST", "/ack", { cursor, stream, offset });
|
|
85
|
+
return body.moved;
|
|
86
|
+
}
|
|
87
|
+
/** `POST /ack` with `delete` — retire a cursor. Fine when it never existed. */
|
|
88
|
+
async deleteCursor(stream, cursor) {
|
|
89
|
+
await this.#call("POST", "/ack", { cursor, stream, delete: true });
|
|
90
|
+
}
|
|
91
|
+
/** `POST /publish` — one record under this caller's identity. */
|
|
92
|
+
async publish(topic, payload, attribution = {}) {
|
|
93
|
+
return this.#call("POST", "/publish", {
|
|
94
|
+
topic,
|
|
95
|
+
payload,
|
|
96
|
+
written_by: attribution.writtenBy,
|
|
97
|
+
actor_id: attribution.actorId,
|
|
98
|
+
actor_label: attribution.actorLabel,
|
|
99
|
+
actor_kind: attribution.actorKind,
|
|
100
|
+
actor_groups: attribution.actorGroups,
|
|
101
|
+
fallback_reason: attribution.fallbackReason,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** The same, with the topic built from its parts. */
|
|
105
|
+
async publishTo(parts, payload, attribution = {}) {
|
|
106
|
+
return this.publish(buildTopic(parts), payload, attribution);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `GET /kv` — the retained entries under `prefix`, every page followed.
|
|
110
|
+
* `contract` narrows the scan at the node, before payloads are decoded.
|
|
111
|
+
*/
|
|
112
|
+
async kv(prefix = "", options = {}) {
|
|
113
|
+
const contracts = typeof options.contract === "string" ? [options.contract] : (options.contract ?? []);
|
|
114
|
+
const entries = [];
|
|
115
|
+
let after = "";
|
|
116
|
+
for (;;) {
|
|
117
|
+
const query = new URLSearchParams({ prefix, max: "10000" });
|
|
118
|
+
for (const name of contracts)
|
|
119
|
+
query.append("contract", name);
|
|
120
|
+
if (after)
|
|
121
|
+
query.set("after", after);
|
|
122
|
+
const body = await this.#call("GET", `/kv?${query.toString()}`, undefined, options.signal);
|
|
123
|
+
for (const entry of body.entries ?? []) {
|
|
124
|
+
entries.push({
|
|
125
|
+
path: entry.path,
|
|
126
|
+
nodeId: entry.node_id,
|
|
127
|
+
topic: entry.topic,
|
|
128
|
+
payload: entry.payload,
|
|
129
|
+
ts: entry.ts,
|
|
130
|
+
offset: entry.offset,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const next = body.next ?? "";
|
|
134
|
+
if (!next)
|
|
135
|
+
return entries;
|
|
136
|
+
if (next === after)
|
|
137
|
+
throw new Error("GET /kv: the node repeated a page token");
|
|
138
|
+
after = next;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** `GET /self` — this caller's minted identity and the limits it must respect. */
|
|
142
|
+
async self() {
|
|
143
|
+
const body = await this.#call("GET", "/self");
|
|
144
|
+
return {
|
|
145
|
+
ulid: body.ulid,
|
|
146
|
+
name: body.name,
|
|
147
|
+
node: body.node,
|
|
148
|
+
element: body.element,
|
|
149
|
+
mount: body.mount,
|
|
150
|
+
limits: {
|
|
151
|
+
maxRecordBytes: body.limits.max_record_bytes,
|
|
152
|
+
maxBlobBytes: body.limits.max_blob_bytes,
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
async #call(method, route, body, signal) {
|
|
157
|
+
// One timeout per call, dropped again as soon as the answer is in, so a
|
|
158
|
+
// long-lived client does not collect timers.
|
|
159
|
+
const timer = AbortSignal.timeout(this.#timeout);
|
|
160
|
+
const response = await this.#fetch(`${this.#base}${route}`, {
|
|
161
|
+
method,
|
|
162
|
+
headers: this.#headers,
|
|
163
|
+
body: body === undefined ? undefined : JSON.stringify(stripUndefined(body)),
|
|
164
|
+
signal: signal ? AbortSignal.any([signal, timer]) : timer,
|
|
165
|
+
});
|
|
166
|
+
const text = await response.text();
|
|
167
|
+
const parsed = text ? safeJson(text) : undefined;
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
const error = parsed;
|
|
170
|
+
throw new DoorError(response.status, route.split("?")[0], error?.error ?? text, error?.reason);
|
|
171
|
+
}
|
|
172
|
+
return parsed;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function toRecord(r) {
|
|
176
|
+
return {
|
|
177
|
+
offset: r.offset,
|
|
178
|
+
originOffset: r.origin_offset,
|
|
179
|
+
topic: r.topic,
|
|
180
|
+
payload: r.payload,
|
|
181
|
+
ts: r.ts,
|
|
182
|
+
writtenBy: r.written_by ?? "",
|
|
183
|
+
actorId: r.actor_id ?? "",
|
|
184
|
+
actorLabel: r.actor_label ?? "",
|
|
185
|
+
actorKind: r.actor_kind ?? "",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function toGap(g) {
|
|
189
|
+
return {
|
|
190
|
+
stream: g.stream,
|
|
191
|
+
fromOffset: g.from_offset,
|
|
192
|
+
toOffset: g.to_offset,
|
|
193
|
+
firstTs: g.first_ts,
|
|
194
|
+
lastTs: g.last_ts,
|
|
195
|
+
approx: g.approx ?? false,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/** Attribution fields left unset must not reach the node as nulls. */
|
|
199
|
+
function stripUndefined(value) {
|
|
200
|
+
if (value === null || typeof value !== "object")
|
|
201
|
+
return value;
|
|
202
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
|
|
203
|
+
}
|
|
204
|
+
function safeJson(text) {
|
|
205
|
+
try {
|
|
206
|
+
return JSON.parse(text);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=door.js.map
|
package/dist/door.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"door.js","sourceRoot":"","sources":["../src/door.ts"],"names":[],"mappings":"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"}
|