@procedures/client 0.3.0 → 0.5.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/CHANGELOG.md +68 -0
- package/README.md +62 -7
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts +2 -1
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +9 -2
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +3 -1
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +8 -6
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/local/client/transport.d.ts +2 -1
- package/dist/adapters/local/client/transport.d.ts.map +1 -1
- package/dist/adapters/local/client/transport.js +12 -4
- package/dist/adapters/local/client/transport.js.map +1 -1
- package/dist/adapters/local/client/types.d.ts +6 -0
- package/dist/adapters/local/client/types.d.ts.map +1 -1
- package/dist/adapters/local/client/types.js.map +1 -1
- package/dist/adapters/sequence.d.ts +36 -0
- package/dist/adapters/sequence.d.ts.map +1 -0
- package/dist/adapters/sequence.js +55 -0
- package/dist/adapters/sequence.js.map +1 -0
- package/dist/adapters/websocket/client/node.d.ts +30 -0
- package/dist/adapters/websocket/client/node.d.ts.map +1 -0
- package/dist/adapters/websocket/client/node.js +32 -0
- package/dist/adapters/websocket/client/node.js.map +1 -0
- package/dist/adapters/websocket/client/transport.d.ts +39 -1
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +119 -16
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/client/types.d.ts +15 -5
- package/dist/adapters/websocket/client/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/types.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +22 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +29 -1
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +6 -0
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.d.ts +4 -3
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +18 -9
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +3 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +4 -0
- package/dist/browser.js.map +1 -1
- package/dist/client/errors/http-errors.d.ts.map +1 -1
- package/dist/client/errors/http-errors.js +6 -0
- package/dist/client/errors/http-errors.js.map +1 -1
- package/dist/client/errors/index.d.ts +1 -1
- package/dist/client/errors/index.js +1 -1
- package/dist/client/errors/registry.d.ts +2 -1
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +73 -22
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/middleware/rate-limit.d.ts.map +1 -1
- package/dist/client/middleware/rate-limit.js +0 -1
- package/dist/client/middleware/rate-limit.js.map +1 -1
- package/dist/client/middleware/timeout.d.ts.map +1 -1
- package/dist/client/middleware/timeout.js +2 -9
- package/dist/client/middleware/timeout.js.map +1 -1
- package/dist/events/index.d.ts +4 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +3 -0
- package/dist/events/index.js.map +1 -1
- package/dist/events/publisher.d.ts +95 -0
- package/dist/events/publisher.d.ts.map +1 -0
- package/dist/events/publisher.js +268 -0
- package/dist/events/publisher.js.map +1 -0
- package/dist/events/remote.d.ts +164 -0
- package/dist/events/remote.d.ts.map +1 -0
- package/dist/events/remote.js +250 -0
- package/dist/events/remote.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +11 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +10 -0
- package/dist/node.js.map +1 -0
- package/dist/procedures/collection/index.d.ts +1 -1
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +6 -1
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts +16 -1
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +13 -2
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +14 -2
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/core/array.d.ts +1 -1
- package/dist/procedures/core/array.d.ts.map +1 -1
- package/dist/procedures/core/array.js +56 -15
- package/dist/procedures/core/array.js.map +1 -1
- package/dist/procedures/core/index.d.ts.map +1 -1
- package/dist/procedures/core/index.js +9 -2
- package/dist/procedures/core/index.js.map +1 -1
- package/dist/procedures/core/logic.js +1 -1
- package/dist/procedures/core/logic.js.map +1 -1
- package/dist/procedures/core/math.d.ts.map +1 -1
- package/dist/procedures/core/math.js +5 -1
- package/dist/procedures/core/math.js.map +1 -1
- package/dist/procedures/core/meta.d.ts.map +1 -1
- package/dist/procedures/core/meta.js +41 -7
- package/dist/procedures/core/meta.js.map +1 -1
- package/dist/procedures/core/type.d.ts.map +1 -1
- package/dist/procedures/core/type.js +4 -2
- package/dist/procedures/core/type.js.map +1 -1
- package/dist/procedures/discovery.d.ts +4 -6
- package/dist/procedures/discovery.d.ts.map +1 -1
- package/dist/procedures/discovery.js +4 -6
- package/dist/procedures/discovery.js.map +1 -1
- package/dist/procedures/invoke.d.ts +4 -2
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +1 -1
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/ref.d.ts +4 -2
- package/dist/procedures/ref.d.ts.map +1 -1
- package/dist/procedures/ref.js +2 -1
- package/dist/procedures/ref.js.map +1 -1
- package/dist/protocol.d.ts +10 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +13 -2
- package/dist/protocol.js.map +1 -1
- package/dist/server/host-policy.d.ts +11 -0
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +5 -0
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/procedure-server.d.ts +26 -3
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +38 -6
- package/dist/server/procedure-server.js.map +1 -1
- package/package.json +7 -6
- package/src/adapters/http/client/transport.ts +9 -2
- package/src/adapters/http/server/transport.ts +8 -6
- package/src/adapters/local/client/transport.ts +12 -4
- package/src/adapters/local/client/types.ts +7 -0
- package/src/adapters/sequence.ts +55 -0
- package/src/adapters/websocket/client/node.ts +34 -0
- package/src/adapters/websocket/client/transport.ts +128 -16
- package/src/adapters/websocket/client/types.ts +16 -5
- package/src/adapters/websocket/server/transport.ts +43 -1
- package/src/adapters/websocket/server/types.ts +7 -0
- package/src/adapters/websocket/shared/rpc.ts +19 -9
- package/src/browser.ts +15 -0
- package/src/client/errors/README.md +2 -0
- package/src/client/errors/http-errors.ts +5 -0
- package/src/client/errors/index.ts +1 -1
- package/src/client/errors/registry.ts +80 -22
- package/src/client/middleware/rate-limit.ts +0 -2
- package/src/client/middleware/timeout.ts +2 -9
- package/src/events/index.ts +16 -0
- package/src/events/publisher.ts +314 -0
- package/src/events/remote.ts +369 -0
- package/src/index.ts +4 -0
- package/src/node.ts +11 -0
- package/src/procedures/collection/index.ts +1 -0
- package/src/procedures/collection/procedures.ts +6 -1
- package/src/procedures/collection/schemas.ts +28 -2
- package/src/procedures/collection/storage.ts +14 -2
- package/src/procedures/core/array.ts +53 -16
- package/src/procedures/core/index.ts +9 -2
- package/src/procedures/core/logic.ts +1 -1
- package/src/procedures/core/math.ts +5 -1
- package/src/procedures/core/meta.ts +43 -7
- package/src/procedures/core/type.ts +4 -2
- package/src/procedures/discovery.ts +4 -6
- package/src/procedures/invoke.ts +4 -2
- package/src/procedures/ref.ts +4 -2
- package/src/protocol.ts +16 -3
- package/src/server/host-policy.ts +14 -0
- package/src/server/procedure-server.ts +45 -8
- package/dist/procedures/cli/announce.d.ts +0 -67
- package/dist/procedures/cli/announce.d.ts.map +0 -1
- package/dist/procedures/cli/announce.js +0 -392
- package/dist/procedures/cli/announce.js.map +0 -1
- package/dist/procedures/cli/discover.d.ts +0 -66
- package/dist/procedures/cli/discover.d.ts.map +0 -1
- package/dist/procedures/cli/discover.js +0 -310
- package/dist/procedures/cli/discover.js.map +0 -1
- package/dist/procedures/cli/index.d.ts +0 -13
- package/dist/procedures/cli/index.d.ts.map +0 -1
- package/dist/procedures/cli/index.js +0 -203
- package/dist/procedures/cli/index.js.map +0 -1
- package/dist/procedures/cli/init.d.ts +0 -17
- package/dist/procedures/cli/init.d.ts.map +0 -1
- package/dist/procedures/cli/init.js +0 -336
- package/dist/procedures/cli/init.js.map +0 -1
- package/src/procedures/cli/announce.ts +0 -470
- package/src/procedures/cli/discover.ts +0 -390
- package/src/procedures/cli/index.ts +0 -231
- package/src/procedures/cli/init.ts +0 -370
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subscriber of the remote events (`documentation/EVENTS.md`).
|
|
3
|
+
*
|
|
4
|
+
* A peer subscribes to the topics of the bus of another peer with a streaming call of the
|
|
5
|
+
* procedure `events.subscribe`. The first item of the stream confirms the subscription, and each
|
|
6
|
+
* other item is one event. The frames are the frames of each call: credit is the backpressure,
|
|
7
|
+
* and a cancel is the unsubscribe. Thus each transport with streams carries a subscription:
|
|
8
|
+
* WebSocket in the two directions, HTTP with NDJSON, a message port and the local transport.
|
|
9
|
+
*
|
|
10
|
+
* - `remoteEvents()` gives an async iterator of the events.
|
|
11
|
+
* - `subscribeRemote()` calls a handler for each event, and gives the unsubscribe function.
|
|
12
|
+
*
|
|
13
|
+
* After a failure that a retry can repair (a closed connection, `UNAVAILABLE`, `TIMEOUT`), the
|
|
14
|
+
* subscriber sends the call again after a backoff. The delivery is at most once: the events
|
|
15
|
+
* between the failure and the new confirmation are lost. Each other error ends the subscription.
|
|
16
|
+
*
|
|
17
|
+
* This file imports only types and the backoff, so a page or a worker that subscribes stays small.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Message, Method, ResponseItem, Transport } from "../client/types.js";
|
|
21
|
+
import { backoffDelay } from "@mark1russell7/service/backoff";
|
|
22
|
+
|
|
23
|
+
/** The path of the procedure of a subscription. */
|
|
24
|
+
export const EVENTS_SUBSCRIBE_PATH: readonly ["events", "subscribe"] = Object.freeze(["events", "subscribe"] as const);
|
|
25
|
+
|
|
26
|
+
/** The method of the procedure of a subscription. */
|
|
27
|
+
const SUBSCRIBE_METHOD: Method = Object.freeze({ service: "events", operation: "subscribe" });
|
|
28
|
+
|
|
29
|
+
/** The input of `events.subscribe`. */
|
|
30
|
+
export interface SubscribeInput {
|
|
31
|
+
/** The topics: from 1 to 64 names of 1 to 256 characters. */
|
|
32
|
+
readonly topics: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An item of the stream of `events.subscribe`. The first item is `subscribed`. Each other item is
|
|
37
|
+
* an `event`. A subscriber ignores an item of another type: it comes from a newer publisher.
|
|
38
|
+
*/
|
|
39
|
+
export type EventItem =
|
|
40
|
+
| { readonly type: "subscribed"; readonly topics: readonly string[] }
|
|
41
|
+
| {
|
|
42
|
+
readonly type: "event";
|
|
43
|
+
readonly topic: string;
|
|
44
|
+
readonly data: unknown;
|
|
45
|
+
/** The number of the event in its subscription, from 1. A gap shows lost events. */
|
|
46
|
+
readonly seq: number;
|
|
47
|
+
/** The number of events that the publisher dropped immediately before this one. */
|
|
48
|
+
readonly dropped?: number | undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** One event that a subscriber gets. */
|
|
52
|
+
export interface RemoteEvent<T = unknown> {
|
|
53
|
+
/** The topic (the channel of the bus of the publisher). */
|
|
54
|
+
readonly topic: string;
|
|
55
|
+
/** The data of the event, after a trip as JSON. */
|
|
56
|
+
readonly data: T;
|
|
57
|
+
/** The number of the event in its subscription, from 1. A resubscribe starts again at 1. */
|
|
58
|
+
readonly seq: number;
|
|
59
|
+
/** The number of events that the publisher dropped immediately before this one: 0 without a loss. */
|
|
60
|
+
readonly dropped: number;
|
|
61
|
+
/** The id of the subscription. It stays the same after a resubscribe. */
|
|
62
|
+
readonly subscription: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The handler of the events of a subscription. When it gives a promise, the subscriber waits for
|
|
67
|
+
* it before the next event, so a slow handler holds the credit of its own subscription only.
|
|
68
|
+
*/
|
|
69
|
+
export type RemoteEventHandler<T = unknown> = (data: T, event: RemoteEvent<T>) => void | Promise<void>;
|
|
70
|
+
|
|
71
|
+
/** The options of a subscription. */
|
|
72
|
+
export interface RemoteEventsOptions {
|
|
73
|
+
/** When this signal aborts, the subscription ends: the transport sends a cancel. */
|
|
74
|
+
signal?: AbortSignal | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* True: the subscriber sends the call again after a failure that a retry can repair. False: such
|
|
77
|
+
* a failure ends the subscription.
|
|
78
|
+
* @default true
|
|
79
|
+
*/
|
|
80
|
+
resubscribe?: boolean | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* This function gives the time before resubscribe try `attempt` (from 1), in milliseconds. The
|
|
83
|
+
* default is an exponential backoff from 250 ms to 30 s with full jitter. The WebSocket
|
|
84
|
+
* transport uses its reconnect backoff.
|
|
85
|
+
*/
|
|
86
|
+
backoff?: ((attempt: number) => number) | undefined;
|
|
87
|
+
/** This function gets the topics of each confirmation: the first one, and one after each resubscribe. */
|
|
88
|
+
onSubscribed?: ((topics: readonly string[]) => void) | undefined;
|
|
89
|
+
/** The metadata of the call, for example a trace. */
|
|
90
|
+
metadata?: Record<string, unknown> | undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The options of `subscribeRemote()`. */
|
|
94
|
+
export interface SubscribeOptions extends RemoteEventsOptions {
|
|
95
|
+
/**
|
|
96
|
+
* This function gets the error that ends the subscription: a refusal (`TOPIC_NOT_FOUND`,
|
|
97
|
+
* `FORBIDDEN`), an older publisher (`NOT_FOUND`), or a transport that closed. An unsubscribe is
|
|
98
|
+
* not an error.
|
|
99
|
+
*/
|
|
100
|
+
onError?: ((error: SubscriptionError) => void) | undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The unsubscribe function of a subscription. A second call does nothing. `ready` resolves at the
|
|
105
|
+
* first confirmation of the publisher. It rejects when the subscription ends before it.
|
|
106
|
+
*/
|
|
107
|
+
export interface Subscription {
|
|
108
|
+
(): void;
|
|
109
|
+
/** This promise resolves when the publisher confirms the subscription the first time. */
|
|
110
|
+
readonly ready: Promise<void>;
|
|
111
|
+
/** The id of the subscription: the `subscription` of its events. */
|
|
112
|
+
readonly id: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The error that ends a subscription. */
|
|
116
|
+
export class SubscriptionError extends Error {
|
|
117
|
+
override readonly name = "SubscriptionError";
|
|
118
|
+
|
|
119
|
+
constructor(
|
|
120
|
+
/** The code: a code of the protocol, for example `TOPIC_NOT_FOUND`. */
|
|
121
|
+
readonly code: string,
|
|
122
|
+
message: string,
|
|
123
|
+
/** True when a resubscribe can repair the failure. */
|
|
124
|
+
readonly retryable: boolean,
|
|
125
|
+
/** The reason of a `FORBIDDEN` error, for example `topic-not-allowed`. */
|
|
126
|
+
readonly reason?: string | undefined,
|
|
127
|
+
) {
|
|
128
|
+
super(message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The parts that a transport adds to its subscriptions. The WebSocket transport waits for its
|
|
134
|
+
* connection, uses its reconnect backoff, ends its subscriptions at `close()`, and gives each
|
|
135
|
+
* event to its `onEvent` option.
|
|
136
|
+
*/
|
|
137
|
+
export interface SubscriptionHooks {
|
|
138
|
+
/** When this signal aborts, each subscription ends without an error. */
|
|
139
|
+
readonly lifetime?: AbortSignal | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* This function resolves when the transport can send the call. It rejects when the transport
|
|
142
|
+
* closed. After a lost connection, the subscriber waits for it, not for a backoff.
|
|
143
|
+
*/
|
|
144
|
+
readonly ready?: ((signal: AbortSignal) => Promise<void>) | undefined;
|
|
145
|
+
/** The backoff, when the options give none. */
|
|
146
|
+
readonly backoff?: ((attempt: number) => number) | undefined;
|
|
147
|
+
/** This function gets each event of each subscription. */
|
|
148
|
+
readonly onEvent?: ((event: RemoteEvent) => void) | undefined;
|
|
149
|
+
/** False: no resubscribe, whatever the options say (a server cannot reach a closed connection). */
|
|
150
|
+
readonly resubscribe?: boolean | undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The part of a transport that a subscription uses. */
|
|
154
|
+
export type EventTransport = Pick<Transport, "send">;
|
|
155
|
+
|
|
156
|
+
/** The codes after which a resubscribe can succeed, also when the item does not say retryable. */
|
|
157
|
+
const RESUBSCRIBE_CODES: ReadonlySet<string> = new Set(["UNAVAILABLE", "TIMEOUT", "CONNECTION_CLOSED", "ABORTED"]);
|
|
158
|
+
|
|
159
|
+
/** The default backoff: from 250 ms, twice as long at each try, at most 30 s, with full jitter. */
|
|
160
|
+
function defaultBackoff(attempt: number): number {
|
|
161
|
+
return backoffDelay({ base: 250, factor: 2, cap: 30_000, jitter: { kind: "subtract", fraction: 1 } }, attempt);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let subscriptions = 0;
|
|
165
|
+
|
|
166
|
+
/** The topics of a subscription as a list. */
|
|
167
|
+
function topicList(topics: string | readonly string[]): readonly string[] {
|
|
168
|
+
return typeof topics === "string" ? [topics] : [...topics];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** A signal that aborts when one of the signals aborts, and a function that removes its listeners. */
|
|
172
|
+
function linkSignals(signals: ReadonlyArray<AbortSignal | undefined>): { signal: AbortSignal; unlink: () => void } {
|
|
173
|
+
const controller = new AbortController();
|
|
174
|
+
const present = signals.filter((signal): signal is AbortSignal => signal !== undefined);
|
|
175
|
+
const abort = (): void => controller.abort();
|
|
176
|
+
if (present.some((signal) => signal.aborted)) controller.abort();
|
|
177
|
+
else for (const signal of present) signal.addEventListener("abort", abort, { once: true });
|
|
178
|
+
return {
|
|
179
|
+
signal: controller.signal,
|
|
180
|
+
unlink: () => {
|
|
181
|
+
for (const signal of present) signal.removeEventListener("abort", abort);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** This function waits for a time, or until the signal aborts. */
|
|
187
|
+
function pause(ms: number, signal: AbortSignal): Promise<void> {
|
|
188
|
+
if (signal.aborted || !(ms > 0)) return Promise.resolve();
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
const done = (): void => {
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
signal.removeEventListener("abort", done);
|
|
193
|
+
resolve();
|
|
194
|
+
};
|
|
195
|
+
const timer = setTimeout(done, ms);
|
|
196
|
+
signal.addEventListener("abort", done, { once: true });
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The error of an error item of the call. */
|
|
201
|
+
function errorOfItem(item: ResponseItem<unknown>): SubscriptionError {
|
|
202
|
+
const status = item.status;
|
|
203
|
+
if (status.type !== "error") return new SubscriptionError("INTERNAL_ERROR", "The item is not an error", false);
|
|
204
|
+
const code = String(status.code);
|
|
205
|
+
const reason = item.metadata?.["reason"];
|
|
206
|
+
return new SubscriptionError(code, status.message, status.retryable === true || RESUBSCRIBE_CODES.has(code), typeof reason === "string" ? reason : undefined);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The error of a call that failed in the transport: the connection closed. A resubscribe can repair it. */
|
|
210
|
+
function errorOfFailure(error: unknown): SubscriptionError {
|
|
211
|
+
if (error instanceof SubscriptionError) return error;
|
|
212
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
213
|
+
return new SubscriptionError("CONNECTION_CLOSED", message, true);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The events of a subscription, with each resubscribe. The generator ends at the unsubscribe. */
|
|
217
|
+
async function* eventsOf<T>(
|
|
218
|
+
transport: EventTransport,
|
|
219
|
+
topics: readonly string[],
|
|
220
|
+
options: RemoteEventsOptions,
|
|
221
|
+
hooks: SubscriptionHooks,
|
|
222
|
+
id: string,
|
|
223
|
+
): AsyncGenerator<RemoteEvent<T>, void, undefined> {
|
|
224
|
+
const { signal, unlink } = linkSignals([options.signal, hooks.lifetime]);
|
|
225
|
+
const resubscribe = hooks.resubscribe !== false && options.resubscribe !== false;
|
|
226
|
+
const backoff = options.backoff ?? hooks.backoff ?? defaultBackoff;
|
|
227
|
+
let attempt = 0;
|
|
228
|
+
let tries = 0;
|
|
229
|
+
try {
|
|
230
|
+
for (;;) {
|
|
231
|
+
if (signal.aborted) return;
|
|
232
|
+
if (hooks.ready) {
|
|
233
|
+
await hooks.ready(signal);
|
|
234
|
+
if (signal.aborted) return;
|
|
235
|
+
}
|
|
236
|
+
const message: Message<SubscribeInput> = {
|
|
237
|
+
id: `${id}.${++tries}`,
|
|
238
|
+
method: SUBSCRIBE_METHOD,
|
|
239
|
+
payload: { topics: [...topics] },
|
|
240
|
+
metadata: { ...(options.metadata ?? {}) },
|
|
241
|
+
signal,
|
|
242
|
+
};
|
|
243
|
+
let failure: { error: SubscriptionError; lost: boolean } | undefined;
|
|
244
|
+
try {
|
|
245
|
+
for await (const item of transport.send<SubscribeInput, EventItem>(message)) {
|
|
246
|
+
if (item.status.type === "error") {
|
|
247
|
+
failure = { error: errorOfItem(item), lost: false };
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
const payload = item.payload as EventItem | undefined;
|
|
251
|
+
if (payload === null || typeof payload !== "object") continue;
|
|
252
|
+
if (payload.type === "subscribed") {
|
|
253
|
+
attempt = 0;
|
|
254
|
+
options.onSubscribed?.(payload.topics);
|
|
255
|
+
} else if (payload.type === "event") {
|
|
256
|
+
const event: RemoteEvent<T> = {
|
|
257
|
+
topic: payload.topic,
|
|
258
|
+
data: payload.data as T,
|
|
259
|
+
seq: payload.seq,
|
|
260
|
+
dropped: payload.dropped ?? 0,
|
|
261
|
+
subscription: id,
|
|
262
|
+
};
|
|
263
|
+
hooks.onEvent?.(event as RemoteEvent);
|
|
264
|
+
yield event;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch (error) {
|
|
268
|
+
failure = { error: errorOfFailure(error), lost: true };
|
|
269
|
+
}
|
|
270
|
+
// An unsubscribe, or the end of the transport: no error
|
|
271
|
+
if (signal.aborted) return;
|
|
272
|
+
// The publisher ended the subscription
|
|
273
|
+
if (!failure) return;
|
|
274
|
+
if (!resubscribe || !failure.error.retryable) throw failure.error;
|
|
275
|
+
attempt++;
|
|
276
|
+
// After a lost connection, the transport waits for its reconnect: no second backoff
|
|
277
|
+
if (!(failure.lost && hooks.ready)) await pause(backoff(attempt), signal);
|
|
278
|
+
}
|
|
279
|
+
} finally {
|
|
280
|
+
unlink();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* This function gives the events of a subscription to topics of the bus of the other side. The
|
|
286
|
+
* iteration sends the call. A `break` of the reader, or the abort of the signal, sends the cancel.
|
|
287
|
+
* An error that ends the subscription ends the iteration with a `SubscriptionError`.
|
|
288
|
+
*
|
|
289
|
+
* @param transport - The transport to the publisher: HTTP, WebSocket, a message port, local
|
|
290
|
+
* @param topics - One topic, or a list of 1 to 64 topics
|
|
291
|
+
* @param options - The signal, the resubscribe, the backoff and the confirmation handler
|
|
292
|
+
* @param hooks - The parts that a transport adds (its connection, its backoff, its `onEvent`)
|
|
293
|
+
*/
|
|
294
|
+
export function remoteEvents<T = unknown>(
|
|
295
|
+
transport: EventTransport,
|
|
296
|
+
topics: string | readonly string[],
|
|
297
|
+
options: RemoteEventsOptions = {},
|
|
298
|
+
hooks: SubscriptionHooks = {},
|
|
299
|
+
): AsyncIterable<RemoteEvent<T>> {
|
|
300
|
+
const list = topicList(topics);
|
|
301
|
+
return {
|
|
302
|
+
[Symbol.asyncIterator]: () => eventsOf<T>(transport, list, options, hooks, `events-${++subscriptions}`),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* This function subscribes to topics of the bus of the other side, and calls the handler for each
|
|
308
|
+
* event in order. It gives the unsubscribe function, with the `ready` promise of the first
|
|
309
|
+
* confirmation. An error of the handler goes to `console.error`, and the subscription goes on.
|
|
310
|
+
*
|
|
311
|
+
* @param transport - The transport to the publisher
|
|
312
|
+
* @param topics - One topic, or a list of 1 to 64 topics
|
|
313
|
+
* @param handler - The handler of each event: it gets the data and the event
|
|
314
|
+
* @param options - The options of `remoteEvents()`, and the `onError` handler of the end
|
|
315
|
+
* @param hooks - The parts that a transport adds
|
|
316
|
+
*/
|
|
317
|
+
export function subscribeRemote<T = unknown>(
|
|
318
|
+
transport: EventTransport,
|
|
319
|
+
topics: string | readonly string[],
|
|
320
|
+
handler: RemoteEventHandler<T>,
|
|
321
|
+
options: SubscribeOptions = {},
|
|
322
|
+
hooks: SubscriptionHooks = {},
|
|
323
|
+
): Subscription {
|
|
324
|
+
const controller = new AbortController();
|
|
325
|
+
const id = `events-${++subscriptions}`;
|
|
326
|
+
let confirmed = false;
|
|
327
|
+
let confirm: () => void = () => undefined;
|
|
328
|
+
let refuse: (error: SubscriptionError) => void = () => undefined;
|
|
329
|
+
const ready = new Promise<void>((resolve, reject) => {
|
|
330
|
+
confirm = resolve;
|
|
331
|
+
refuse = reject;
|
|
332
|
+
});
|
|
333
|
+
// A caller that does not wait for `ready` gets no unhandled rejection
|
|
334
|
+
ready.catch(() => undefined);
|
|
335
|
+
const { signal, unlink } = linkSignals([options.signal, controller.signal]);
|
|
336
|
+
const settings: RemoteEventsOptions = {
|
|
337
|
+
...options,
|
|
338
|
+
signal,
|
|
339
|
+
onSubscribed: (list) => {
|
|
340
|
+
if (!confirmed) {
|
|
341
|
+
confirmed = true;
|
|
342
|
+
confirm();
|
|
343
|
+
}
|
|
344
|
+
options.onSubscribed?.(list);
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
void (async () => {
|
|
349
|
+
try {
|
|
350
|
+
for await (const event of { [Symbol.asyncIterator]: () => eventsOf<T>(transport, topicList(topics), settings, hooks, id) }) {
|
|
351
|
+
try {
|
|
352
|
+
await handler(event.data, event);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
console.error(`[events] The handler of ${event.topic} failed:`, error);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (!confirmed) refuse(new SubscriptionError("ABORTED", "The subscription ended before its confirmation", false));
|
|
358
|
+
} catch (error) {
|
|
359
|
+
const failure = errorOfFailure(error);
|
|
360
|
+
if (!confirmed) refuse(failure);
|
|
361
|
+
options.onError?.(failure);
|
|
362
|
+
} finally {
|
|
363
|
+
unlink();
|
|
364
|
+
}
|
|
365
|
+
})();
|
|
366
|
+
|
|
367
|
+
const unsubscribe = (): void => controller.abort();
|
|
368
|
+
return Object.assign(unsubscribe, { ready, id });
|
|
369
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*
|
|
7
7
|
* This entry point is for Node. It exports the browser-safe entry point
|
|
8
8
|
* ("@procedures/client/browser") and the server transports, which use `http` and `ws`.
|
|
9
|
+
*
|
|
10
|
+
* The `WebSocketTransport` of this entry uses the global `WebSocket`, because some applications
|
|
11
|
+
* bundle this entry for a browser. The Node entry "@procedures/client/node" has a
|
|
12
|
+
* `WebSocketTransport` whose sockets are of the `ws` package: a drop ends them at once.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
export * from "./browser.js";
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node entry of `@procedures/client`: `@procedures/client/node`.
|
|
3
|
+
*
|
|
4
|
+
* This entry exports the client transports with the defaults of Node. Its `WebSocketTransport`
|
|
5
|
+
* uses the `WebSocket` class of the `ws` package, so a drop ends a socket at once with
|
|
6
|
+
* `terminate()` (finding of roadmap R36). The other entries keep the global `WebSocket`: a browser
|
|
7
|
+
* bundle imports `@procedures/client/browser`, and that entry does not import `ws`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export { WebSocketTransport } from "./adapters/websocket/client/node.js";
|
|
11
|
+
export type { WebSocketTransportOptions, WebSocketClass } from "./adapters/websocket/client/types.js";
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
deleteBatchInputSchema,
|
|
34
34
|
deleteBatchOutputSchema,
|
|
35
35
|
type GetInput,
|
|
36
|
+
type GetFoundOutput,
|
|
36
37
|
type SetInput,
|
|
37
38
|
type DeleteInput,
|
|
38
39
|
type HasInput,
|
|
@@ -107,7 +108,11 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
|
|
|
107
108
|
},
|
|
108
109
|
handler: async (input: GetInput, ctx : ProcedureContext) => {
|
|
109
110
|
const storage = getStorage(ctx);
|
|
110
|
-
|
|
111
|
+
const value = await storage.get(input.id);
|
|
112
|
+
if (input.found !== true) return value;
|
|
113
|
+
// The output with `found` keeps a stored null apart from a missing id, in one call
|
|
114
|
+
const output: GetFoundOutput = value === undefined ? { found: false } : { found: true, value };
|
|
115
|
+
return output;
|
|
111
116
|
},
|
|
112
117
|
}),
|
|
113
118
|
|
|
@@ -117,6 +117,12 @@ export const valueSchema : ZodLike<unknown> = createSchema<unknown>((data) => {
|
|
|
117
117
|
*/
|
|
118
118
|
export interface GetInput {
|
|
119
119
|
id: string;
|
|
120
|
+
/**
|
|
121
|
+
* True: the output is `{ found, value }` (a `GetFoundOutput`), so a stored `null` and a missing
|
|
122
|
+
* id give different outputs. Without it, the output is the item, or `null` on the wire for a
|
|
123
|
+
* missing id. `ApiStorage` of `@procedures/store` sends it.
|
|
124
|
+
*/
|
|
125
|
+
found?: boolean;
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
export const getInputSchema : ZodLike<GetInput> = createSchema<GetInput>((data) => {
|
|
@@ -138,11 +144,31 @@ export const getInputSchema : ZodLike<GetInput> = createSchema<GetInput>((data)
|
|
|
138
144
|
},
|
|
139
145
|
};
|
|
140
146
|
}
|
|
141
|
-
|
|
147
|
+
const found = data["found"];
|
|
148
|
+
if (found !== undefined && typeof found !== "boolean") {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: {
|
|
152
|
+
message: "found must be a boolean",
|
|
153
|
+
errors: [{ path: ["found"], message: "found must be a boolean" }],
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return { success: true, data: found === true ? { id: data["id"], found: true } : { id: data["id"] } };
|
|
142
158
|
});
|
|
143
159
|
|
|
144
160
|
/**
|
|
145
|
-
*
|
|
161
|
+
* The output of `get` with `found: true` in the input. `found` is true when the collection has
|
|
162
|
+
* the id. Then `value` is the item, also when the item is `null`.
|
|
163
|
+
*/
|
|
164
|
+
export interface GetFoundOutput {
|
|
165
|
+
found: boolean;
|
|
166
|
+
value?: unknown;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Output for get operation: the item, or undefined for a missing id. With `found: true` in the
|
|
171
|
+
* input, the output is a `GetFoundOutput`.
|
|
146
172
|
*/
|
|
147
173
|
export const getOutputSchema : ZodLike<unknown> = createSchema<unknown>((data) => {
|
|
148
174
|
// Accept any value (item) or undefined
|
|
@@ -9,9 +9,16 @@
|
|
|
9
9
|
* the collections package (roadmap 3.3).
|
|
10
10
|
*
|
|
11
11
|
* Each method gives a Promise: a caller cannot tell a local storage from a remote one.
|
|
12
|
+
*
|
|
13
|
+
* The contract (the suite of `@procedures/collections/testing` tests it for each storage):
|
|
14
|
+
* - A storage keeps a copy of each value that a write takes, and each read gives a new copy.
|
|
15
|
+
* - A stored `null` reads back as `null`. A missing id reads back as `undefined`.
|
|
16
|
+
* - A delta of `getChangesSince` follows the order of the last change of each id.
|
|
17
|
+
* - After `close()`, each operation rejects with an error whose message says "closed". A second
|
|
18
|
+
* `close()` does nothing.
|
|
12
19
|
*/
|
|
13
20
|
export interface CollectionStorage<T> {
|
|
14
|
-
/** The item with this id, or undefined. */
|
|
21
|
+
/** The item with this id, or undefined. A stored null is null. */
|
|
15
22
|
get(id: string): Promise<T | undefined>;
|
|
16
23
|
/** Every item of the collection. */
|
|
17
24
|
getAll(): Promise<T[]>;
|
|
@@ -50,7 +57,10 @@ export interface CollectionStorage<T> {
|
|
|
50
57
|
* that keeps no writes does not have this method. `ProcedureServer.useStorage` calls it.
|
|
51
58
|
*/
|
|
52
59
|
flush?(): Promise<void>;
|
|
53
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* This method releases the resources of the storage. After it, each operation rejects. A second
|
|
62
|
+
* `close()` does nothing.
|
|
63
|
+
*/
|
|
54
64
|
close(): Promise<void>;
|
|
55
65
|
/** The kind, the size and the statistics of the storage. */
|
|
56
66
|
getMetadata?(): Promise<StorageMetadata>;
|
|
@@ -65,6 +75,8 @@ export interface CollectionStorage<T> {
|
|
|
65
75
|
* the first call, a version of an older run, or a change log that dropped old deletes.
|
|
66
76
|
* - When `full` is false, `changed` has the items that changed after the version, and `deleted`
|
|
67
77
|
* has the ids that were deleted after it.
|
|
78
|
+
* - The order of a full result is the order of the entries. The order of a delta is the order of
|
|
79
|
+
* the last change of each id.
|
|
68
80
|
*/
|
|
69
81
|
export interface StorageChanges<T> {
|
|
70
82
|
/** The version of the storage that includes these changes. */
|
|
@@ -10,6 +10,7 @@ import { anyOutput, arrayOutput, booleanOutput, numberOutput } from "./outputs.j
|
|
|
10
10
|
import { defineProcedure, namespace } from "../define.js";
|
|
11
11
|
import type { AnyProcedure, Procedure } from "../types.js";
|
|
12
12
|
import { PURE_EFFECTS, withEffects } from "../effects.js";
|
|
13
|
+
import { isPlainObject } from "../ref.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* This function gives the own field `key` of an item, or undefined. A field of the prototype
|
|
@@ -301,10 +302,47 @@ interface UniqueInput {
|
|
|
301
302
|
}
|
|
302
303
|
|
|
303
304
|
const UniqueInputSchema = z.object({
|
|
304
|
-
items: z.array(z.unknown()).describe("The array"),
|
|
305
|
-
key: z.string().optional().describe("The key (a field name of the items)"),
|
|
305
|
+
items: z.array(z.unknown()).describe("The array. Equal JSON values are duplicates: objects and arrays compare by value."),
|
|
306
|
+
key: z.string().optional().describe("The key (a field name of the items). The values of the field compare by value."),
|
|
306
307
|
});
|
|
307
308
|
|
|
309
|
+
/**
|
|
310
|
+
* This function gives a function that makes a text key for a value: two equal JSON values get
|
|
311
|
+
* the same key. An array compares by its items in order, and a plain object by its own fields in
|
|
312
|
+
* any order. A number and its string get different keys. Another object (a `Date`, a `Map`) and
|
|
313
|
+
* a symbol compare by identity.
|
|
314
|
+
*/
|
|
315
|
+
function valueKeys(): (value: unknown) => string {
|
|
316
|
+
const identities = new Map<unknown, number>();
|
|
317
|
+
const identity = (value: unknown): string => {
|
|
318
|
+
let id = identities.get(value);
|
|
319
|
+
if (id === undefined) {
|
|
320
|
+
id = identities.size;
|
|
321
|
+
identities.set(value, id);
|
|
322
|
+
}
|
|
323
|
+
return `#${id}`;
|
|
324
|
+
};
|
|
325
|
+
const keyOf = (value: unknown, parents: Set<unknown>): string => {
|
|
326
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
327
|
+
// -0 and 0 are one value, and NaN is one value, as in a Set
|
|
328
|
+
if (typeof value === "number") return Object.is(value, -0) ? "0" : String(value);
|
|
329
|
+
if (typeof value === "bigint") return `${value}n`;
|
|
330
|
+
if (value === null || value === undefined || typeof value === "boolean") return String(value);
|
|
331
|
+
// A cycle is not JSON: its object compares by identity
|
|
332
|
+
if (parents.has(value) || (!Array.isArray(value) && !isPlainObject(value))) return identity(value);
|
|
333
|
+
parents.add(value);
|
|
334
|
+
const text = Array.isArray(value)
|
|
335
|
+
? `[${value.map((item) => keyOf(item, parents)).join(",")}]`
|
|
336
|
+
: `{${Object.keys(value)
|
|
337
|
+
.sort()
|
|
338
|
+
.map((field) => `${JSON.stringify(field)}:${keyOf(value[field], parents)}`)
|
|
339
|
+
.join(",")}}`;
|
|
340
|
+
parents.delete(value);
|
|
341
|
+
return text;
|
|
342
|
+
};
|
|
343
|
+
return (value) => keyOf(value, new Set());
|
|
344
|
+
}
|
|
345
|
+
|
|
308
346
|
type UniqueProcedure = Procedure<UniqueInput, unknown[], { description: string; tags: string[] }>;
|
|
309
347
|
|
|
310
348
|
const uniqueProcedure: UniqueProcedure = defineProcedure({
|
|
@@ -312,20 +350,19 @@ const uniqueProcedure: UniqueProcedure = defineProcedure({
|
|
|
312
350
|
input: UniqueInputSchema,
|
|
313
351
|
output: arrayOutput("The items without duplicates"),
|
|
314
352
|
metadata: {
|
|
315
|
-
description: "Remove duplicate values (optionally by key)",
|
|
353
|
+
description: "Remove duplicate values (optionally by key). It compares by value and keeps the first item of each value.",
|
|
316
354
|
tags: ["core", "array"],
|
|
317
355
|
},
|
|
318
356
|
handler: async (input: UniqueInput): Promise<unknown[]> => {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
return [...new Set(input.items)];
|
|
357
|
+
// Before, objects compared by reference, so two equal objects both stayed (catalog test, R29)
|
|
358
|
+
const keyOf = valueKeys();
|
|
359
|
+
const seen = new Set<string>();
|
|
360
|
+
return input.items.filter((item) => {
|
|
361
|
+
const key = keyOf(input.key ? fieldOf(item, input.key) : item);
|
|
362
|
+
if (seen.has(key)) return false;
|
|
363
|
+
seen.add(key);
|
|
364
|
+
return true;
|
|
365
|
+
});
|
|
329
366
|
},
|
|
330
367
|
});
|
|
331
368
|
|
|
@@ -623,8 +660,8 @@ interface FilterInput {
|
|
|
623
660
|
value?: unknown | undefined;
|
|
624
661
|
/** Values to match (any of these) */
|
|
625
662
|
values?: unknown[] | undefined;
|
|
626
|
-
/** Match mode: 'equals', 'not', 'in', 'notIn', 'contains', 'startsWith', 'endsWith' */
|
|
627
|
-
match?:
|
|
663
|
+
/** Match mode: 'equals', 'not', 'in', 'notIn', 'contains', 'startsWith', 'endsWith', 'gt', 'gte', 'lt', 'lte' */
|
|
664
|
+
match?:'equals' | 'not' | 'in' | 'notIn' | 'contains' | 'startsWith' | 'endsWith' | 'gt' | 'gte' | 'lt' | 'lte' | undefined;
|
|
628
665
|
/** Invert the filter */
|
|
629
666
|
invert?: boolean | undefined;
|
|
630
667
|
}
|
|
@@ -634,7 +671,7 @@ const FilterInputSchema = z.object({
|
|
|
634
671
|
key: z.string().optional().describe("Property to match against"),
|
|
635
672
|
value: z.unknown().optional().describe("Value to match (equality check)"),
|
|
636
673
|
values: z.array(z.unknown()).optional().describe("Values to match (any of these)"),
|
|
637
|
-
match: z.enum(["equals", "not", "in", "notIn", "contains", "startsWith", "endsWith", "gt", "gte", "lt", "lte"]).optional().describe("Match mode: 'equals'
|
|
674
|
+
match: z.enum(["equals", "not", "in", "notIn", "contains", "startsWith", "endsWith", "gt", "gte", "lt", "lte"]).optional().describe("Match mode (default 'equals'): 'equals' and 'not' compare with value, 'in' and 'notIn' with values. 'contains', 'startsWith' and 'endsWith' compare strings. 'gt', 'gte', 'lt' and 'lte' compare numbers."),
|
|
638
675
|
invert: z.boolean().optional().describe("Invert the filter"),
|
|
639
676
|
});
|
|
640
677
|
|
|
@@ -84,9 +84,16 @@ const ConditionalInputSchema = z.looseObject({
|
|
|
84
84
|
const LogicInputSchema = z.looseObject({
|
|
85
85
|
values: z.array(z.unknown()).describe("The operands, in order: values or calls. The evaluation stops at the deciding value."),
|
|
86
86
|
});
|
|
87
|
-
|
|
87
|
+
// not, all, any and none take values (roadmap R29, the owner's choice). They do not run a call in
|
|
88
|
+
// their operands: a call is an object, thus truthy. In a program, the calls of their input run
|
|
89
|
+
// first, as for each procedure that is not control flow. client.and and client.or run calls.
|
|
90
|
+
const NotInputSchema = z.looseObject({
|
|
91
|
+
value: operand("The value to negate. This procedure does not run a call: a call is an object, thus truthy."),
|
|
92
|
+
});
|
|
88
93
|
const VariadicBoolInputSchema = z.looseObject({
|
|
89
|
-
values: z
|
|
94
|
+
values: z
|
|
95
|
+
.array(z.unknown())
|
|
96
|
+
.describe("The values to evaluate. This procedure does not run calls: a call is an object, thus truthy. client.and and client.or run calls."),
|
|
90
97
|
});
|
|
91
98
|
const MapInputSchema = z.looseObject({
|
|
92
99
|
items: operand("The array, or a call that gives it"),
|
|
@@ -184,7 +184,7 @@ export const orMetadata: LogicMetadata = {
|
|
|
184
184
|
* Metadata for the NOT operator.
|
|
185
185
|
*/
|
|
186
186
|
export const notMetadata: LogicMetadata = {
|
|
187
|
-
description: "Logical NOT",
|
|
187
|
+
description: "Logical NOT of a value",
|
|
188
188
|
tags: ["core", "logic"],
|
|
189
189
|
arity: "unary",
|
|
190
190
|
commutative: false, // n/a for unary
|
|
@@ -151,10 +151,14 @@ const modProcedure: ModProcedure = defineProcedure({
|
|
|
151
151
|
input: ModInputSchema,
|
|
152
152
|
output: numberOutput("The remainder of a divided by b"),
|
|
153
153
|
metadata: {
|
|
154
|
-
description: "Modulo (remainder of a / b)",
|
|
154
|
+
description: "Modulo (remainder of a / b). It refuses b = 0, as client.divide does.",
|
|
155
155
|
tags: ["core", "math"],
|
|
156
156
|
},
|
|
157
157
|
handler: async (input: ModInput): Promise<number> => {
|
|
158
|
+
// Before, b = 0 gave NaN, and client.divide refused it (catalog test, roadmap R29)
|
|
159
|
+
if (input.b === 0) {
|
|
160
|
+
throw new Error("Division by zero");
|
|
161
|
+
}
|
|
158
162
|
return input.a % input.b;
|
|
159
163
|
},
|
|
160
164
|
});
|