@spine-event-engine/auth 2.0.0-snapshot.24 → 2.0.0-snapshot.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/REFERENCE.md +42 -10
- package/dist/gateway/dynamic-unary-forwarder.d.ts +62 -3
- package/dist/gateway/dynamic-unary-forwarder.d.ts.map +1 -1
- package/dist/gateway/dynamic-unary-forwarder.js +189 -10
- package/dist/gateway/dynamic-unary-forwarder.js.map +1 -1
- package/dist/gateway/index.d.ts +5 -0
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +65 -7
- package/dist/gateway/index.js.map +1 -1
- package/dist/index.d.ts +45 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +44 -3
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +220 -32
- package/dist/native/index.js.map +1 -1
- package/dist/request/index.d.ts +8 -0
- package/dist/request/index.d.ts.map +1 -1
- package/dist/request/index.js +93 -44
- package/dist/request/index.js.map +1 -1
- package/dist/subscriptions/index.d.ts +35 -3
- package/dist/subscriptions/index.d.ts.map +1 -1
- package/dist/subscriptions/index.js +433 -121
- package/dist/subscriptions/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { clone, create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
15
15
|
import { SUBSCRIPTION_ACTIVATION_HANDSHAKE_MS } from "@spine-event-engine/core/spi/subscription-lifecycle";
|
|
16
|
+
import { ReaderIdentities, snapshotTrustedReader, } from "@spine-event-engine/core/spi/trusted-reader";
|
|
16
17
|
import { ActorContextSchema, TenantIdSchema } from "@spine-event-engine/proto";
|
|
17
18
|
import { SubscriptionSchema, SubscriptionUpdateSchema, TopicSchema, } from "@spine-event-engine/proto/client";
|
|
18
19
|
import { IncomingRequests } from "../request/index.js";
|
|
@@ -34,6 +35,7 @@ export class InMemorySubscriptionBindings {
|
|
|
34
35
|
#limits;
|
|
35
36
|
/**
|
|
36
37
|
* Creates the in-memory binding store.
|
|
38
|
+
*
|
|
37
39
|
* @param options Supplies identifiers, process limits, and disposal behavior.
|
|
38
40
|
*/
|
|
39
41
|
constructor(options) {
|
|
@@ -43,6 +45,7 @@ export class InMemorySubscriptionBindings {
|
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
47
|
* Returns the number of retained private bindings for lifecycle observability.
|
|
48
|
+
*
|
|
46
49
|
* @returns Returns the retained binding count.
|
|
47
50
|
*/
|
|
48
51
|
get size() {
|
|
@@ -50,6 +53,7 @@ export class InMemorySubscriptionBindings {
|
|
|
50
53
|
}
|
|
51
54
|
/**
|
|
52
55
|
* Removes already-expired logical bindings without a background timer.
|
|
56
|
+
*
|
|
53
57
|
* @param nowMs Supplies the current time in milliseconds.
|
|
54
58
|
* @returns Completes after expired envelopes are removed.
|
|
55
59
|
*/
|
|
@@ -61,14 +65,13 @@ export class InMemorySubscriptionBindings {
|
|
|
61
65
|
}
|
|
62
66
|
/**
|
|
63
67
|
* Closes the store by aborting work and waiting for bounded cleanup.
|
|
64
|
-
*
|
|
65
68
|
* Cooperative callbacks allow cleanup to complete before this method resolves.
|
|
66
69
|
* Non-cooperative raw callbacks cause an `AggregateError` after
|
|
67
70
|
* `shutdownTimeoutMs`; their exactly-once disposal remains queued until they
|
|
68
71
|
* settle.
|
|
72
|
+
* `AggregateError` when cleanup exceeds its shutdown limit.
|
|
69
73
|
*
|
|
70
74
|
* @returns Completes after bounded cooperative cleanup, or rejects with an
|
|
71
|
-
* `AggregateError` when cleanup exceeds its shutdown limit.
|
|
72
75
|
*/
|
|
73
76
|
async close() {
|
|
74
77
|
this.#closed = true;
|
|
@@ -84,6 +87,7 @@ export class InMemorySubscriptionBindings {
|
|
|
84
87
|
}
|
|
85
88
|
/**
|
|
86
89
|
* Creates an inactive binding from a trusted rewritten topic.
|
|
90
|
+
*
|
|
87
91
|
* @param input Supplies the topic and expiry.
|
|
88
92
|
* @returns Returns the retained public subscription wire.
|
|
89
93
|
*/
|
|
@@ -103,6 +107,7 @@ export class InMemorySubscriptionBindings {
|
|
|
103
107
|
this.#bindings.set(id, {
|
|
104
108
|
definition: wire.wire.bytes.slice(),
|
|
105
109
|
context: clone(ActorContextSchema, topic.context),
|
|
110
|
+
reader: input.reader,
|
|
106
111
|
expiresAtMs: input.whenExpires,
|
|
107
112
|
state: "inactive",
|
|
108
113
|
controller: new AbortController(),
|
|
@@ -120,6 +125,7 @@ export class InMemorySubscriptionBindings {
|
|
|
120
125
|
}
|
|
121
126
|
/**
|
|
122
127
|
* Activates only an owned inactive binding and restores retry state after callback failure.
|
|
128
|
+
*
|
|
123
129
|
* @param input Supplies ownership facts and the backend callback.
|
|
124
130
|
* @returns Returns the ownership transition outcome.
|
|
125
131
|
*/
|
|
@@ -134,6 +140,9 @@ export class InMemorySubscriptionBindings {
|
|
|
134
140
|
return { kind: "denied" };
|
|
135
141
|
binding.state = "active";
|
|
136
142
|
binding.controller = new AbortController();
|
|
143
|
+
/**
|
|
144
|
+
* Cancels this activation when the caller aborts.
|
|
145
|
+
*/
|
|
137
146
|
const abort = () => {
|
|
138
147
|
binding.controller.abort();
|
|
139
148
|
};
|
|
@@ -155,6 +164,7 @@ export class InMemorySubscriptionBindings {
|
|
|
155
164
|
}
|
|
156
165
|
/**
|
|
157
166
|
* Cancels an owned binding and retains retry state after failed cleanup.
|
|
167
|
+
*
|
|
158
168
|
* @param input Supplies ownership facts and the backend callback.
|
|
159
169
|
* @returns Returns the ownership transition outcome.
|
|
160
170
|
*/
|
|
@@ -182,7 +192,12 @@ export class InMemorySubscriptionBindings {
|
|
|
182
192
|
return { kind: "closed" };
|
|
183
193
|
});
|
|
184
194
|
}
|
|
185
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Checks whether the retained binding still matches its current ID slot.
|
|
197
|
+
*
|
|
198
|
+
* @param input Retained binding and current lifecycle state.
|
|
199
|
+
* @returns Whether this binding is still current.
|
|
200
|
+
*/ #owned(input) {
|
|
186
201
|
const binding = this.#bindings.get(input.id);
|
|
187
202
|
if (binding === undefined)
|
|
188
203
|
return Promise.resolve(undefined);
|
|
@@ -190,15 +205,22 @@ export class InMemorySubscriptionBindings {
|
|
|
190
205
|
this.#expire(input.id, binding);
|
|
191
206
|
return Promise.resolve(undefined);
|
|
192
207
|
}
|
|
193
|
-
return SubscriptionGatewayValues.contextsEqual(binding.context, input.context)
|
|
208
|
+
return SubscriptionGatewayValues.contextsEqual(binding.context, input.context) &&
|
|
209
|
+
ReaderIdentities.matches(binding.reader, input.reader)
|
|
194
210
|
? Promise.resolve(binding)
|
|
195
211
|
: Promise.resolve(undefined);
|
|
196
212
|
}
|
|
197
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Checks retained binding state before an asynchronous effect.
|
|
215
|
+
*
|
|
216
|
+
* @param input Retained binding and current lifecycle state.
|
|
217
|
+
* @returns Whether the effect may proceed.
|
|
218
|
+
*/ #precheck(input) {
|
|
198
219
|
const binding = this.#bindings.get(input.id);
|
|
199
220
|
if (binding === undefined)
|
|
200
221
|
return "absent";
|
|
201
|
-
if (!SubscriptionGatewayValues.contextsEqual(binding.context, input.context)
|
|
222
|
+
if (!SubscriptionGatewayValues.contextsEqual(binding.context, input.context) ||
|
|
223
|
+
!ReaderIdentities.matches(binding.reader, input.reader))
|
|
202
224
|
return "denied";
|
|
203
225
|
if (binding.expiring ||
|
|
204
226
|
(binding.expiresAtMs !== undefined && binding.expiresAtMs <= input.nowMs)) {
|
|
@@ -207,7 +229,12 @@ export class InMemorySubscriptionBindings {
|
|
|
207
229
|
}
|
|
208
230
|
return "owned";
|
|
209
231
|
}
|
|
210
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Expires one retained binding only if it is still current.
|
|
234
|
+
*
|
|
235
|
+
* @param id The stable identifier for this record.
|
|
236
|
+
* @param binding Retained subscription binding.
|
|
237
|
+
*/ #expire(id, binding) {
|
|
211
238
|
if (binding.expiring || this.#bindings.get(id) !== binding)
|
|
212
239
|
return;
|
|
213
240
|
binding.expiring = true;
|
|
@@ -215,16 +242,33 @@ export class InMemorySubscriptionBindings {
|
|
|
215
242
|
// spine-log-boundary: auth.subscription_expiry_cleanup
|
|
216
243
|
void this.#disposeAfterWork(id, binding).catch(() => undefined);
|
|
217
244
|
}
|
|
218
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Clears retained definition bytes and releases binding state.
|
|
247
|
+
*
|
|
248
|
+
* @param id The stable identifier for this record.
|
|
249
|
+
* @param binding Retained subscription binding.
|
|
250
|
+
*/ #dispose(id, binding) {
|
|
219
251
|
binding.definition.fill(0);
|
|
220
252
|
binding.state = "closed";
|
|
221
253
|
this.#bindings.delete(id);
|
|
222
254
|
}
|
|
223
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Starts one retained definition effect and tracks its completion.
|
|
257
|
+
*
|
|
258
|
+
* @param binding Retained subscription binding.
|
|
259
|
+
* @param callback Definition or lifecycle callback.
|
|
260
|
+
* @returns Completion after the operation settles.
|
|
261
|
+
*/ async #runEffect(binding, callback) {
|
|
224
262
|
const effect = this.#startEffect(binding, callback);
|
|
225
263
|
await SubscriptionGatewayValues.withTimeout(effect, this.#limits.operationTimeoutMs, binding.controller);
|
|
226
264
|
}
|
|
227
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Runs a retained binding effect while observing cancellation.
|
|
267
|
+
*
|
|
268
|
+
* @param binding Retained subscription binding.
|
|
269
|
+
* @param callback Definition or lifecycle callback.
|
|
270
|
+
* @returns Completion after the operation settles.
|
|
271
|
+
*/ async #runActiveEffect(binding, callback) {
|
|
228
272
|
let observeAbort = () => undefined;
|
|
229
273
|
const aborted = new Promise((resolve) => {
|
|
230
274
|
observeAbort = () => {
|
|
@@ -245,7 +289,13 @@ export class InMemorySubscriptionBindings {
|
|
|
245
289
|
}
|
|
246
290
|
throw new Error("subscription operation aborted");
|
|
247
291
|
}
|
|
248
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Builds the public definition callback effect for a retained binding.
|
|
294
|
+
*
|
|
295
|
+
* @param binding Retained subscription binding.
|
|
296
|
+
* @param callback Definition or lifecycle callback.
|
|
297
|
+
* @returns Completion after the operation settles.
|
|
298
|
+
*/ #startEffect(binding, callback) {
|
|
249
299
|
const definition = SubscriptionGatewayValues.copyPublic({
|
|
250
300
|
kind: "public-subscription",
|
|
251
301
|
bytes: binding.definition,
|
|
@@ -265,7 +315,13 @@ export class InMemorySubscriptionBindings {
|
|
|
265
315
|
void settled.then(() => definition.bytes.fill(0));
|
|
266
316
|
return effect;
|
|
267
317
|
}
|
|
268
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Waits for active callback work before clearing a binding.
|
|
320
|
+
*
|
|
321
|
+
* @param id The stable identifier for this record.
|
|
322
|
+
* @param binding Retained subscription binding.
|
|
323
|
+
* @returns Completion after the operation settles.
|
|
324
|
+
*/ #disposeAfterWork(id, binding) {
|
|
269
325
|
const cleanup = binding.tail
|
|
270
326
|
.then(() => binding.effectTail)
|
|
271
327
|
.then(() => this.#disposeWithCallback(id, binding));
|
|
@@ -275,10 +331,21 @@ export class InMemorySubscriptionBindings {
|
|
|
275
331
|
void cleanup.catch(() => undefined);
|
|
276
332
|
return cleanup;
|
|
277
333
|
}
|
|
278
|
-
|
|
334
|
+
/**
|
|
335
|
+
* Waits for accepted binding effects before disposal.
|
|
336
|
+
*
|
|
337
|
+
* @param cleanup Resource cleanup operation.
|
|
338
|
+
* @returns Completion after the operation settles.
|
|
339
|
+
*/ #waitForShutdown(cleanup) {
|
|
279
340
|
return SubscriptionGatewayValues.withTimeout(cleanup, this.#limits.shutdownTimeoutMs, new AbortController());
|
|
280
341
|
}
|
|
281
|
-
|
|
342
|
+
/**
|
|
343
|
+
* Detaches a retained binding and invokes its cleanup callback.
|
|
344
|
+
*
|
|
345
|
+
* @param id The stable identifier for this record.
|
|
346
|
+
* @param binding Retained subscription binding.
|
|
347
|
+
* @returns Completion after the operation settles.
|
|
348
|
+
*/ async #disposeWithCallback(id, binding) {
|
|
282
349
|
if (this.#bindings.get(id) !== binding)
|
|
283
350
|
return;
|
|
284
351
|
const definition = SubscriptionGatewayValues.copyPublic({
|
|
@@ -339,6 +406,7 @@ export class SubscriptionGateway {
|
|
|
339
406
|
#closed = false;
|
|
340
407
|
/**
|
|
341
408
|
* Creates the subscription gateway.
|
|
409
|
+
*
|
|
342
410
|
* @param options Supplies admission, authorization, binding, and backend collaborators.
|
|
343
411
|
*/
|
|
344
412
|
constructor(options) {
|
|
@@ -349,6 +417,7 @@ export class SubscriptionGateway {
|
|
|
349
417
|
}
|
|
350
418
|
/**
|
|
351
419
|
* Handles one admitted subscription RPC request.
|
|
420
|
+
*
|
|
352
421
|
* @param request Supplies the copied request admission facts.
|
|
353
422
|
* @returns Returns the opaque operation result.
|
|
354
423
|
*/
|
|
@@ -367,6 +436,7 @@ export class SubscriptionGateway {
|
|
|
367
436
|
}
|
|
368
437
|
/**
|
|
369
438
|
* Closes admission and closes retained bindings.
|
|
439
|
+
*
|
|
370
440
|
* @returns Completes after retained bindings close.
|
|
371
441
|
*/
|
|
372
442
|
async close() {
|
|
@@ -381,6 +451,7 @@ export class SubscriptionGateway {
|
|
|
381
451
|
}
|
|
382
452
|
/**
|
|
383
453
|
* Schedules finite local expiry cleanup for a recovered durable definition.
|
|
454
|
+
*
|
|
384
455
|
* @param whenExpires Supplies the retained expiry in epoch milliseconds.
|
|
385
456
|
*/
|
|
386
457
|
scheduleExpiry(whenExpires) {
|
|
@@ -394,7 +465,12 @@ export class SubscriptionGateway {
|
|
|
394
465
|
}, Math.max(0, whenExpires - nowMs));
|
|
395
466
|
this.#expiryTimers.add(timer);
|
|
396
467
|
}
|
|
397
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Rejects stale or malformed gateway operations before binding mutations.
|
|
470
|
+
*
|
|
471
|
+
* @param request The request being processed.
|
|
472
|
+
* @returns Authorized result or a structured rejection.
|
|
473
|
+
*/ async #handleOperation(request) {
|
|
398
474
|
const kind = SubscriptionGatewayValues.operationFor(request);
|
|
399
475
|
if (kind === undefined)
|
|
400
476
|
return SubscriptionGatewayValues.rejected("unknown-operation");
|
|
@@ -414,7 +490,13 @@ export class SubscriptionGateway {
|
|
|
414
490
|
return prepared;
|
|
415
491
|
return this.#perform(prepared, request.updates, request.signal);
|
|
416
492
|
}
|
|
417
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Resolves the session and policy decision for one subscription operation.
|
|
495
|
+
*
|
|
496
|
+
* @param kind Requested subscription operation.
|
|
497
|
+
* @param request The request being processed.
|
|
498
|
+
* @returns Prepared operation or an authentication/policy rejection.
|
|
499
|
+
*/ async #prepareSecurity(kind, request) {
|
|
418
500
|
const source = SubscriptionGatewayValues.decode(kind, request.wire.bytes, request.transport);
|
|
419
501
|
if (source === undefined)
|
|
420
502
|
return SubscriptionGatewayValues.rejected("malformed-request");
|
|
@@ -431,119 +513,261 @@ export class SubscriptionGateway {
|
|
|
431
513
|
return SubscriptionGatewayValues.rejected("forbidden");
|
|
432
514
|
return this.#resolveTrusted(kind, request, source, session);
|
|
433
515
|
}
|
|
434
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Builds detached trusted reader and actor facts after policy admission.
|
|
518
|
+
*
|
|
519
|
+
* @param kind Requested subscription operation.
|
|
520
|
+
* @param request The request being processed.
|
|
521
|
+
* @param source Decoded incoming subscription request.
|
|
522
|
+
* @param session Resolved gateway session or explicit public principal.
|
|
523
|
+
* @returns Prepared operation or a structured context rejection.
|
|
524
|
+
*/ async #resolveTrusted(kind, request, source, session) {
|
|
435
525
|
const contextRequest = SubscriptionGatewayValues.decode(kind, request.wire.bytes, request.transport);
|
|
436
526
|
if (contextRequest === undefined)
|
|
437
527
|
return SubscriptionGatewayValues.rejected("malformed-request");
|
|
438
|
-
const
|
|
528
|
+
const resolved = await this.#options.contexts.resolve(session.principal, contextRequest, this.#options.clock);
|
|
529
|
+
const context = SubscriptionGatewayValues.trustedContext(resolved);
|
|
530
|
+
const reader = this.#trustedReader(session.principal, resolved);
|
|
439
531
|
const nowMs = this.#nowMs();
|
|
440
|
-
const expiresAtMs = session.expiresAt
|
|
441
|
-
|
|
442
|
-
: SubscriptionGatewayValues.timestampMs(session.expiresAt.seconds, session.expiresAt.nanos);
|
|
443
|
-
if (nowMs === undefined ||
|
|
444
|
-
(this.#options.publicAccess !== true && expiresAtMs === undefined) ||
|
|
445
|
-
(expiresAtMs !== undefined && expiresAtMs <= nowMs) ||
|
|
446
|
-
!SubscriptionGatewayValues.matches(source.requestedContext, context))
|
|
532
|
+
const expiresAtMs = this.#expiresAtMs(session.expiresAt);
|
|
533
|
+
if (nowMs === undefined || !this.#validContext(source, context, nowMs, expiresAtMs))
|
|
447
534
|
return SubscriptionGatewayValues.rejected("denied");
|
|
448
535
|
return {
|
|
449
536
|
source,
|
|
450
537
|
context,
|
|
538
|
+
reader,
|
|
451
539
|
expiresAtMs,
|
|
452
540
|
nowMs,
|
|
453
541
|
};
|
|
454
542
|
}
|
|
455
|
-
|
|
543
|
+
/**
|
|
544
|
+
* Converts a verified session expiry into the gateway clock's millisecond unit.
|
|
545
|
+
*
|
|
546
|
+
* @param value Verified session expiry timestamp, if present.
|
|
547
|
+
* @returns The expiry in epoch milliseconds.
|
|
548
|
+
*/
|
|
549
|
+
#expiresAtMs(value) {
|
|
550
|
+
return value === undefined
|
|
551
|
+
? undefined
|
|
552
|
+
: SubscriptionGatewayValues.timestampMs(value.seconds, value.nanos);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Detaches the verified principal and current authority facts from the resolver.
|
|
556
|
+
*
|
|
557
|
+
* @param principal Verified principal from session resolution.
|
|
558
|
+
* @param resolved Resolved context and authority facts.
|
|
559
|
+
* @returns The detached verified reader facts.
|
|
560
|
+
*/
|
|
561
|
+
#trustedReader(principal, resolved) {
|
|
562
|
+
return snapshotTrustedReader({
|
|
563
|
+
...(this.#options.publicAccess === true ? {} : { principal }),
|
|
564
|
+
...(this.#options.publicAccess === true || resolved.authorityFacts === undefined
|
|
565
|
+
? {}
|
|
566
|
+
: { authorityFacts: resolved.authorityFacts }),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Checks session time and the resolver's trusted actor/tenant against the request.
|
|
571
|
+
*
|
|
572
|
+
* @param source Decoded incoming subscription request.
|
|
573
|
+
* @param context The trusted context for this operation.
|
|
574
|
+
* @param nowMs The current time in milliseconds.
|
|
575
|
+
* @param expiresAtMs The session expiry in milliseconds, when present.
|
|
576
|
+
* @returns The validated actor context.
|
|
577
|
+
*/
|
|
578
|
+
#validContext(source, context, nowMs, expiresAtMs) {
|
|
579
|
+
return ((this.#options.publicAccess === true || expiresAtMs !== undefined) &&
|
|
580
|
+
(expiresAtMs === undefined || expiresAtMs > nowMs) &&
|
|
581
|
+
SubscriptionGatewayValues.matches(source.requestedContext, context));
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Reads the configured clock as finite epoch milliseconds.
|
|
585
|
+
*
|
|
586
|
+
* @returns Current epoch milliseconds, if valid.
|
|
587
|
+
*/ #nowMs() {
|
|
456
588
|
const now = this.#options.clock.now();
|
|
457
589
|
return SubscriptionGatewayValues.timestampMs(now.seconds, now.nanos);
|
|
458
590
|
}
|
|
459
|
-
|
|
460
|
-
|
|
591
|
+
/**
|
|
592
|
+
* Runs the prepared subscription operation after security checks.
|
|
593
|
+
*
|
|
594
|
+
* @param prepared Policy-admitted subscription operation.
|
|
595
|
+
* @param updates The admitted subscription update sink.
|
|
596
|
+
* @param signal The cancellation signal for this operation.
|
|
597
|
+
* @returns The admitted subscription outcome.
|
|
598
|
+
*/ async #perform(prepared, updates, signal) {
|
|
599
|
+
const { source, context, reader, expiresAtMs, nowMs } = prepared;
|
|
461
600
|
const rewritten = SubscriptionGatewayValues.rewrite(source, context);
|
|
462
601
|
if (source.kind === "subscribe")
|
|
463
|
-
return this.#subscribe(rewritten, context, expiresAtMs);
|
|
602
|
+
return this.#subscribe(rewritten, context, reader, expiresAtMs);
|
|
464
603
|
const id = source.subscription.id?.value;
|
|
465
604
|
if (id === undefined || id.length === 0)
|
|
466
605
|
return SubscriptionGatewayValues.rejected("denied");
|
|
467
606
|
return source.kind === "activate"
|
|
468
|
-
? this.#activate(id, context, nowMs, expiresAtMs, updates ?? SubscriptionGatewayValues.discardUpdate, signal)
|
|
469
|
-
: this.#cancel(id, context, nowMs);
|
|
607
|
+
? this.#activate(id, context, reader, nowMs, expiresAtMs, updates ?? SubscriptionGatewayValues.discardUpdate, signal)
|
|
608
|
+
: this.#cancel(id, context, nowMs, reader);
|
|
470
609
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
610
|
+
/**
|
|
611
|
+
* Verifies membership and activates a retained subscription.
|
|
612
|
+
*
|
|
613
|
+
* @param id The stable identifier for this record.
|
|
614
|
+
* @param context The trusted context for this operation.
|
|
615
|
+
* @param reader The verified reader facts for this operation.
|
|
616
|
+
* @param nowMs The current time in milliseconds.
|
|
617
|
+
* @param expiresAtMs The session expiry in milliseconds, when present.
|
|
618
|
+
* @param updates The admitted subscription update sink.
|
|
619
|
+
* @param signal The cancellation signal for this operation.
|
|
620
|
+
* @returns The update stream after admission.
|
|
621
|
+
*/ async #activate(id, context, reader, nowMs, expiresAtMs, updates, signal) {
|
|
477
622
|
if (signal?.aborted)
|
|
478
623
|
return SubscriptionGatewayValues.rejected("denied");
|
|
479
624
|
if (this.#options.publicAccess === true)
|
|
480
625
|
this.#clearPublicPending(id);
|
|
481
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
482
|
-
const expiry = expiresAtMs === undefined
|
|
483
|
-
? undefined
|
|
484
|
-
: setTimeout(() => {
|
|
485
|
-
activeController.abort();
|
|
486
|
-
}, Math.max(0, expiresAtMs - nowMs));
|
|
487
626
|
let activationFailure;
|
|
488
627
|
let outcome = SubscriptionGatewayValues.rejected("denied");
|
|
489
628
|
try {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
629
|
+
outcome = await this.#activationEffect(id, context, reader, nowMs, expiresAtMs, updates, signal);
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
activationFailure = this.#activationError(error);
|
|
633
|
+
}
|
|
634
|
+
if (this.#options.publicAccess === true)
|
|
635
|
+
await this.#publicActivationCleanup(id, context, reader, nowMs, activationFailure);
|
|
636
|
+
if (activationFailure !== undefined)
|
|
637
|
+
throw activationFailure;
|
|
638
|
+
return outcome;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Preserves a non-Error activation failure as a throwable cause.
|
|
642
|
+
*
|
|
643
|
+
* @param error The failure being classified or propagated.
|
|
644
|
+
* @returns The structured activation error.
|
|
645
|
+
*/
|
|
646
|
+
#activationError(error) {
|
|
647
|
+
return error instanceof Error
|
|
648
|
+
? error
|
|
649
|
+
: new Error("Public subscription activation failed with a non-Error value.", {
|
|
650
|
+
cause: error,
|
|
496
651
|
});
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Releases a public definition while retaining both activation and cleanup failures.
|
|
655
|
+
*
|
|
656
|
+
* @param id The stable identifier for this record.
|
|
657
|
+
* @param context The trusted context for this operation.
|
|
658
|
+
* @param reader The verified reader facts for this operation.
|
|
659
|
+
* @param nowMs The current time in milliseconds.
|
|
660
|
+
* @param activationFailure Failure raised while activating the subscription.
|
|
661
|
+
* @returns Completion after the operation settles.
|
|
662
|
+
*/
|
|
663
|
+
async #publicActivationCleanup(id, context, reader, nowMs, activationFailure) {
|
|
664
|
+
try {
|
|
665
|
+
await this.#cancel(id, context, this.#nowMs() ?? nowMs, reader);
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
const cleanupFailure = error instanceof Error
|
|
669
|
+
? error
|
|
670
|
+
: new Error("Public subscription cleanup failed with a non-Error value.", {
|
|
671
|
+
cause: error,
|
|
672
|
+
});
|
|
673
|
+
if (activationFailure !== undefined)
|
|
674
|
+
throw new AggregateError([activationFailure, cleanupFailure], "public subscription cleanup failed");
|
|
675
|
+
throw cleanupFailure;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Runs one reader-bound binding activation under cancellation and session expiry.
|
|
680
|
+
*
|
|
681
|
+
* @param id The stable identifier for this record.
|
|
682
|
+
* @param context The trusted context for this operation.
|
|
683
|
+
* @param reader The verified reader facts for this operation.
|
|
684
|
+
* @param nowMs The current time in milliseconds.
|
|
685
|
+
* @param expiresAtMs The session expiry in milliseconds, when present.
|
|
686
|
+
* @param updates The admitted subscription update sink.
|
|
687
|
+
* @param signal The cancellation signal for this operation.
|
|
688
|
+
* @returns The tracked activation effect.
|
|
689
|
+
*/
|
|
690
|
+
async #activationEffect(id, context, reader, nowMs, expiresAtMs, updates, signal) {
|
|
691
|
+
const activeController = new AbortController();
|
|
692
|
+
const active = activeController.signal;
|
|
693
|
+
/**
|
|
694
|
+
* Cancels this activation when the caller aborts.
|
|
695
|
+
*/
|
|
696
|
+
const abort = () => {
|
|
697
|
+
activeController.abort();
|
|
698
|
+
};
|
|
699
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
700
|
+
const expiry = this.#activationExpiry(expiresAtMs, nowMs, activeController);
|
|
701
|
+
try {
|
|
702
|
+
const result = await this.#activateBinding(id, context, reader, nowMs, active, updates);
|
|
703
|
+
return result.kind === "activated"
|
|
704
|
+
? { kind: "activated" }
|
|
705
|
+
: SubscriptionGatewayValues.rejected("denied");
|
|
501
706
|
}
|
|
502
707
|
catch (error) {
|
|
503
708
|
if (error instanceof Error && error.message === "binding-busy")
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
activationFailure =
|
|
507
|
-
error instanceof Error
|
|
508
|
-
? error
|
|
509
|
-
: new Error("Public subscription activation failed with a non-Error value.", {
|
|
510
|
-
cause: error,
|
|
511
|
-
});
|
|
709
|
+
return SubscriptionGatewayValues.rejected("binding-busy");
|
|
710
|
+
throw error;
|
|
512
711
|
}
|
|
513
712
|
finally {
|
|
514
713
|
if (expiry !== undefined)
|
|
515
714
|
clearTimeout(expiry);
|
|
516
715
|
signal?.removeEventListener("abort", abort);
|
|
517
716
|
}
|
|
518
|
-
let cleanupFailure;
|
|
519
|
-
if (this.#options.publicAccess === true)
|
|
520
|
-
try {
|
|
521
|
-
await this.#cancel(id, context, this.#nowMs() ?? nowMs);
|
|
522
|
-
}
|
|
523
|
-
catch (error) {
|
|
524
|
-
cleanupFailure =
|
|
525
|
-
error instanceof Error
|
|
526
|
-
? error
|
|
527
|
-
: new Error("Public subscription cleanup failed with a non-Error value.", {
|
|
528
|
-
cause: error,
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
if (activationFailure !== undefined) {
|
|
532
|
-
if (cleanupFailure !== undefined)
|
|
533
|
-
throw new AggregateError([activationFailure, cleanupFailure], "public subscription cleanup failed");
|
|
534
|
-
throw activationFailure;
|
|
535
|
-
}
|
|
536
|
-
if (cleanupFailure !== undefined)
|
|
537
|
-
throw cleanupFailure;
|
|
538
|
-
return outcome;
|
|
539
717
|
}
|
|
540
|
-
|
|
718
|
+
/**
|
|
719
|
+
* Cancels activation when the admitted reader session reaches its expiry.
|
|
720
|
+
*
|
|
721
|
+
* @param expiresAtMs The session expiry in milliseconds, when present.
|
|
722
|
+
* @param nowMs The current time in milliseconds.
|
|
723
|
+
* @param controller Abort controller for the active operation.
|
|
724
|
+
* @returns The pending activation expiry.
|
|
725
|
+
*/
|
|
726
|
+
#activationExpiry(expiresAtMs, nowMs, controller) {
|
|
727
|
+
return expiresAtMs === undefined
|
|
728
|
+
? undefined
|
|
729
|
+
: setTimeout(() => {
|
|
730
|
+
controller.abort();
|
|
731
|
+
}, Math.max(0, expiresAtMs - nowMs));
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Sends one reader-bound activation to the durable binding port.
|
|
735
|
+
*
|
|
736
|
+
* @param id The stable identifier for this record.
|
|
737
|
+
* @param context The trusted context for this operation.
|
|
738
|
+
* @param reader The verified reader facts for this operation.
|
|
739
|
+
* @param nowMs The current time in milliseconds.
|
|
740
|
+
* @param signal The cancellation signal for this operation.
|
|
741
|
+
* @param updates The admitted subscription update sink.
|
|
742
|
+
* @returns The activated reader-bound definition.
|
|
743
|
+
*/
|
|
744
|
+
#activateBinding(id, context, reader, nowMs, signal, updates) {
|
|
745
|
+
return this.#options.bindings.activate({
|
|
746
|
+
id,
|
|
747
|
+
context,
|
|
748
|
+
reader: ReaderIdentities.from(reader),
|
|
749
|
+
nowMs,
|
|
750
|
+
signal,
|
|
751
|
+
onDefinition: (definition, effectSignal) => this.#forwardActivate({ ...definition, reader }, updates, effectSignal),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Verifies reader membership and removes a retained subscription.
|
|
756
|
+
*
|
|
757
|
+
* @param id The stable identifier for this record.
|
|
758
|
+
* @param context The trusted context for this operation.
|
|
759
|
+
* @param nowMs The current time in milliseconds.
|
|
760
|
+
* @param reader The verified reader facts for this operation.
|
|
761
|
+
* @returns The cancellation acknowledgment.
|
|
762
|
+
*/ #cancel(id, context, nowMs, reader = {}) {
|
|
763
|
+
const identity = ReaderIdentities.from(reader);
|
|
541
764
|
const active = this.#cancellations.get(id) ?? [];
|
|
542
|
-
const matching = active.find((entry) => SubscriptionGatewayValues.contextsEqual(entry.context, context)
|
|
765
|
+
const matching = active.find((entry) => SubscriptionGatewayValues.contextsEqual(entry.context, context) &&
|
|
766
|
+
ReaderIdentities.matches(entry.reader, identity));
|
|
543
767
|
if (matching !== undefined)
|
|
544
768
|
return matching.promise;
|
|
545
|
-
const promise = this.#cancelOnce(id, context, nowMs);
|
|
546
|
-
const entry = { context: clone(ActorContextSchema, context), promise };
|
|
769
|
+
const promise = this.#cancelOnce(id, context, nowMs, reader);
|
|
770
|
+
const entry = { context: clone(ActorContextSchema, context), reader: identity, promise };
|
|
547
771
|
active.push(entry);
|
|
548
772
|
this.#cancellations.set(id, active);
|
|
549
773
|
// spine-log-boundary: auth.subscription_cancel_finally_observer
|
|
@@ -561,13 +785,22 @@ export class SubscriptionGateway {
|
|
|
561
785
|
.catch(() => undefined);
|
|
562
786
|
return promise;
|
|
563
787
|
}
|
|
564
|
-
|
|
788
|
+
/**
|
|
789
|
+
* Serializes one cancellation against concurrent work for its ID.
|
|
790
|
+
*
|
|
791
|
+
* @param id The stable identifier for this record.
|
|
792
|
+
* @param context The trusted context for this operation.
|
|
793
|
+
* @param nowMs The current time in milliseconds.
|
|
794
|
+
* @param reader The verified reader facts for this operation.
|
|
795
|
+
* @returns The cancellation outcome.
|
|
796
|
+
*/ async #cancelOnce(id, context, nowMs, reader) {
|
|
565
797
|
try {
|
|
566
798
|
const result = await this.#options.bindings.cancel({
|
|
567
799
|
id,
|
|
568
800
|
context,
|
|
801
|
+
reader: ReaderIdentities.from(reader),
|
|
569
802
|
nowMs,
|
|
570
|
-
onDefinition: (definition, effectSignal) => this.#forwardCancel(definition, effectSignal),
|
|
803
|
+
onDefinition: (definition, effectSignal) => this.#forwardCancel({ ...definition, reader }, effectSignal),
|
|
571
804
|
});
|
|
572
805
|
if (result.kind === "denied")
|
|
573
806
|
return SubscriptionGatewayValues.rejected("denied");
|
|
@@ -580,43 +813,32 @@ export class SubscriptionGateway {
|
|
|
580
813
|
throw error;
|
|
581
814
|
}
|
|
582
815
|
}
|
|
583
|
-
|
|
816
|
+
/**
|
|
817
|
+
* Validates and retains a reader-bound subscription definition.
|
|
818
|
+
*
|
|
819
|
+
* @param bytes The serialized message bytes.
|
|
820
|
+
* @param context The trusted context for this operation.
|
|
821
|
+
* @param reader The verified reader facts for this operation.
|
|
822
|
+
* @param expiresAtMs The session expiry in milliseconds, when present.
|
|
823
|
+
* @returns The new subscription definition.
|
|
824
|
+
*/ async #subscribe(bytes, context, reader, expiresAtMs) {
|
|
584
825
|
const wire = await this.#options.bindings.create({
|
|
585
826
|
topic: { kind: "subscription-topic", bytes: bytes.slice() },
|
|
827
|
+
reader: ReaderIdentities.from(reader),
|
|
586
828
|
...(expiresAtMs === undefined ? {} : { whenExpires: expiresAtMs }),
|
|
587
829
|
});
|
|
830
|
+
const admittedWire = { ...wire, reader };
|
|
588
831
|
const id = fromBinary(SubscriptionSchema, wire.bytes).id?.value;
|
|
589
832
|
if (id === undefined || id.length === 0)
|
|
590
833
|
throw new Error("retained subscription has no ID");
|
|
591
834
|
const controller = new AbortController();
|
|
592
835
|
try {
|
|
593
|
-
await this.#receiveBackend(
|
|
594
|
-
|
|
595
|
-
if (nowMs === undefined || (expiresAtMs !== undefined && expiresAtMs <= nowMs)) {
|
|
596
|
-
await this.#compensateDefinition(wire, controller);
|
|
597
|
-
await this.#options.bindings.cancel({
|
|
598
|
-
id,
|
|
599
|
-
context,
|
|
600
|
-
nowMs: nowMs ?? expiresAtMs ?? 0,
|
|
601
|
-
onDefinition: () => Promise.resolve(),
|
|
602
|
-
});
|
|
603
|
-
return SubscriptionGatewayValues.rejected("denied");
|
|
604
|
-
}
|
|
605
|
-
if (expiresAtMs !== undefined)
|
|
606
|
-
this.scheduleExpiry(expiresAtMs);
|
|
607
|
-
if (this.#options.publicAccess === true)
|
|
608
|
-
this.#schedulePublicPending(id, context);
|
|
609
|
-
return { kind: "subscribed", wire: SubscriptionGatewayValues.copyPublic(wire) };
|
|
836
|
+
await this.#receiveBackend(admittedWire, controller);
|
|
837
|
+
return await this.#confirmCreated(id, context, reader, expiresAtMs, admittedWire, controller);
|
|
610
838
|
}
|
|
611
839
|
catch (error) {
|
|
612
840
|
try {
|
|
613
|
-
await this.#
|
|
614
|
-
await this.#options.bindings.cancel({
|
|
615
|
-
id,
|
|
616
|
-
context,
|
|
617
|
-
nowMs: this.#nowMs() ?? expiresAtMs ?? 0,
|
|
618
|
-
onDefinition: () => Promise.resolve(),
|
|
619
|
-
});
|
|
841
|
+
await this.#discardCreated(id, context, reader, this.#nowMs() ?? expiresAtMs ?? 0, admittedWire, controller);
|
|
620
842
|
// spine-log-boundary: auth.subscription_recovered_cleanup
|
|
621
843
|
}
|
|
622
844
|
catch {
|
|
@@ -625,7 +847,59 @@ export class SubscriptionGateway {
|
|
|
625
847
|
throw error;
|
|
626
848
|
}
|
|
627
849
|
}
|
|
628
|
-
|
|
850
|
+
/**
|
|
851
|
+
* Returns an admitted public definition only while its reader session remains valid.
|
|
852
|
+
*
|
|
853
|
+
* @param id The stable identifier for this record.
|
|
854
|
+
* @param context The trusted context for this operation.
|
|
855
|
+
* @param reader The verified reader facts for this operation.
|
|
856
|
+
* @param expiresAtMs The session expiry in milliseconds, when present.
|
|
857
|
+
* @param wire Canonical serialized subscription bytes.
|
|
858
|
+
* @param controller Abort controller for the active operation.
|
|
859
|
+
* @returns The confirmed retained definition.
|
|
860
|
+
*/
|
|
861
|
+
async #confirmCreated(id, context, reader, expiresAtMs, wire, controller) {
|
|
862
|
+
const nowMs = this.#nowMs();
|
|
863
|
+
if (nowMs === undefined || (expiresAtMs !== undefined && expiresAtMs <= nowMs)) {
|
|
864
|
+
await this.#discardCreated(id, context, reader, nowMs ?? expiresAtMs ?? 0, wire, controller);
|
|
865
|
+
return SubscriptionGatewayValues.rejected("denied");
|
|
866
|
+
}
|
|
867
|
+
if (expiresAtMs !== undefined)
|
|
868
|
+
this.scheduleExpiry(expiresAtMs);
|
|
869
|
+
if (this.#options.publicAccess === true)
|
|
870
|
+
this.#schedulePublicPending(id, context);
|
|
871
|
+
return {
|
|
872
|
+
kind: "subscribed",
|
|
873
|
+
wire: { kind: "public-subscription", bytes: wire.bytes.slice() },
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Compensates a created definition before it can escape admission.
|
|
878
|
+
*
|
|
879
|
+
* @param id The stable identifier for this record.
|
|
880
|
+
* @param context The trusted context for this operation.
|
|
881
|
+
* @param reader The verified reader facts for this operation.
|
|
882
|
+
* @param nowMs The current time in milliseconds.
|
|
883
|
+
* @param wire Canonical serialized subscription bytes.
|
|
884
|
+
* @param controller Abort controller for the active operation.
|
|
885
|
+
* @returns Completion after the operation settles.
|
|
886
|
+
*/
|
|
887
|
+
async #discardCreated(id, context, reader, nowMs, wire, controller) {
|
|
888
|
+
await this.#compensateDefinition(wire, controller);
|
|
889
|
+
await this.#options.bindings.cancel({
|
|
890
|
+
id,
|
|
891
|
+
context,
|
|
892
|
+
reader: ReaderIdentities.from(reader),
|
|
893
|
+
nowMs,
|
|
894
|
+
onDefinition: () => Promise.resolve(),
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Schedules activation retry for a public subscription.
|
|
899
|
+
*
|
|
900
|
+
* @param id The stable identifier for this record.
|
|
901
|
+
* @param context The trusted context for this operation.
|
|
902
|
+
*/ #schedulePublicPending(id, context) {
|
|
629
903
|
this.#clearPublicPending(id);
|
|
630
904
|
const timer = setTimeout(() => {
|
|
631
905
|
if (this.#publicPendingTimers.get(id) !== timer)
|
|
@@ -641,24 +915,52 @@ export class SubscriptionGateway {
|
|
|
641
915
|
}, SUBSCRIPTION_ACTIVATION_HANDSHAKE_MS);
|
|
642
916
|
this.#publicPendingTimers.set(id, timer);
|
|
643
917
|
}
|
|
644
|
-
|
|
918
|
+
/**
|
|
919
|
+
* Retries pending public activation after backend availability changes.
|
|
920
|
+
*
|
|
921
|
+
* @param id The stable identifier for this record.
|
|
922
|
+
* @param context The trusted context for this operation.
|
|
923
|
+
*/ #retryPublicPending(id, context) {
|
|
645
924
|
if (!this.#closed && !this.#publicPendingTimers.has(id))
|
|
646
925
|
this.#schedulePublicPending(id, context);
|
|
647
926
|
}
|
|
648
|
-
|
|
927
|
+
/**
|
|
928
|
+
* Clears the pending public activation timer for an ID.
|
|
929
|
+
*
|
|
930
|
+
* @param id The stable identifier for this record.
|
|
931
|
+
*/ #clearPublicPending(id) {
|
|
649
932
|
const timer = this.#publicPendingTimers.get(id);
|
|
650
933
|
if (timer !== undefined)
|
|
651
934
|
clearTimeout(timer);
|
|
652
935
|
this.#publicPendingTimers.delete(id);
|
|
653
936
|
}
|
|
654
|
-
|
|
937
|
+
/**
|
|
938
|
+
* Attaches the native backend subscription after admission.
|
|
939
|
+
*
|
|
940
|
+
* @param wire Canonical serialized subscription bytes.
|
|
941
|
+
* @param controller Abort controller for the active operation.
|
|
942
|
+
* @returns Completion after the operation settles.
|
|
943
|
+
*/ async #receiveBackend(wire, controller) {
|
|
655
944
|
return SubscriptionGatewayValues.withTimeout(this.#options.creator.subscribe(SubscriptionGatewayValues.copyPublic(wire), controller.signal, this.#limits.maxBackendEnvelopeBytes), this.#limits.operationTimeoutMs, controller);
|
|
656
945
|
}
|
|
657
|
-
|
|
946
|
+
/**
|
|
947
|
+
* Removes a definition after a failed backend subscription step.
|
|
948
|
+
*
|
|
949
|
+
* @param wire Canonical serialized subscription bytes.
|
|
950
|
+
* @param controller Abort controller for the active operation.
|
|
951
|
+
* @returns Completion after the operation settles.
|
|
952
|
+
*/ async #compensateDefinition(wire, controller) {
|
|
658
953
|
const signal = controller.signal.aborted ? new AbortController().signal : controller.signal;
|
|
659
954
|
await this.#options.creator.cancel({ wire: SubscriptionGatewayValues.copyPublic(wire) }, signal);
|
|
660
955
|
}
|
|
661
|
-
|
|
956
|
+
/**
|
|
957
|
+
* Forwards activation with the verified reader assertion.
|
|
958
|
+
*
|
|
959
|
+
* @param definition The serialized subscription definition.
|
|
960
|
+
* @param updates The admitted subscription update sink.
|
|
961
|
+
* @param signal The cancellation signal for this operation.
|
|
962
|
+
* @returns Completion after the operation settles.
|
|
963
|
+
*/ async #forwardActivate(definition, updates, signal) {
|
|
662
964
|
const privateWire = SubscriptionGatewayValues.copyPublic(definition);
|
|
663
965
|
try {
|
|
664
966
|
const publicSubscription = fromBinary(SubscriptionSchema, definition.bytes);
|
|
@@ -683,7 +985,13 @@ export class SubscriptionGateway {
|
|
|
683
985
|
privateWire.bytes.fill(0);
|
|
684
986
|
}
|
|
685
987
|
}
|
|
686
|
-
|
|
988
|
+
/**
|
|
989
|
+
* Forwards cancellation with the verified reader assertion.
|
|
990
|
+
*
|
|
991
|
+
* @param definition The serialized subscription definition.
|
|
992
|
+
* @param signal The cancellation signal for this operation.
|
|
993
|
+
* @returns Completion after the operation settles.
|
|
994
|
+
*/ async #forwardCancel(definition, signal) {
|
|
687
995
|
const privateWire = SubscriptionGatewayValues.copyPublic(definition);
|
|
688
996
|
try {
|
|
689
997
|
await this.#options.creator.cancel({ wire: privateWire }, signal);
|
|
@@ -715,7 +1023,11 @@ const SubscriptionGatewayValues = Object.freeze({
|
|
|
715
1023
|
* @returns Copied public subscription bytes.
|
|
716
1024
|
*/
|
|
717
1025
|
copyPublic(wire) {
|
|
718
|
-
return {
|
|
1026
|
+
return {
|
|
1027
|
+
kind: wire.kind,
|
|
1028
|
+
bytes: wire.bytes.slice(),
|
|
1029
|
+
...(wire.reader === undefined ? {} : { reader: snapshotTrustedReader(wire.reader) }),
|
|
1030
|
+
};
|
|
719
1031
|
},
|
|
720
1032
|
/**
|
|
721
1033
|
* Builds a successful subscription response.
|