@procedures/client 0.8.0 → 0.9.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 +78 -0
- package/README.md +95 -8
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts +4 -0
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +11 -1
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/client/types.d.ts +19 -0
- package/dist/adapters/http/client/types.d.ts.map +1 -1
- package/dist/adapters/http/server/cors.d.ts +64 -0
- package/dist/adapters/http/server/cors.d.ts.map +1 -0
- package/dist/adapters/http/server/cors.js +113 -0
- package/dist/adapters/http/server/cors.js.map +1 -0
- package/dist/adapters/http/server/handler.d.ts +136 -0
- package/dist/adapters/http/server/handler.d.ts.map +1 -0
- package/dist/adapters/http/server/handler.js +495 -0
- package/dist/adapters/http/server/handler.js.map +1 -0
- package/dist/adapters/http/server/strategies.d.ts +14 -5
- package/dist/adapters/http/server/strategies.d.ts.map +1 -1
- package/dist/adapters/http/server/strategies.js.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +30 -46
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +48 -432
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/server/types.d.ts +13 -0
- package/dist/adapters/http/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +4 -12
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +46 -1
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +152 -15
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +10 -3
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
- package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade-router.js +62 -0
- package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
- package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
- package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
- package/dist/adapters/websocket/server/upgrade.js +49 -0
- package/dist/adapters/websocket/server/upgrade.js.map +1 -0
- package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.js +8 -2
- package/dist/adapters/websocket/shared/rpc.js.map +1 -1
- package/dist/browser.d.ts +12 -4
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +13 -2
- package/dist/browser.js.map +1 -1
- package/dist/client/client.d.ts +43 -0
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +69 -1
- package/dist/client/client.js.map +1 -1
- package/dist/events/publisher.d.ts +25 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +66 -9
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/rows.d.ts +31 -0
- package/dist/events/rows.d.ts.map +1 -0
- package/dist/events/rows.js +39 -0
- package/dist/events/rows.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +14 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +9 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/capability.d.ts +6 -0
- package/dist/procedures/capability.d.ts.map +1 -1
- package/dist/procedures/capability.js +25 -0
- package/dist/procedures/capability.js.map +1 -1
- package/dist/procedures/collection/changes.d.ts +94 -0
- package/dist/procedures/collection/changes.d.ts.map +1 -0
- package/dist/procedures/collection/changes.js +182 -0
- package/dist/procedures/collection/changes.js.map +1 -0
- package/dist/procedures/collection/index.d.ts +10 -3
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +8 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/partition.d.ts +67 -0
- package/dist/procedures/collection/partition.d.ts.map +1 -0
- package/dist/procedures/collection/partition.js +82 -0
- package/dist/procedures/collection/partition.js.map +1 -0
- package/dist/procedures/collection/procedures.d.ts +39 -2
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +147 -44
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/query.d.ts +246 -0
- package/dist/procedures/collection/query.d.ts.map +1 -0
- package/dist/procedures/collection/query.js +854 -0
- package/dist/procedures/collection/query.js.map +1 -0
- package/dist/procedures/collection/schemas.d.ts +50 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +144 -6
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +58 -6
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/collection/storage.js +23 -0
- package/dist/procedures/collection/storage.js.map +1 -1
- package/dist/procedures/elicit.d.ts +190 -0
- package/dist/procedures/elicit.d.ts.map +1 -0
- package/dist/procedures/elicit.js +127 -0
- package/dist/procedures/elicit.js.map +1 -0
- package/dist/procedures/hints.d.ts +43 -0
- package/dist/procedures/hints.d.ts.map +1 -0
- package/dist/procedures/hints.js +74 -0
- package/dist/procedures/hints.js.map +1 -0
- package/dist/procedures/index.d.ts +9 -2
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +5 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts +24 -2
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +14 -1
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/progress.d.ts +24 -0
- package/dist/procedures/progress.d.ts.map +1 -0
- package/dist/procedures/progress.js +11 -0
- package/dist/procedures/progress.js.map +1 -0
- package/dist/procedures/registry.d.ts.map +1 -1
- package/dist/procedures/registry.js +5 -0
- package/dist/procedures/registry.js.map +1 -1
- package/dist/procedures/types.d.ts +55 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/procedures/untrusted.d.ts +28 -0
- package/dist/procedures/untrusted.d.ts.map +1 -0
- package/dist/procedures/untrusted.js +32 -0
- package/dist/procedures/untrusted.js.map +1 -0
- package/dist/server/browser-guard.d.ts +12 -2
- package/dist/server/browser-guard.d.ts.map +1 -1
- package/dist/server/browser-guard.js +43 -6
- package/dist/server/browser-guard.js.map +1 -1
- package/dist/server/host-policy.d.ts +10 -1
- 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/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +68 -7
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +124 -24
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +51 -4
- package/dist/server/request-auth.d.ts.map +1 -1
- package/dist/server/request-auth.js +116 -5
- package/dist/server/request-auth.js.map +1 -1
- package/dist/server/types.d.ts +73 -5
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/package.json +7 -3
- package/src/adapters/http/client/transport.ts +11 -1
- package/src/adapters/http/client/types.ts +22 -0
- package/src/adapters/http/server/cors.ts +144 -0
- package/src/adapters/http/server/handler.ts +624 -0
- package/src/adapters/http/server/strategies.ts +16 -6
- package/src/adapters/http/server/transport.ts +55 -478
- package/src/adapters/http/server/types.ts +15 -0
- package/src/adapters/websocket/client/transport.ts +4 -12
- package/src/adapters/websocket/server/transport.ts +166 -16
- package/src/adapters/websocket/server/types.ts +11 -3
- package/src/adapters/websocket/server/upgrade-router.ts +77 -0
- package/src/adapters/websocket/server/upgrade.ts +88 -0
- package/src/adapters/websocket/shared/rpc.ts +9 -2
- package/src/browser.ts +85 -2
- package/src/client/client.ts +83 -1
- package/src/events/publisher.ts +75 -11
- package/src/events/rows.ts +48 -0
- package/src/index.ts +1 -1
- package/src/node.ts +21 -0
- package/src/procedures/capability.ts +21 -0
- package/src/procedures/collection/changes.ts +245 -0
- package/src/procedures/collection/index.ts +57 -1
- package/src/procedures/collection/partition.ts +123 -0
- package/src/procedures/collection/procedures.ts +206 -51
- package/src/procedures/collection/query.ts +1009 -0
- package/src/procedures/collection/schemas.ts +196 -7
- package/src/procedures/collection/storage.ts +78 -6
- package/src/procedures/elicit.ts +320 -0
- package/src/procedures/hints.ts +91 -0
- package/src/procedures/index.ts +83 -2
- package/src/procedures/invoke.ts +33 -2
- package/src/procedures/progress.ts +25 -0
- package/src/procedures/registry.ts +4 -0
- package/src/procedures/types.ts +65 -0
- package/src/procedures/untrusted.ts +50 -0
- package/src/server/browser-guard.ts +48 -6
- package/src/server/host-policy.ts +13 -1
- package/src/server/index.ts +3 -0
- package/src/server/procedure-server.ts +147 -26
- package/src/server/request-auth.ts +128 -6
- package/src/server/types.ts +82 -5
package/src/client/client.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
Method,
|
|
31
31
|
Metadata,
|
|
32
32
|
Message,
|
|
33
|
+
ResponseItem,
|
|
33
34
|
ClientMiddleware,
|
|
34
35
|
ClientContext,
|
|
35
36
|
ClientRunner,
|
|
@@ -56,8 +57,19 @@ import { RouteResolver, type ResolvedRoute, type RouteResolutionResult } from ".
|
|
|
56
57
|
import { BatchExecutor, type ExecutionContext } from "./batch-executor.js";
|
|
57
58
|
import type { ProcedureRegistry } from "../procedures/registry.js";
|
|
58
59
|
import type { EventBus } from "../events/types.js";
|
|
60
|
+
import {
|
|
61
|
+
remoteEvents,
|
|
62
|
+
subscribeRemote,
|
|
63
|
+
type EventTransport,
|
|
64
|
+
type RemoteEvent,
|
|
65
|
+
type RemoteEventHandler,
|
|
66
|
+
type RemoteEventsOptions,
|
|
67
|
+
type SubscribeOptions,
|
|
68
|
+
type Subscription,
|
|
69
|
+
} from "../events/remote.js";
|
|
59
70
|
import { narrowCapability } from "../procedures/capability.js";
|
|
60
|
-
import { TIMEOUT_METADATA_KEY, absorbTimeout, writeRemainingTime } from "./deadline.js";
|
|
71
|
+
import { DEADLINE_METADATA_KEY, TIMEOUT_METADATA_KEY, absorbTimeout, writeRemainingTime } from "./deadline.js";
|
|
72
|
+
import { ms } from "../duration.js";
|
|
61
73
|
import type { Capability, ClearanceOrder, NestedCallOptions } from "../procedures/types.js";
|
|
62
74
|
import { PROCEDURE_REGISTRY } from "../procedures/registry.js";
|
|
63
75
|
import { validateSchema } from "../procedures/schema.js";
|
|
@@ -169,6 +181,12 @@ function callerOf(options: ExecOptions): CallerContext {
|
|
|
169
181
|
return caller;
|
|
170
182
|
}
|
|
171
183
|
|
|
184
|
+
/**
|
|
185
|
+
* The timeouts of a subscription: none. The timeout middleware takes 0 as "no timeout", so the
|
|
186
|
+
* timeouts of the client do not end a subscription, which has no end (roadmap CLI-15).
|
|
187
|
+
*/
|
|
188
|
+
const NO_SUBSCRIPTION_TIMEOUT = Object.freeze({ overall: ms(0), perAttempt: ms(0) });
|
|
189
|
+
|
|
172
190
|
/**
|
|
173
191
|
* Generate a unique message ID.
|
|
174
192
|
*/
|
|
@@ -221,6 +239,8 @@ export class Client<TContext = {}> {
|
|
|
221
239
|
private readonly throwOnError: boolean;
|
|
222
240
|
/** The bus of the local procedures. Undefined: the global bus. */
|
|
223
241
|
private bus: EventBus | undefined;
|
|
242
|
+
/** The transport of the subscriptions through the middleware, made at the first subscription */
|
|
243
|
+
private subscriptionTransport: EventTransport | undefined;
|
|
224
244
|
/** The interceptors of the local invocations (roadmap 1.2) */
|
|
225
245
|
private interceptors: readonly Interceptor[] = [];
|
|
226
246
|
/** The capability of the local invocations (CAPABILITIES-2026-10). Undefined: no limits. */
|
|
@@ -779,6 +799,68 @@ export class Client<TContext = {}> {
|
|
|
779
799
|
}
|
|
780
800
|
}
|
|
781
801
|
|
|
802
|
+
/**
|
|
803
|
+
* This method subscribes to topics of the event bus of the server (`documentation/EVENTS.md`).
|
|
804
|
+
* The call of `events.subscribe` goes through the middleware and the context of this client, as
|
|
805
|
+
* `stream()` does. Thus a token of the auth middleware goes with it. The subscriber resubscribes
|
|
806
|
+
* after a failure that a retry can repair, and the subscribers of the same topics share one call.
|
|
807
|
+
*
|
|
808
|
+
* A subscription has no deadline: the timeouts of the client (its timeout middleware, the
|
|
809
|
+
* `timeout` of its context and a `procedure-timeout` of its metadata) do not apply to it. For a
|
|
810
|
+
* deadline, give it in the metadata of the options, for example
|
|
811
|
+
* `{ metadata: { timeout: { overall: minutes(5) } } }`.
|
|
812
|
+
*
|
|
813
|
+
* @param topics - One topic, or a list of 1 to 64 topics
|
|
814
|
+
* @param handler - The handler of each event. A promise that it gives holds the next event.
|
|
815
|
+
* @param options - `onSubscribed`, `onError`, a signal, the metadata of the call, the resubscribe rules
|
|
816
|
+
* @returns The unsubscribe function, with the `ready` promise of the first confirmation
|
|
817
|
+
*
|
|
818
|
+
* @example
|
|
819
|
+
* ```typescript
|
|
820
|
+
* const unsubscribe = client.subscribe("collections.users", (event) => console.log(event));
|
|
821
|
+
* await unsubscribe.ready;
|
|
822
|
+
* unsubscribe();
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
subscribe<T = unknown>(topics: string | readonly string[], handler: RemoteEventHandler<T>, options: SubscribeOptions = {}): Subscription {
|
|
826
|
+
return subscribeRemote<T>(this.eventTransport(), topics, handler, options);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* This method gives the events of topics of the event bus of the server as an async iterator,
|
|
831
|
+
* through the middleware of this client. A `break` or the signal ends the subscription. As for
|
|
832
|
+
* `subscribe()`, the timeouts of the client do not apply: the metadata of the options can give one.
|
|
833
|
+
*
|
|
834
|
+
* @param topics - One topic, or a list of 1 to 64 topics
|
|
835
|
+
* @param options - A signal, `onSubscribed`, the metadata of the call, the resubscribe rules
|
|
836
|
+
*/
|
|
837
|
+
events<T = unknown>(topics: string | readonly string[], options: RemoteEventsOptions = {}): AsyncIterable<RemoteEvent<T>> {
|
|
838
|
+
return remoteEvents<T>(this.eventTransport(), topics, options);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* The transport of the subscriptions of this client: each call goes through the middleware, with
|
|
843
|
+
* the context of the client in its metadata. The subscriptions of one client share it, so they
|
|
844
|
+
* share their calls. The timeouts and the deadline of the client stay out: only the metadata of
|
|
845
|
+
* the subscription can give them.
|
|
846
|
+
*/
|
|
847
|
+
private eventTransport(): EventTransport {
|
|
848
|
+
if (!this.subscriptionTransport) {
|
|
849
|
+
const send = <TReq, TRes>(message: Message<TReq>): AsyncIterable<ResponseItem<TRes>> => {
|
|
850
|
+
const metadata: Metadata = { ...this.defaultMetadata, ...(this.getEffectiveContext() as object) };
|
|
851
|
+
delete metadata["timeout"];
|
|
852
|
+
delete metadata[TIMEOUT_METADATA_KEY];
|
|
853
|
+
delete metadata[DEADLINE_METADATA_KEY];
|
|
854
|
+
metadata["timeout"] = NO_SUBSCRIPTION_TIMEOUT as unknown as NonNullable<Metadata["timeout"]>;
|
|
855
|
+
Object.assign(metadata, message.metadata);
|
|
856
|
+
absorbTimeout(metadata);
|
|
857
|
+
return this.composeMiddleware<TReq, TRes>()({ message: { ...message, metadata } });
|
|
858
|
+
};
|
|
859
|
+
this.subscriptionTransport = { send };
|
|
860
|
+
}
|
|
861
|
+
return this.subscriptionTransport;
|
|
862
|
+
}
|
|
863
|
+
|
|
782
864
|
/**
|
|
783
865
|
* Compose middleware chain using universal middleware composition.
|
|
784
866
|
*
|
package/src/events/publisher.ts
CHANGED
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
*
|
|
9
9
|
* - The topic rule (`topics`) is the expose rule of the topics. A topic outside it gives
|
|
10
10
|
* `TOPIC_NOT_FOUND`.
|
|
11
|
+
* - `reads` gives the resources that a subscriber of a topic reads. The capability of the call
|
|
12
|
+
* must permit them, as for a procedure that reads them (roadmap CLI-15). By default, a topic
|
|
13
|
+
* `collections.<name>` reads `collection:<name>`.
|
|
11
14
|
* - `allow` examines each topic with the capability of the call. A refusal gives `FORBIDDEN` with
|
|
12
15
|
* the reason `topic-not-allowed`.
|
|
13
16
|
* - The procedure declares that it reads the resource `events`, so the capability of the host
|
|
14
17
|
* applies to it as to each procedure.
|
|
15
18
|
*
|
|
19
|
+
* - A value with rows (`withRows()`) reaches each subscriber through the output filter of its host
|
|
20
|
+
* (`ctx.admit`): each row passes the filter, and the subscriber gets only the admitted rows.
|
|
21
|
+
*
|
|
16
22
|
* The delivery is at most once. The queue of a subscription holds at most `buffer` events. When
|
|
17
23
|
* it is full, the publisher drops the oldest event, and the next event tells the number of lost
|
|
18
24
|
* events (`dropped`). The bus never waits for a subscriber.
|
|
@@ -21,10 +27,15 @@
|
|
|
21
27
|
import { z } from "zod";
|
|
22
28
|
import { createProcedure } from "../procedures/define.js";
|
|
23
29
|
import { InvocationError } from "../procedures/invoke.js";
|
|
30
|
+
import { capabilityRefusal } from "../procedures/capability.js";
|
|
31
|
+
import { collectionChangeTopic, collectionTopicReads } from "../procedures/collection/changes.js";
|
|
32
|
+
import { collectionTopicPartition, partitionOfCall } from "../procedures/collection/partition.js";
|
|
24
33
|
import type { AnyProcedure, Capability, ProcedureContext } from "../procedures/types.js";
|
|
34
|
+
import type { Principal } from "../server/types.js";
|
|
25
35
|
import type { EventBus, Unsubscribe } from "./types.js";
|
|
26
36
|
import { getGlobalEventBus } from "./bus.js";
|
|
27
37
|
import { EVENTS_SUBSCRIBE_PATH, type EventItem, type SubscribeInput } from "./remote.js";
|
|
38
|
+
import { admittedValue, rowsOf } from "./rows.js";
|
|
28
39
|
|
|
29
40
|
/** The number of topics that one subscription can name. */
|
|
30
41
|
export const MAX_SUBSCRIPTION_TOPICS = 64;
|
|
@@ -44,6 +55,8 @@ export interface TopicCall {
|
|
|
44
55
|
readonly capability: Capability;
|
|
45
56
|
/** The metadata of the call: the connection id and the headers of a network request. */
|
|
46
57
|
readonly metadata: Readonly<Record<string, unknown>>;
|
|
58
|
+
/** The principal of the call, from the authentication of the host. */
|
|
59
|
+
readonly principal?: Principal | undefined;
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
/** The options of an `EventPublisher`. */
|
|
@@ -64,6 +77,18 @@ export interface EventPublisherOptions {
|
|
|
64
77
|
* @default 1024
|
|
65
78
|
*/
|
|
66
79
|
buffer?: number | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* The resources that a subscriber of a topic reads (roadmap CLI-15). The publisher refuses a
|
|
82
|
+
* topic whose reads the capability of the call does not permit, as for a procedure that reads
|
|
83
|
+
* them: `FORBIDDEN` with the reason `effects-exceeded`.
|
|
84
|
+
* @default `collectionTopicReads`: a topic `collections.<name>` reads `collection:<name>`
|
|
85
|
+
*/
|
|
86
|
+
reads?: ((topic: string) => readonly string[]) | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A procedure that reads resources, for the admission of a topic under a capability. */
|
|
90
|
+
function topicReader(reads: readonly string[]): AnyProcedure {
|
|
91
|
+
return { path: [...EVENTS_SUBSCRIBE_PATH], metadata: { effects: { reads: [...reads], writes: [] } } } as unknown as AnyProcedure;
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
/** This function gives the test of a topic rule. */
|
|
@@ -131,22 +156,27 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
131
156
|
private readonly queue: Array<Extract<EventItem, { type: "event" }>> = [];
|
|
132
157
|
private seq = 0;
|
|
133
158
|
private dropped = 0;
|
|
134
|
-
private waiter:
|
|
159
|
+
private waiter:
|
|
160
|
+
| { resolve: (result: IteratorResult<EventItem> | PromiseLike<IteratorResult<EventItem>>) => void; reject: (error: unknown) => void }
|
|
161
|
+
| undefined;
|
|
135
162
|
private closed = false;
|
|
136
163
|
private failure: unknown;
|
|
137
164
|
private readonly unsubscribes: Unsubscribe[];
|
|
138
165
|
|
|
139
166
|
constructor(
|
|
140
167
|
bus: EventBus,
|
|
141
|
-
topics
|
|
168
|
+
/** The topics of the subscriber, each with the topic of the bus that it listens to */
|
|
169
|
+
routes: ReadonlyArray<readonly [topic: string, busTopic: string]>,
|
|
142
170
|
private readonly buffer: number,
|
|
143
171
|
private readonly signal: AbortSignal | undefined,
|
|
144
172
|
private readonly onClose: (subscription: TopicSubscription) => void,
|
|
173
|
+
/** The output filter of the host for this subscriber: each row of a value passes it */
|
|
174
|
+
private readonly admit: ((item: unknown) => Promise<boolean>) | undefined,
|
|
145
175
|
) {
|
|
146
|
-
this.confirmation = { type: "subscribed", topics };
|
|
176
|
+
this.confirmation = { type: "subscribed", topics: routes.map(([topic]) => topic) };
|
|
147
177
|
// Before, clear() removed the listeners, and the subscriber saw no end (EVENTS.md)
|
|
148
|
-
this.unsubscribes =
|
|
149
|
-
bus.on(
|
|
178
|
+
this.unsubscribes = routes.map(([topic, busTopic]) =>
|
|
179
|
+
bus.on(busTopic, (data) => this.push(topic, data), { onClear: () => this.fail(clearedError(topic), { keepQueue: true }) }),
|
|
150
180
|
);
|
|
151
181
|
signal?.addEventListener("abort", this.onAbort, { once: true });
|
|
152
182
|
}
|
|
@@ -166,7 +196,7 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
166
196
|
const waiter = this.waiter;
|
|
167
197
|
if (waiter && this.queue.length === 0) {
|
|
168
198
|
this.waiter = undefined;
|
|
169
|
-
waiter.resolve(
|
|
199
|
+
waiter.resolve(this.deliver(event));
|
|
170
200
|
return;
|
|
171
201
|
}
|
|
172
202
|
if (this.queue.length >= this.buffer) {
|
|
@@ -177,6 +207,16 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
177
207
|
this.queue.push(event);
|
|
178
208
|
}
|
|
179
209
|
|
|
210
|
+
/**
|
|
211
|
+
* The item of an event for the subscriber. The rows of its data pass the output filter of the
|
|
212
|
+
* host for this subscriber, and the subscriber gets only the admitted rows.
|
|
213
|
+
*/
|
|
214
|
+
private deliver(event: Extract<EventItem, { type: "event" }>): Promise<IteratorResult<EventItem>> {
|
|
215
|
+
const item = this.withDropped(event) as Extract<EventItem, { type: "event" }>;
|
|
216
|
+
if (!this.admit || !rowsOf(item.data)) return Promise.resolve({ value: item, done: false });
|
|
217
|
+
return admittedValue(item.data, this.admit).then((data): IteratorResult<EventItem> => ({ value: { ...item, data }, done: false }));
|
|
218
|
+
}
|
|
219
|
+
|
|
180
220
|
/** The event with the number of events that the publisher dropped before it. */
|
|
181
221
|
private withDropped(event: Extract<EventItem, { type: "event" }>): EventItem {
|
|
182
222
|
if (this.dropped === 0) return event;
|
|
@@ -192,7 +232,7 @@ class TopicSubscription implements AsyncIterableIterator<EventItem> {
|
|
|
192
232
|
return Promise.resolve({ value: confirmation, done: false });
|
|
193
233
|
}
|
|
194
234
|
const event = this.queue.shift();
|
|
195
|
-
if (event) return
|
|
235
|
+
if (event) return this.deliver(event);
|
|
196
236
|
if (this.failure !== undefined) {
|
|
197
237
|
const failure = this.failure;
|
|
198
238
|
this.failure = undefined;
|
|
@@ -268,6 +308,7 @@ export class EventPublisher {
|
|
|
268
308
|
readonly procedure: AnyProcedure;
|
|
269
309
|
private readonly known: (topic: string) => boolean;
|
|
270
310
|
private readonly allow: EventPublisherOptions["allow"];
|
|
311
|
+
private readonly reads: (topic: string) => readonly string[];
|
|
271
312
|
private readonly buffer: number;
|
|
272
313
|
private readonly open = new Set<TopicSubscription>();
|
|
273
314
|
|
|
@@ -275,6 +316,7 @@ export class EventPublisher {
|
|
|
275
316
|
this.bus = options.bus ?? getGlobalEventBus();
|
|
276
317
|
this.known = topicMatcher(options.topics);
|
|
277
318
|
this.allow = options.allow;
|
|
319
|
+
this.reads = options.reads ?? collectionTopicReads;
|
|
278
320
|
this.buffer = Math.max(1, Math.floor(options.buffer ?? 1024));
|
|
279
321
|
this.procedure = createProcedure()
|
|
280
322
|
.path([...EVENTS_SUBSCRIBE_PATH])
|
|
@@ -307,24 +349,46 @@ export class EventPublisher {
|
|
|
307
349
|
for (const subscription of [...this.open]) subscription.fail(stoppedError());
|
|
308
350
|
}
|
|
309
351
|
|
|
352
|
+
/**
|
|
353
|
+
* The topic of the bus for a topic of a subscriber (roadmap CLI-17). A topic of a partitioned
|
|
354
|
+
* collection names the partition that the subscriber asks for, as the input of a procedure does:
|
|
355
|
+
* the resolver of the repository gives the partition, so a subscriber cannot name the topic of
|
|
356
|
+
* another tenant. A collection without partitions refuses a topic with a partition.
|
|
357
|
+
*/
|
|
358
|
+
private async busTopic(topic: string, ctx: ProcedureContext): Promise<string> {
|
|
359
|
+
const parts = collectionTopicPartition(topic);
|
|
360
|
+
if (!parts) return topic;
|
|
361
|
+
const partition = await partitionOfCall(ctx, parts.collection, parts.partition);
|
|
362
|
+
return collectionChangeTopic(parts.collection, partition);
|
|
363
|
+
}
|
|
364
|
+
|
|
310
365
|
/**
|
|
311
366
|
* The stream of one subscription. The publisher examines each topic before it adds a listener:
|
|
312
|
-
* the topic rule
|
|
367
|
+
* the topic rule, the reads, `allow`, and the partition. A call with one bad topic makes no subscription.
|
|
313
368
|
*/
|
|
314
|
-
private subscribe(topics: readonly string[], ctx: ProcedureContext): TopicSubscription {
|
|
369
|
+
private async subscribe(topics: readonly string[], ctx: ProcedureContext): Promise<TopicSubscription> {
|
|
315
370
|
const unique = [...new Set(topics)];
|
|
316
371
|
for (const topic of unique) {
|
|
317
372
|
if (!this.known(topic)) throw new TopicNotFoundError(`The host has no topic "${topic}"`);
|
|
318
373
|
}
|
|
374
|
+
// A subscriber of a topic reads its resources: the capability of the call must permit them
|
|
375
|
+
for (const topic of unique) {
|
|
376
|
+
const reads = this.reads(topic);
|
|
377
|
+
if (reads.length === 0) continue;
|
|
378
|
+
const refusal = capabilityRefusal(topicReader(reads), ctx.capability);
|
|
379
|
+
if (refusal) throw new InvocationError("FORBIDDEN", `The topic "${topic}": ${refusal.message}`, ctx.path, refusal.reason);
|
|
380
|
+
}
|
|
319
381
|
if (this.allow) {
|
|
320
|
-
const call: TopicCall = { capability: ctx.capability, metadata: ctx.metadata };
|
|
382
|
+
const call: TopicCall = { capability: ctx.capability, metadata: ctx.metadata, ...(ctx.principal ? { principal: ctx.principal } : {}) };
|
|
321
383
|
for (const topic of unique) {
|
|
322
384
|
if (!this.allow(topic, call)) {
|
|
323
385
|
throw new InvocationError("FORBIDDEN", `The capability of the call does not permit the topic "${topic}"`, ctx.path, "topic-not-allowed");
|
|
324
386
|
}
|
|
325
387
|
}
|
|
326
388
|
}
|
|
327
|
-
const
|
|
389
|
+
const routes: Array<readonly [string, string]> = [];
|
|
390
|
+
for (const topic of unique) routes.push([topic, await this.busTopic(topic, ctx)]);
|
|
391
|
+
const subscription = new TopicSubscription(this.bus, routes, this.buffer, ctx.signal, (closed) => this.open.delete(closed), ctx.admit);
|
|
328
392
|
this.open.add(subscription);
|
|
329
393
|
return subscription.closeIfAborted();
|
|
330
394
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rows of a published value (roadmap CLI-15).
|
|
3
|
+
*
|
|
4
|
+
* A value on the bus can hold rows of data: the values of a collection change event, for example.
|
|
5
|
+
* The publisher of the remote events gives each row to the output filter of the host (`admitItem`)
|
|
6
|
+
* for each subscriber, and each subscriber gets only the rows that its filter admits. Thus a row
|
|
7
|
+
* reaches a subscriber only through the same filter as a read.
|
|
8
|
+
*
|
|
9
|
+
* The rows are on a symbol key, so they stay in this process: JSON does not take them.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** The rows of a published value, and the function that makes the value of the admitted rows. */
|
|
13
|
+
export interface PublishedRows {
|
|
14
|
+
/** The rows that the output filter examines, one at a time. */
|
|
15
|
+
readonly rows: readonly unknown[];
|
|
16
|
+
/** This function gives the value for a subscriber: `admitted[i]` tells if row `i` stays. */
|
|
17
|
+
keep(admitted: readonly boolean[]): unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The key of the rows of a published value. */
|
|
21
|
+
const ROWS: unique symbol = Symbol.for("procedures.publishedRows");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* This function gives the value with its rows, for the filter of each subscriber. The rows are not
|
|
25
|
+
* enumerable: a copy or a comparison of the value does not see them.
|
|
26
|
+
*/
|
|
27
|
+
export function withRows<T extends object>(value: T, rows: PublishedRows): T {
|
|
28
|
+
return Object.defineProperty(value, ROWS, { value: rows, enumerable: false });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The rows of a published value, or undefined for a value without rows. */
|
|
32
|
+
export function rowsOf(value: unknown): PublishedRows | undefined {
|
|
33
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
34
|
+
const rows = (value as { [ROWS]?: unknown })[ROWS];
|
|
35
|
+
return typeof rows === "object" && rows !== null ? (rows as PublishedRows) : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The value of a published value for one subscriber: each row passes the filter, and the value
|
|
40
|
+
* keeps the admitted rows. An error of the filter refuses the row. Without rows or a filter, the
|
|
41
|
+
* value stays as it is.
|
|
42
|
+
*/
|
|
43
|
+
export async function admittedValue(value: unknown, admit: ((item: unknown) => Promise<boolean>) | undefined): Promise<unknown> {
|
|
44
|
+
const rows = rowsOf(value);
|
|
45
|
+
if (!rows || !admit) return value;
|
|
46
|
+
const admitted = await Promise.all(rows.rows.map((row) => admit(row).catch(() => false)));
|
|
47
|
+
return rows.keep(admitted);
|
|
48
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type {
|
|
|
27
27
|
HttpUrlStrategy,
|
|
28
28
|
} from "./server/index.js";
|
|
29
29
|
export { WebSocketServerTransport } from "./server/index.js";
|
|
30
|
-
export { checkBrowserRequest, isLoopbackHost, isLoopbackAddress, hostNameOf } from "./server/browser-guard.js";
|
|
30
|
+
export { checkBrowserRequest, hasAmbientCredentials, isLoopbackHost, isLoopbackAddress, hostNameOf } from "./server/browser-guard.js";
|
|
31
31
|
export type { BrowserGuardOptions, GuardedRequest, GuardResult } from "./server/browser-guard.js";
|
|
32
32
|
export type {
|
|
33
33
|
WebSocketServerTransportOptions,
|
package/src/node.ts
CHANGED
|
@@ -9,9 +9,30 @@
|
|
|
9
9
|
* This entry also exports `writeNdjsonStream`: the writer of an NDJSON stream response of wire
|
|
10
10
|
* protocol v1, for a `ServerResponse` of `node:http`. `HttpServerTransport` uses it, so a server
|
|
11
11
|
* without Express writes the same bytes.
|
|
12
|
+
*
|
|
13
|
+
* The procedure faces of Node, without a framework (CLI-3): `procedureHandler(server, options)`
|
|
14
|
+
* gives a request handler `(req, res) => boolean`, and `upgradeHandler(server, options)` gives an
|
|
15
|
+
* upgrade handler `(req, socket, head) => boolean`. Each one takes the requests of its path and
|
|
16
|
+
* gives `false` for the others, so the routes of the application share the listener.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
export { WebSocketTransport } from "./adapters/websocket/client/node.js";
|
|
15
20
|
export type { WebSocketTransportOptions, WebSocketClass } from "./adapters/websocket/client/types.js";
|
|
16
21
|
export { writeNdjsonStream } from "./adapters/http/server/ndjson.js";
|
|
17
22
|
export type { WriteNdjsonStreamOptions, NdjsonError } from "./adapters/http/server/ndjson.js";
|
|
23
|
+
export { procedureHandler } from "./adapters/http/server/handler.js";
|
|
24
|
+
export type { ProcedureHandlerOptions, NodeProcedureHandler, NodeUrlStrategy, UrlRequest } from "./adapters/http/server/handler.js";
|
|
25
|
+
export type { CorsOptions } from "./adapters/http/server/cors.js";
|
|
26
|
+
export type { RoutedRequest } from "./adapters/http/server/strategies.js";
|
|
27
|
+
export { defaultServerUrlStrategy, rpcServerUrlStrategy } from "./adapters/http/server/strategies.js";
|
|
28
|
+
export { upgradeHandler } from "./adapters/websocket/server/upgrade.js";
|
|
29
|
+
export type { UpgradeHandlerOptions, NodeUpgradeHandler } from "./adapters/websocket/server/upgrade.js";
|
|
30
|
+
export { WebSocketServerTransport } from "./adapters/websocket/server/transport.js";
|
|
31
|
+
export type {
|
|
32
|
+
WebSocketServerTransportOptions,
|
|
33
|
+
WebSocketAuthHandler,
|
|
34
|
+
WebSocketConnectionHandler,
|
|
35
|
+
WebSocketMessage,
|
|
36
|
+
TrackedConnection,
|
|
37
|
+
ConnectionEventHandler,
|
|
38
|
+
} from "./adapters/websocket/server/types.js";
|
|
@@ -178,6 +178,27 @@ export function narrowCapability<C>(
|
|
|
178
178
|
return Object.freeze(result) as Capability<C>;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* This function joins two requests for a narrower capability into one request that asks for
|
|
183
|
+
* both: the meet of the clearances, the intersection of the effects and the shorter time. Two
|
|
184
|
+
* different clearances need an order.
|
|
185
|
+
*/
|
|
186
|
+
export function joinNarrowing<C>(first: CapabilityRequest<C>, second: CapabilityRequest<C>, order?: ClearanceOrder<C>): CapabilityRequest<C> {
|
|
187
|
+
const result: CapabilityRequest<C> = {};
|
|
188
|
+
if (first.clearance === undefined || second.clearance === undefined || Object.is(first.clearance, second.clearance)) {
|
|
189
|
+
const clearance = first.clearance ?? second.clearance;
|
|
190
|
+
if (clearance !== undefined) result.clearance = clearance;
|
|
191
|
+
} else {
|
|
192
|
+
if (!order) throw new Error("Two different clearances need a clearance order: give clearanceOrder to the host (hostPolicy)");
|
|
193
|
+
result.clearance = order.meet(first.clearance, second.clearance);
|
|
194
|
+
}
|
|
195
|
+
const effects = first.effects === undefined ? second.effects : narrowEffects(first.effects, second.effects);
|
|
196
|
+
if (effects !== undefined) result.effects = effects;
|
|
197
|
+
const times = [first.timeoutMs, second.timeoutMs].filter((time): time is number => typeof time === "number" && Number.isFinite(time) && time >= 0);
|
|
198
|
+
if (times.length > 0) result.timeoutMs = Math.min(...times);
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
181
202
|
// =============================================================================
|
|
182
203
|
// Admission
|
|
183
204
|
// =============================================================================
|