@ny4n/chord 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +261 -2
- package/dist/api.d.ts +20 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +77 -0
- package/dist/api.js.map +1 -0
- package/dist/bundler.d.ts +6 -0
- package/dist/bundler.d.ts.map +1 -0
- package/dist/bundler.js +3 -0
- package/dist/bundler.js.map +1 -0
- package/dist/context/index.d.ts +24 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +97 -0
- package/dist/context/index.js.map +1 -0
- package/dist/delta/apply-immutable-trusted.d.ts +4 -0
- package/dist/delta/apply-immutable-trusted.d.ts.map +1 -0
- package/dist/delta/apply-immutable-trusted.js +127 -0
- package/dist/delta/apply-immutable-trusted.js.map +1 -0
- package/dist/delta/core.d.ts +108 -0
- package/dist/delta/core.d.ts.map +1 -0
- package/dist/delta/core.js +645 -0
- package/dist/delta/core.js.map +1 -0
- package/dist/delta/diff.d.ts +5 -0
- package/dist/delta/diff.d.ts.map +1 -0
- package/dist/delta/diff.js +433 -0
- package/dist/delta/diff.js.map +1 -0
- package/dist/delta/draft.d.ts +5 -0
- package/dist/delta/draft.d.ts.map +1 -0
- package/dist/delta/draft.js +2 -0
- package/dist/delta/draft.js.map +1 -0
- package/dist/delta/index.d.ts +5 -0
- package/dist/delta/index.d.ts.map +1 -0
- package/dist/delta/index.js +6 -0
- package/dist/delta/index.js.map +1 -0
- package/dist/delta/revision-validator.d.ts +7 -0
- package/dist/delta/revision-validator.d.ts.map +1 -0
- package/dist/delta/revision-validator.js +71 -0
- package/dist/delta/revision-validator.js.map +1 -0
- package/dist/delta/tracker.d.ts +26 -0
- package/dist/delta/tracker.d.ts.map +1 -0
- package/dist/delta/tracker.js +2069 -0
- package/dist/delta/tracker.js.map +1 -0
- package/dist/facets/host.d.ts +12 -0
- package/dist/facets/host.d.ts.map +1 -0
- package/dist/facets/host.js +763 -0
- package/dist/facets/host.js.map +1 -0
- package/dist/facets/loader.d.ts +3 -0
- package/dist/facets/loader.d.ts.map +1 -0
- package/dist/facets/loader.js +5 -0
- package/dist/facets/loader.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +10 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +124 -0
- package/dist/json.js.map +1 -0
- package/dist/node/bundle-loader.d.ts +30 -0
- package/dist/node/bundle-loader.d.ts.map +1 -0
- package/dist/node/bundle-loader.js +377 -0
- package/dist/node/bundle-loader.js.map +1 -0
- package/dist/node/bundle.d.ts +26 -0
- package/dist/node/bundle.d.ts.map +1 -0
- package/dist/node/bundle.js +183 -0
- package/dist/node/bundle.js.map +1 -0
- package/dist/node/manifest.d.ts +36 -0
- package/dist/node/manifest.d.ts.map +1 -0
- package/dist/node/manifest.js +6 -0
- package/dist/node/manifest.js.map +1 -0
- package/dist/node/package.d.ts +15 -0
- package/dist/node/package.d.ts.map +1 -0
- package/dist/node/package.js +199 -0
- package/dist/node/package.js.map +1 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +3 -0
- package/dist/node.js.map +1 -0
- package/dist/services/consumer.d.ts +11 -0
- package/dist/services/consumer.d.ts.map +1 -0
- package/dist/services/consumer.js +585 -0
- package/dist/services/consumer.js.map +1 -0
- package/dist/services/errors.d.ts +8 -0
- package/dist/services/errors.d.ts.map +1 -0
- package/dist/services/errors.js +22 -0
- package/dist/services/errors.js.map +1 -0
- package/dist/services/handle.d.ts +15 -0
- package/dist/services/handle.d.ts.map +1 -0
- package/dist/services/handle.js +101 -0
- package/dist/services/handle.js.map +1 -0
- package/dist/services/instances.d.ts +26 -0
- package/dist/services/instances.d.ts.map +1 -0
- package/dist/services/instances.js +140 -0
- package/dist/services/instances.js.map +1 -0
- package/dist/services/loopback.d.ts +5 -0
- package/dist/services/loopback.d.ts.map +1 -0
- package/dist/services/loopback.js +15 -0
- package/dist/services/loopback.js.map +1 -0
- package/dist/services/provider.d.ts +37 -0
- package/dist/services/provider.d.ts.map +1 -0
- package/dist/services/provider.js +521 -0
- package/dist/services/provider.js.map +1 -0
- package/dist/services/state-codec.d.ts +15 -0
- package/dist/services/state-codec.d.ts.map +1 -0
- package/dist/services/state-codec.js +133 -0
- package/dist/services/state-codec.js.map +1 -0
- package/dist/services/state-internals.d.ts +13 -0
- package/dist/services/state-internals.d.ts.map +1 -0
- package/dist/services/state-internals.js +10 -0
- package/dist/services/state-internals.js.map +1 -0
- package/dist/services/state.d.ts +28 -0
- package/dist/services/state.d.ts.map +1 -0
- package/dist/services/state.js +397 -0
- package/dist/services/state.js.map +1 -0
- package/dist/services/wire.d.ts +63 -0
- package/dist/services/wire.d.ts.map +1 -0
- package/dist/services/wire.js +186 -0
- package/dist/services/wire.js.map +1 -0
- package/dist/types.d.ts +277 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +71 -5
- package/src/delta/README.md +191 -0
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { awaitWithContext, BACKGROUND_CONTEXT } from "../context/index.js";
|
|
2
|
+
import { RemoteServiceError } from "./errors.js";
|
|
3
|
+
import { ServiceSlot } from "./handle.js";
|
|
4
|
+
import { InstanceDirectory } from "./instances.js";
|
|
5
|
+
import { ReplicatedStateReplica, serviceDeliveryContext } from "./state.js";
|
|
6
|
+
class MemberSlot {
|
|
7
|
+
#serviceId;
|
|
8
|
+
#member;
|
|
9
|
+
#invoke;
|
|
10
|
+
#state;
|
|
11
|
+
#isActive;
|
|
12
|
+
#assertAccess;
|
|
13
|
+
value;
|
|
14
|
+
#kind;
|
|
15
|
+
#expectedKind;
|
|
16
|
+
constructor(serviceId, member, invoke, isActive, assertAccess, reportError) {
|
|
17
|
+
this.#serviceId = serviceId;
|
|
18
|
+
this.#member = member;
|
|
19
|
+
this.#invoke = invoke;
|
|
20
|
+
this.#isActive = isActive;
|
|
21
|
+
this.#assertAccess = assertAccess;
|
|
22
|
+
this.#state = new ReplicatedStateReplica(reportError);
|
|
23
|
+
const callable = () => { };
|
|
24
|
+
this.value = new Proxy(callable, {
|
|
25
|
+
apply: (_target, _thisArg, args) => this.#call(args),
|
|
26
|
+
get: (_target, property) => {
|
|
27
|
+
if (property === "value") {
|
|
28
|
+
this.#assertAccess();
|
|
29
|
+
this.#expect("state");
|
|
30
|
+
return this.#state.value;
|
|
31
|
+
}
|
|
32
|
+
if (property === "subscribe")
|
|
33
|
+
return this.#subscribe.bind(this);
|
|
34
|
+
if (property === Symbol.toStringTag)
|
|
35
|
+
return "RemoteServiceMember";
|
|
36
|
+
if (property === "then")
|
|
37
|
+
return undefined;
|
|
38
|
+
return undefined;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
setDescription(kind) {
|
|
43
|
+
if (this.#kind !== undefined && this.#kind !== kind) {
|
|
44
|
+
throw new Error(`Remote service member ${this.#serviceId}.${this.#member} changed kind`);
|
|
45
|
+
}
|
|
46
|
+
this.#kind = kind;
|
|
47
|
+
if (this.#expectedKind !== undefined && this.#expectedKind !== kind) {
|
|
48
|
+
throw new RemoteServiceError("service_member_mismatch", `Remote service member ${this.#serviceId}.${this.#member} is ${kind}, not ${this.#expectedKind}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
hydrate(sequence, ops, context) {
|
|
52
|
+
this.setDescription("state");
|
|
53
|
+
this.#state.hydrate(sequence, ops, context);
|
|
54
|
+
}
|
|
55
|
+
update(sequence, ops, context) {
|
|
56
|
+
this.setDescription("state");
|
|
57
|
+
this.#state.update(sequence, ops, context);
|
|
58
|
+
}
|
|
59
|
+
clear() {
|
|
60
|
+
this.#state.clear();
|
|
61
|
+
}
|
|
62
|
+
#subscribe(listener) {
|
|
63
|
+
this.#assertAccess();
|
|
64
|
+
if (typeof listener !== "function") {
|
|
65
|
+
throw new TypeError("Replicated state subscription listener must be a function");
|
|
66
|
+
}
|
|
67
|
+
this.#expect("state");
|
|
68
|
+
return this.#state.subscribe(listener);
|
|
69
|
+
}
|
|
70
|
+
#expect(kind) {
|
|
71
|
+
if (this.#expectedKind !== undefined && this.#expectedKind !== kind) {
|
|
72
|
+
throw new RemoteServiceError("service_member_mismatch", `Remote service member ${this.#serviceId}.${this.#member} was used as two different kinds`);
|
|
73
|
+
}
|
|
74
|
+
this.#expectedKind = kind;
|
|
75
|
+
if (this.#kind !== undefined && this.#kind !== kind) {
|
|
76
|
+
throw new RemoteServiceError("service_member_mismatch", `Remote service member ${this.#serviceId}.${this.#member} is ${this.#kind}, not ${kind}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
#call(args) {
|
|
80
|
+
this.#assertAccess();
|
|
81
|
+
this.#expect("method");
|
|
82
|
+
if (!this.#isActive()) {
|
|
83
|
+
return Promise.reject(new RemoteServiceError("service_stale_instance", `Remote service ${this.#serviceId} binding is closed`));
|
|
84
|
+
}
|
|
85
|
+
const context = args.at(-1);
|
|
86
|
+
if (!isContext(context)) {
|
|
87
|
+
return Promise.reject(new RemoteServiceError("service_invalid_value", `Remote service method ${this.#serviceId}.${this.#member} requires a trailing Context`));
|
|
88
|
+
}
|
|
89
|
+
const businessArgs = args.slice(0, -1);
|
|
90
|
+
return this.#invoke(businessArgs, context);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
class ServiceFacade {
|
|
94
|
+
#serviceId;
|
|
95
|
+
#address;
|
|
96
|
+
#transport;
|
|
97
|
+
#reportError;
|
|
98
|
+
#slots = new Map();
|
|
99
|
+
#descriptions = new Map();
|
|
100
|
+
#isActive;
|
|
101
|
+
#assertAccess;
|
|
102
|
+
proxy;
|
|
103
|
+
constructor(serviceId, address, transport, isActive, assertAccess, reportError) {
|
|
104
|
+
this.#serviceId = serviceId;
|
|
105
|
+
this.#address = address;
|
|
106
|
+
this.#transport = transport;
|
|
107
|
+
this.#isActive = isActive;
|
|
108
|
+
this.#assertAccess = assertAccess;
|
|
109
|
+
this.#reportError = reportError;
|
|
110
|
+
this.proxy = new Proxy(Object.create(null), {
|
|
111
|
+
get: (_target, property) => {
|
|
112
|
+
if (typeof property !== "string")
|
|
113
|
+
return undefined;
|
|
114
|
+
return this.#slot(property).value;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
install(snapshot, context) {
|
|
119
|
+
if (!sameAddress(snapshot.instance, this.#address))
|
|
120
|
+
throw new Error("Remote service snapshot has the wrong address");
|
|
121
|
+
const members = validateMembers(snapshot.members);
|
|
122
|
+
for (const name of this.#slots.keys()) {
|
|
123
|
+
if (!members.has(name)) {
|
|
124
|
+
throw new RemoteServiceError("service_member_not_found", `Unknown remote service member ${this.#serviceId}.${name}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.#descriptions.clear();
|
|
128
|
+
for (const [name, member] of members)
|
|
129
|
+
this.#descriptions.set(name, member.kind);
|
|
130
|
+
for (const member of members.values()) {
|
|
131
|
+
const slot = this.#slots.get(member.name);
|
|
132
|
+
if (member.kind === "state") {
|
|
133
|
+
(slot ?? this.#slot(member.name)).hydrate(member.sequence, member.ops, context);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
slot?.setDescription(member.kind);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
update(member, sequence, ops, context) {
|
|
141
|
+
if (this.#descriptions.get(member) !== "state") {
|
|
142
|
+
throw new Error(`Remote service update targets non-state member ${this.#serviceId}.${member}`);
|
|
143
|
+
}
|
|
144
|
+
this.#slot(member).update(sequence, ops, context);
|
|
145
|
+
}
|
|
146
|
+
clear() {
|
|
147
|
+
for (const slot of this.#slots.values())
|
|
148
|
+
slot.clear();
|
|
149
|
+
}
|
|
150
|
+
#slot(member) {
|
|
151
|
+
let slot = this.#slots.get(member);
|
|
152
|
+
if (slot !== undefined)
|
|
153
|
+
return slot;
|
|
154
|
+
slot = new MemberSlot(this.#serviceId, member, (args, context) => this.#transport.invoke({
|
|
155
|
+
serviceId: this.#serviceId,
|
|
156
|
+
...(this.#address === undefined ? {} : { instance: this.#address }),
|
|
157
|
+
member,
|
|
158
|
+
args,
|
|
159
|
+
}, context), this.#isActive, this.#assertAccess, this.#reportError);
|
|
160
|
+
const kind = this.#descriptions.get(member);
|
|
161
|
+
if (kind !== undefined)
|
|
162
|
+
slot.setDescription(kind);
|
|
163
|
+
this.#slots.set(member, slot);
|
|
164
|
+
return slot;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
class KeyedBinding {
|
|
168
|
+
#service;
|
|
169
|
+
#transport;
|
|
170
|
+
#reportError;
|
|
171
|
+
#assertAccess;
|
|
172
|
+
#onEmpty;
|
|
173
|
+
#instances;
|
|
174
|
+
#subscription;
|
|
175
|
+
#starting;
|
|
176
|
+
#closed = false;
|
|
177
|
+
#bound;
|
|
178
|
+
#revision = 0;
|
|
179
|
+
constructor(service, transport, reportError, assertAccess, onEmpty, bound) {
|
|
180
|
+
this.#service = service;
|
|
181
|
+
this.#transport = transport;
|
|
182
|
+
this.#reportError = reportError;
|
|
183
|
+
this.#assertAccess = assertAccess;
|
|
184
|
+
this.#onEmpty = onEmpty;
|
|
185
|
+
this.#bound = bound;
|
|
186
|
+
this.#instances = new InstanceDirectory({ ready: false, onError: reportError });
|
|
187
|
+
}
|
|
188
|
+
observe(handler) {
|
|
189
|
+
if (this.#closed)
|
|
190
|
+
throw new Error("Remote keyed service binding is closed");
|
|
191
|
+
let stopped = false;
|
|
192
|
+
const stop = this.#instances.observe((service, context) => {
|
|
193
|
+
const slot = new ServiceSlot(this.#service.id, true);
|
|
194
|
+
void slot.bind(service);
|
|
195
|
+
return handler(slot.view(() => {
|
|
196
|
+
this.#assertAccess();
|
|
197
|
+
if (stopped || context.abortSignal?.aborted) {
|
|
198
|
+
throw new RemoteServiceError("service_stale_instance", `Remote service ${this.#service.id} observation is closed`);
|
|
199
|
+
}
|
|
200
|
+
}), context);
|
|
201
|
+
});
|
|
202
|
+
if (this.#bound && this.#starting === undefined) {
|
|
203
|
+
const revision = this.#revision;
|
|
204
|
+
const starting = this.#start(revision);
|
|
205
|
+
this.#starting = starting;
|
|
206
|
+
void starting.catch((error) => {
|
|
207
|
+
if (!this.#closed && this.#revision === revision && this.#bound)
|
|
208
|
+
this.#reportError(toError(error));
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return () => {
|
|
212
|
+
if (stopped)
|
|
213
|
+
return;
|
|
214
|
+
stopped = true;
|
|
215
|
+
stop();
|
|
216
|
+
if (this.#instances.observerCount === 0)
|
|
217
|
+
this.#onEmpty();
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async rebind(bound, context) {
|
|
221
|
+
if (this.#closed)
|
|
222
|
+
return;
|
|
223
|
+
this.#bound = bound;
|
|
224
|
+
const revision = ++this.#revision;
|
|
225
|
+
await this.#reset(context, false);
|
|
226
|
+
if (this.#closed || this.#revision !== revision || this.#bound !== bound)
|
|
227
|
+
return;
|
|
228
|
+
if (bound && this.#instances.observerCount > 0) {
|
|
229
|
+
const starting = this.#start(revision);
|
|
230
|
+
this.#starting = starting;
|
|
231
|
+
await starting;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
ready() {
|
|
235
|
+
return this.#starting ?? Promise.resolve();
|
|
236
|
+
}
|
|
237
|
+
async close(context) {
|
|
238
|
+
if (this.#closed)
|
|
239
|
+
return;
|
|
240
|
+
this.#closed = true;
|
|
241
|
+
this.#revision += 1;
|
|
242
|
+
await this.#reset(context, true);
|
|
243
|
+
this.#instances.dispose();
|
|
244
|
+
}
|
|
245
|
+
async #reset(context, waitForStarting) {
|
|
246
|
+
this.#instances.reset();
|
|
247
|
+
const starting = this.#starting;
|
|
248
|
+
this.#starting = undefined;
|
|
249
|
+
const subscription = this.#subscription;
|
|
250
|
+
this.#subscription = undefined;
|
|
251
|
+
await Promise.all([
|
|
252
|
+
waitForStarting ? starting?.catch(() => { }) : undefined,
|
|
253
|
+
subscription === undefined ? undefined : subscription.close(context),
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
async #start(revision) {
|
|
257
|
+
const subscription = await this.#transport.subscribe(this.#service.id, "keyed", (update, context) => {
|
|
258
|
+
if (this.#revision === revision)
|
|
259
|
+
this.#update(update, context);
|
|
260
|
+
}, BACKGROUND_CONTEXT);
|
|
261
|
+
if (this.#closed || !this.#bound || this.#revision !== revision) {
|
|
262
|
+
await subscription.close(BACKGROUND_CONTEXT);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
this.#subscription = subscription;
|
|
266
|
+
if (subscription.snapshot.mode !== "keyed" || subscription.snapshot.serviceId !== this.#service.id) {
|
|
267
|
+
throw new Error(`Remote service ${this.#service.id} returned the wrong keyed snapshot`);
|
|
268
|
+
}
|
|
269
|
+
for (const snapshot of subscription.snapshot.instances)
|
|
270
|
+
this.#spawn(snapshot, serviceDeliveryContext());
|
|
271
|
+
subscription.activate();
|
|
272
|
+
this.#instances.ready();
|
|
273
|
+
}
|
|
274
|
+
#update(update, context) {
|
|
275
|
+
if (this.#closed)
|
|
276
|
+
return;
|
|
277
|
+
try {
|
|
278
|
+
switch (update.type) {
|
|
279
|
+
case "reset": {
|
|
280
|
+
validateResetSnapshot(update.snapshot, this.#service.id, "keyed");
|
|
281
|
+
const snapshots = new Map(update.snapshot.instances.map((snapshot) => [snapshot.instance.key, snapshot]));
|
|
282
|
+
for (const instance of [...this.#instances.values()]) {
|
|
283
|
+
if (snapshots.get(instance.key)?.instance?.generation !== instance.generation) {
|
|
284
|
+
this.#instances.remove(instance);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const snapshot of snapshots.values()) {
|
|
288
|
+
const instance = this.#instances.get(snapshot.instance.key);
|
|
289
|
+
if (instance === undefined)
|
|
290
|
+
this.#spawn(snapshot, context);
|
|
291
|
+
else
|
|
292
|
+
instance.facade.install(snapshot, context);
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case "unavailable":
|
|
297
|
+
case "replaced":
|
|
298
|
+
throw new Error("Keyed service received a singleton lifecycle update");
|
|
299
|
+
case "spawned":
|
|
300
|
+
this.#spawn(update.instance, context);
|
|
301
|
+
break;
|
|
302
|
+
case "closed": {
|
|
303
|
+
const instance = this.#instances.get(update.instance.key);
|
|
304
|
+
if (instance?.generation === update.instance.generation)
|
|
305
|
+
this.#instances.remove(instance);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "state": {
|
|
309
|
+
if (update.instance === undefined)
|
|
310
|
+
throw new Error("Keyed state update has no instance address");
|
|
311
|
+
const instance = this.#instances.get(update.instance.key);
|
|
312
|
+
if (instance?.generation !== update.instance.generation)
|
|
313
|
+
return;
|
|
314
|
+
instance.facade.update(update.member, update.sequence, update.ops, context);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
this.#reportError(toError(error));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
#spawn(snapshot, context) {
|
|
324
|
+
const address = snapshot.instance;
|
|
325
|
+
if (address === undefined)
|
|
326
|
+
throw new Error("Keyed service instance snapshot has no address");
|
|
327
|
+
let active = true;
|
|
328
|
+
const facade = new ServiceFacade(this.#service.id, address, this.#transport, () => active && !this.#closed, this.#assertAccess, this.#reportError);
|
|
329
|
+
facade.install(snapshot, context);
|
|
330
|
+
this.#instances.replace({
|
|
331
|
+
key: address.key,
|
|
332
|
+
generation: address.generation,
|
|
333
|
+
service: facade.proxy,
|
|
334
|
+
facade,
|
|
335
|
+
deactivate: () => {
|
|
336
|
+
active = false;
|
|
337
|
+
facade.clear();
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export class RemoteServiceBindingImpl {
|
|
343
|
+
#transport;
|
|
344
|
+
#allowlist = new Set();
|
|
345
|
+
#reportError;
|
|
346
|
+
#modes = new Map();
|
|
347
|
+
#assertAccess;
|
|
348
|
+
#singletons = new Map();
|
|
349
|
+
#keyed = new Map();
|
|
350
|
+
#bound;
|
|
351
|
+
#readinessRevision = 0;
|
|
352
|
+
#bindingTransition = Promise.resolve();
|
|
353
|
+
#disposed = false;
|
|
354
|
+
constructor(options) {
|
|
355
|
+
this.#transport = options.transport;
|
|
356
|
+
const ids = options.services.map(({ id }) => id);
|
|
357
|
+
if (new Set(ids).size !== ids.length)
|
|
358
|
+
throw new TypeError("Remote service binding has duplicate service IDs");
|
|
359
|
+
for (const id of ids)
|
|
360
|
+
this.#allowlist.add(id);
|
|
361
|
+
this.#reportError = options.onError ?? (() => { });
|
|
362
|
+
this.#assertAccess = options.assertAccess ?? (() => { });
|
|
363
|
+
this.#bound = options.bound ?? true;
|
|
364
|
+
}
|
|
365
|
+
use(service) {
|
|
366
|
+
this.#assertRemotable(service);
|
|
367
|
+
this.#assertAvailable(service.id, "singleton");
|
|
368
|
+
let binding = this.#singletons.get(service.id);
|
|
369
|
+
if (binding !== undefined)
|
|
370
|
+
return binding.facade.proxy;
|
|
371
|
+
binding = { facade: undefined, active: true, revision: 0 };
|
|
372
|
+
binding.facade = new ServiceFacade(service.id, undefined, this.#transport, () => binding.active && !this.#disposed && this.#bound, () => this.#assertHandleAccess(), this.#reportError);
|
|
373
|
+
this.#singletons.set(service.id, binding);
|
|
374
|
+
this.#readinessRevision += 1;
|
|
375
|
+
if (this.#bound) {
|
|
376
|
+
const revision = binding.revision;
|
|
377
|
+
const starting = this.#startSingleton(service.id, binding, revision);
|
|
378
|
+
binding.starting = starting;
|
|
379
|
+
void starting.catch((error) => {
|
|
380
|
+
if (binding.active && binding.revision === revision && !this.#disposed && this.#bound) {
|
|
381
|
+
this.#reportError(toError(error));
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return binding.facade.proxy;
|
|
386
|
+
}
|
|
387
|
+
observe(service, handler) {
|
|
388
|
+
this.#assertRemotable(service);
|
|
389
|
+
this.#assertAvailable(service.id, "keyed");
|
|
390
|
+
let binding = this.#keyed.get(service.id);
|
|
391
|
+
if (binding === undefined) {
|
|
392
|
+
binding = new KeyedBinding(service, this.#transport, this.#reportError, () => this.#assertHandleAccess(), () => {
|
|
393
|
+
if (this.#keyed.get(service.id) !== binding)
|
|
394
|
+
return;
|
|
395
|
+
this.#keyed.delete(service.id);
|
|
396
|
+
this.#readinessRevision += 1;
|
|
397
|
+
void binding.close(BACKGROUND_CONTEXT).catch(this.#reportError);
|
|
398
|
+
}, this.#bound);
|
|
399
|
+
this.#keyed.set(service.id, binding);
|
|
400
|
+
this.#readinessRevision += 1;
|
|
401
|
+
}
|
|
402
|
+
return binding.observe(handler);
|
|
403
|
+
}
|
|
404
|
+
async ready(context) {
|
|
405
|
+
if (this.#disposed)
|
|
406
|
+
throw new Error("Remote service binding is disposed");
|
|
407
|
+
while (true) {
|
|
408
|
+
const revision = this.#readinessRevision;
|
|
409
|
+
const starts = [
|
|
410
|
+
this.#bindingTransition,
|
|
411
|
+
...[...this.#singletons.values()].flatMap((binding) => binding.starting === undefined ? [] : [binding.starting]),
|
|
412
|
+
...[...this.#keyed.values()].map((binding) => binding.ready()),
|
|
413
|
+
];
|
|
414
|
+
await awaitWithContext(Promise.all(starts).then(() => undefined), context);
|
|
415
|
+
if (this.#disposed)
|
|
416
|
+
throw new Error("Remote service binding is disposed");
|
|
417
|
+
if (revision === this.#readinessRevision)
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
async rebind(bound, context) {
|
|
422
|
+
if (this.#disposed)
|
|
423
|
+
throw new Error("Remote service binding is disposed");
|
|
424
|
+
this.#bound = bound;
|
|
425
|
+
this.#readinessRevision += 1;
|
|
426
|
+
const transitions = [];
|
|
427
|
+
for (const [serviceId, binding] of this.#singletons) {
|
|
428
|
+
binding.revision += 1;
|
|
429
|
+
binding.facade.clear();
|
|
430
|
+
const subscription = binding.subscription;
|
|
431
|
+
delete binding.subscription;
|
|
432
|
+
const revision = binding.revision;
|
|
433
|
+
const starting = (async () => {
|
|
434
|
+
await subscription?.close(context);
|
|
435
|
+
if (bound)
|
|
436
|
+
await this.#startSingleton(serviceId, binding, revision);
|
|
437
|
+
})();
|
|
438
|
+
binding.starting = starting;
|
|
439
|
+
transitions.push(starting);
|
|
440
|
+
}
|
|
441
|
+
for (const binding of this.#keyed.values())
|
|
442
|
+
transitions.push(binding.rebind(bound, context));
|
|
443
|
+
const completion = (async () => {
|
|
444
|
+
const results = await Promise.allSettled(transitions);
|
|
445
|
+
const errors = results.filter((result) => result.status === "rejected");
|
|
446
|
+
if (errors.length > 0)
|
|
447
|
+
throw new AggregateError(errors.map(({ reason }) => reason), "Failed to rebind services");
|
|
448
|
+
})();
|
|
449
|
+
this.#bindingTransition = completion;
|
|
450
|
+
await completion;
|
|
451
|
+
}
|
|
452
|
+
async dispose(context) {
|
|
453
|
+
if (this.#disposed)
|
|
454
|
+
return;
|
|
455
|
+
this.#disposed = true;
|
|
456
|
+
const closes = [];
|
|
457
|
+
for (const binding of this.#singletons.values()) {
|
|
458
|
+
binding.active = false;
|
|
459
|
+
binding.facade.clear();
|
|
460
|
+
if (binding.starting)
|
|
461
|
+
closes.push(binding.starting.catch(() => { }));
|
|
462
|
+
if (binding.subscription)
|
|
463
|
+
closes.push(Promise.resolve(binding.subscription.close(context)));
|
|
464
|
+
}
|
|
465
|
+
for (const binding of this.#keyed.values())
|
|
466
|
+
closes.push(binding.close(context));
|
|
467
|
+
this.#singletons.clear();
|
|
468
|
+
this.#keyed.clear();
|
|
469
|
+
const results = await Promise.allSettled(closes);
|
|
470
|
+
const errors = results.filter((result) => result.status === "rejected");
|
|
471
|
+
if (errors.length > 0)
|
|
472
|
+
throw new AggregateError(errors.map(({ reason }) => reason), "Failed to dispose services");
|
|
473
|
+
}
|
|
474
|
+
async #startSingleton(serviceId, binding, revision) {
|
|
475
|
+
const subscription = await this.#transport.subscribe(serviceId, "singleton", (update, context) => {
|
|
476
|
+
if (!binding.active || binding.revision !== revision)
|
|
477
|
+
return;
|
|
478
|
+
try {
|
|
479
|
+
if (update.type === "reset") {
|
|
480
|
+
validateResetSnapshot(update.snapshot, serviceId, "singleton");
|
|
481
|
+
const snapshot = update.snapshot.instances[0];
|
|
482
|
+
if (snapshot === undefined)
|
|
483
|
+
binding.facade.clear();
|
|
484
|
+
else
|
|
485
|
+
binding.facade.install(snapshot, context);
|
|
486
|
+
}
|
|
487
|
+
else if (update.type === "unavailable") {
|
|
488
|
+
binding.facade.clear();
|
|
489
|
+
}
|
|
490
|
+
else if (update.type === "replaced") {
|
|
491
|
+
if (update.snapshot.instance !== undefined) {
|
|
492
|
+
throw new Error("Singleton replacement has an instance address");
|
|
493
|
+
}
|
|
494
|
+
binding.facade.install(update.snapshot, context);
|
|
495
|
+
}
|
|
496
|
+
else if (update.type === "state" && update.instance === undefined) {
|
|
497
|
+
binding.facade.update(update.member, update.sequence, update.ops, context);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
this.#reportError(toError(error));
|
|
502
|
+
}
|
|
503
|
+
}, BACKGROUND_CONTEXT);
|
|
504
|
+
if (!binding.active || this.#disposed || !this.#bound || binding.revision !== revision) {
|
|
505
|
+
await subscription.close(BACKGROUND_CONTEXT);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
binding.subscription = subscription;
|
|
509
|
+
const snapshot = subscription.snapshot;
|
|
510
|
+
if (snapshot.mode !== "singleton" || snapshot.serviceId !== serviceId || snapshot.instances.length !== 1) {
|
|
511
|
+
throw new Error(`Remote service ${serviceId} returned an invalid singleton snapshot`);
|
|
512
|
+
}
|
|
513
|
+
binding.facade.install(snapshot.instances[0], serviceDeliveryContext());
|
|
514
|
+
subscription.activate();
|
|
515
|
+
}
|
|
516
|
+
#assertHandleAccess() {
|
|
517
|
+
if (this.#disposed)
|
|
518
|
+
throw new Error("Remote service binding is disposed");
|
|
519
|
+
this.#assertAccess();
|
|
520
|
+
}
|
|
521
|
+
#assertRemotable(service) {
|
|
522
|
+
if (service.local)
|
|
523
|
+
throw new RemoteServiceError("service_not_allowed", `Service ${service.id} is process-local`);
|
|
524
|
+
}
|
|
525
|
+
#assertAvailable(serviceId, mode) {
|
|
526
|
+
if (this.#disposed)
|
|
527
|
+
throw new Error("Remote service binding is disposed");
|
|
528
|
+
if (!this.#allowlist.has(serviceId)) {
|
|
529
|
+
throw new RemoteServiceError("service_not_allowed", `Remote service ${serviceId} is not allowlisted`);
|
|
530
|
+
}
|
|
531
|
+
const existing = this.#modes.get(serviceId);
|
|
532
|
+
if (existing !== undefined && existing !== mode) {
|
|
533
|
+
throw new RemoteServiceError("service_mode_mismatch", `Remote service ${serviceId} is already used as ${existing}`);
|
|
534
|
+
}
|
|
535
|
+
this.#modes.set(serviceId, mode);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function validateResetSnapshot(snapshot, serviceId, mode) {
|
|
539
|
+
if (snapshot.serviceId !== serviceId ||
|
|
540
|
+
snapshot.mode !== mode ||
|
|
541
|
+
(mode === "singleton" && snapshot.instances.length > 1)) {
|
|
542
|
+
throw new Error("Remote service reset has the wrong service or mode");
|
|
543
|
+
}
|
|
544
|
+
const keys = new Set();
|
|
545
|
+
for (const instance of snapshot.instances) {
|
|
546
|
+
if (mode === "singleton" ? instance.instance !== undefined : instance.instance === undefined) {
|
|
547
|
+
throw new Error("Remote service reset has an invalid instance address");
|
|
548
|
+
}
|
|
549
|
+
if (instance.instance !== undefined) {
|
|
550
|
+
if (keys.has(instance.instance.key))
|
|
551
|
+
throw new Error("Remote service reset repeats an instance key");
|
|
552
|
+
keys.add(instance.instance.key);
|
|
553
|
+
}
|
|
554
|
+
for (const member of instance.members) {
|
|
555
|
+
if (member.kind === "state" && (member.ops.length !== 1 || member.ops[0]?.[0] !== "r")) {
|
|
556
|
+
throw new Error("Remote service reset must contain full root replacements");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function validateMembers(members) {
|
|
562
|
+
const result = new Map();
|
|
563
|
+
for (const member of members) {
|
|
564
|
+
if (member.name.length === 0 || result.has(member.name)) {
|
|
565
|
+
throw new Error("Remote service has invalid member descriptions");
|
|
566
|
+
}
|
|
567
|
+
result.set(member.name, member);
|
|
568
|
+
}
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
function sameAddress(left, right) {
|
|
572
|
+
if (left === undefined || right === undefined)
|
|
573
|
+
return left === right;
|
|
574
|
+
return left.key === right.key && left.generation === right.generation;
|
|
575
|
+
}
|
|
576
|
+
function isContext(value) {
|
|
577
|
+
if (typeof value !== "object" || value === null)
|
|
578
|
+
return false;
|
|
579
|
+
const candidate = value;
|
|
580
|
+
return typeof candidate.value === "function" && typeof candidate.toString === "function";
|
|
581
|
+
}
|
|
582
|
+
function toError(error) {
|
|
583
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
584
|
+
}
|
|
585
|
+
//# sourceMappingURL=consumer.js.map
|