@agentunion/fastaun 0.5.33 → 0.5.35
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 +59 -2
- package/README.md +28 -26
- package/_packed_docs/CHANGELOG.md +59 -2
- package/_packed_docs/INDEX.md +26 -12
- package/_packed_docs/KITE_DOCS_GUIDE.md +17 -11
- package/_packed_docs/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +2 -2
- package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +58 -8
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +17 -17
- package/_packed_docs/protocol/echo-probe-gateway.md +99 -0
- package/_packed_docs/protocol/echo-probe-sdk.md +91 -0
- package/_packed_docs/sdk/0.5.33-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +464 -316
- package/_packed_docs/sdk/0.5.33-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +499 -304
- package/_packed_docs/sdk/0.5.34-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +133 -0
- package/_packed_docs/sdk/0.5.34-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +107 -0
- package/_packed_docs/sdk/0.5.35-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +189 -0
- package/_packed_docs/sdk/0.5.35-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +204 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +16 -13
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +13 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +19 -9
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +87 -20
- package/_packed_docs/sdk/09-collab-rpc-manual.md +48 -30
- package/_packed_docs/sdk/09-custody-api-manual.md +3 -3
- package/_packed_docs/sdk/09-group-rpc-manual.md +60 -13
- package/_packed_docs/sdk/09-message-rpc-manual.md +22 -0
- package/_packed_docs/sdk/09-meta-rpc-manual.md +2 -1
- package/_packed_docs/sdk/09-payload-reference.md +9 -8
- package/_packed_docs/sdk/09-proxy-rpc-manual.md +20 -14
- package/_packed_docs/sdk/09-storage-rpc-manual.md +1698 -1659
- package/_packed_docs/sdk/09-stream-rpc-manual.md +33 -20
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +19 -17
- package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +125 -30
- package/_packed_docs/sdk/AUN/344/272/213/344/273/266/345/217/221/345/270/203/350/256/242/351/230/205/345/274/200/345/217/221/347/247/273/346/244/215/346/214/207/345/215/227.md +109 -33
- package/_packed_docs/sdk/INDEX.md +30 -18
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +1 -1
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +5 -5
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +5 -5
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +9 -9
- package/_packed_docs/sdk/README.md +11 -6
- package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/345/256/236/346/226/275/350/256/241/345/210/222.md +400 -0
- package/_packed_docs//346/266/210/346/201/257/345/274/271/346/200/247/350/241/245/346/213/211/351/200/232/351/201/223/346/226/271/346/241/210.md +485 -0
- package/dist/auth.js +39 -45
- package/dist/auth.js.map +1 -1
- package/dist/client/control-messages.js +2 -2
- package/dist/client/control-messages.js.map +1 -1
- package/dist/client/delivery.d.ts +12 -1
- package/dist/client/delivery.js +281 -46
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/lifecycle.js +13 -1
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-budget.d.ts +21 -0
- package/dist/client/rpc-budget.js +82 -0
- package/dist/client/rpc-budget.js.map +1 -0
- package/dist/client/rpc-pipeline.d.ts +5 -0
- package/dist/client/rpc-pipeline.js +227 -63
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/sync-channels.d.ts +90 -0
- package/dist/client/sync-channels.js +508 -0
- package/dist/client/sync-channels.js.map +1 -0
- package/dist/client/v2-e2ee.d.ts +5 -1
- package/dist/client/v2-e2ee.js +420 -303
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +17 -0
- package/dist/client.js +373 -50
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +1 -0
- package/dist/events.js +5 -0
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mail-client.d.ts +101 -0
- package/dist/mail-client.js +369 -0
- package/dist/mail-client.js.map +1 -0
- package/dist/pubsub/binding.js +23 -3
- package/dist/pubsub/binding.js.map +1 -1
- package/dist/pubsub/channel.d.ts +15 -3
- package/dist/pubsub/channel.js +114 -7
- package/dist/pubsub/channel.js.map +1 -1
- package/dist/pubsub/client.d.ts +17 -2
- package/dist/pubsub/client.js +205 -40
- package/dist/pubsub/client.js.map +1 -1
- package/dist/pubsub/credentials.d.ts +3 -0
- package/dist/pubsub/credentials.js +57 -20
- package/dist/pubsub/credentials.js.map +1 -1
- package/dist/pubsub/index.d.ts +5 -2
- package/dist/pubsub/index.js +70 -54
- package/dist/pubsub/index.js.map +1 -1
- package/dist/pubsub/service.d.ts +9 -0
- package/dist/pubsub/service.js +266 -20
- package/dist/pubsub/service.js.map +1 -1
- package/dist/pubsub/types.d.ts +13 -1
- package/dist/storage/lowlevel.d.ts +3 -1
- package/dist/storage/lowlevel.js +13 -0
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/types.d.ts +10 -0
- package/dist/storage/types.js.map +1 -1
- package/dist/storage/vfs.d.ts +3 -1
- package/dist/storage/vfs.js +2 -2
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +55 -5
- package/dist/tools/cross-sdk-agent.js +88 -21
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +16 -0
- package/dist/transport.js +129 -7
- package/dist/transport.js.map +1 -1
- package/dist/udp-standby.d.ts +89 -0
- package/dist/udp-standby.js +487 -0
- package/dist/udp-standby.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
- package/scripts/patch-imapflow-external.mjs +69 -0
package/dist/pubsub/service.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PubSubError } from './errors.js';
|
|
2
2
|
import { FairScheduler } from './scheduler.js';
|
|
3
|
+
import { matchPattern, publisherFilter, validatePattern } from './channel.js';
|
|
3
4
|
import { digest } from '../org-protocol.js';
|
|
4
5
|
const copy = (v) => v === undefined ? v : JSON.parse(JSON.stringify(v));
|
|
5
6
|
const json = (v) => { if (v === null || typeof v !== 'object')
|
|
@@ -16,6 +17,9 @@ export class EventService {
|
|
|
16
17
|
clock;
|
|
17
18
|
limits;
|
|
18
19
|
state;
|
|
20
|
+
publisherResolver;
|
|
21
|
+
publisherProfiles = new Map();
|
|
22
|
+
publisherLookups = new Map();
|
|
19
23
|
deliveryLeases = new Map();
|
|
20
24
|
lifecycleGeneration = 0;
|
|
21
25
|
generation = 0;
|
|
@@ -33,6 +37,8 @@ export class EventService {
|
|
|
33
37
|
requests = new Map();
|
|
34
38
|
published = new Map();
|
|
35
39
|
counters = new Map();
|
|
40
|
+
publicationSummaries = new Map();
|
|
41
|
+
publicationBytes = 0;
|
|
36
42
|
scheduler;
|
|
37
43
|
snapshots = new Map();
|
|
38
44
|
snapshotRefs = new Map();
|
|
@@ -82,42 +88,101 @@ export class EventService {
|
|
|
82
88
|
return this; this.starting = this.initialize().finally(() => { this.starting = undefined; }); return this.starting; }
|
|
83
89
|
async initialize() {
|
|
84
90
|
const saved = await this.state?.load?.();
|
|
91
|
+
this.leases.clear();
|
|
92
|
+
this.leaseSizes.clear();
|
|
93
|
+
this.leaseBytes = this.subscriptionBytes = 0;
|
|
94
|
+
this.nextLeaseExpiry = Infinity;
|
|
95
|
+
this.subscriptions.clear();
|
|
96
|
+
this.subscriptionsByEvent.clear();
|
|
97
|
+
this.snapshots.clear();
|
|
85
98
|
if (saved && saved.service_aid === this.service_aid) {
|
|
86
|
-
this.catalog = copy(saved.catalog
|
|
87
|
-
this.allowlist = copy(saved.allowlist
|
|
99
|
+
this.catalog = copy({ ...saved.catalog, ...this.catalog });
|
|
100
|
+
this.allowlist = copy({ ...saved.allowlist, ...this.allowlist });
|
|
88
101
|
this.issuedGrants = new Map(saved.issued_grants ?? []);
|
|
89
102
|
this.pausedCids = new Set(saved.paused_cids ?? []);
|
|
90
103
|
}
|
|
91
104
|
this.validateCatalog(this.catalog);
|
|
92
105
|
this.refreshRevision();
|
|
93
|
-
await this.saveState();
|
|
94
106
|
await this.credentials?.refreshRevocations?.();
|
|
107
|
+
if (saved && saved.service_aid === this.service_aid) {
|
|
108
|
+
for (const [id, value] of saved.leases ?? []) {
|
|
109
|
+
const lease = copy(value);
|
|
110
|
+
if (lease.expires_at <= this.clock() || lease.cid && (this.pausedCids.has(lease.cid) || this.credentials?.isRevoked?.(lease.grant ?? lease.cid)))
|
|
111
|
+
continue;
|
|
112
|
+
lease.scope = lease.scope.filter(item => !!this.catalog[item.event_name] && (lease.auth_mode !== 'allowlist' || this.allowlisted(lease.aid, item)));
|
|
113
|
+
if (!lease.scope.length)
|
|
114
|
+
continue;
|
|
115
|
+
if (lease.auth_mode === 'credential') {
|
|
116
|
+
if (!this.credentials || !lease.grant)
|
|
117
|
+
continue;
|
|
118
|
+
try {
|
|
119
|
+
const credentials = this.credentials;
|
|
120
|
+
const verified = credentials._restoreVerificationContext ? await credentials._restoreVerificationContext(lease.grant, lease.aid, lease.scope, lease.verification_context) : await credentials.verify(lease.grant, lease.aid, lease.scope, undefined);
|
|
121
|
+
lease.expires_at = Math.min(lease.expires_at, Number(verified.expires_at ?? lease.expires_at));
|
|
122
|
+
lease.authorization_until = Math.min(lease.expires_at, Number(verified.proof_expires_at ?? lease.expires_at));
|
|
123
|
+
if (lease.authorization_until <= this.clock())
|
|
124
|
+
continue;
|
|
125
|
+
if (verified.verification_context !== undefined)
|
|
126
|
+
lease.verification_context = verified.verification_context;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof PubSubError && ['FORBIDDEN', 'REAUTH_REQUIRED'].includes(error.code))
|
|
130
|
+
continue;
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
this.leases.set(id, lease);
|
|
135
|
+
const size = this.leaseSize(lease);
|
|
136
|
+
this.leaseSizes.set(id, size);
|
|
137
|
+
this.leaseBytes += size;
|
|
138
|
+
this.nextLeaseExpiry = Math.min(this.nextLeaseExpiry, lease.expires_at);
|
|
139
|
+
}
|
|
140
|
+
for (const [key, value] of saved.subscriptions ?? []) {
|
|
141
|
+
const sub = copy(value), lease = this.leases.get(sub.leaseId);
|
|
142
|
+
sub.filter = publisherFilter(sub.filter);
|
|
143
|
+
sub.size = new TextEncoder().encode(key + sub.aid + sub.leaseId + json(sub.filter)).byteLength + 128;
|
|
144
|
+
if (!lease || lease.aid !== sub.aid || !lease.scope.some(item => item.event_name === sub.event && item.action === 'subscribe'))
|
|
145
|
+
continue;
|
|
146
|
+
this.subscriptions.set(key, sub);
|
|
147
|
+
this.subscriptionBytes += sub.size;
|
|
148
|
+
this.generation = Math.max(this.generation, sub.generation);
|
|
149
|
+
let view = this.subscriptionsByEvent.get(sub.event);
|
|
150
|
+
if (!view)
|
|
151
|
+
this.subscriptionsByEvent.set(sub.event, view = new Map());
|
|
152
|
+
view.set(key, sub);
|
|
153
|
+
}
|
|
154
|
+
this.pruneSubscriptions();
|
|
155
|
+
}
|
|
156
|
+
await this.saveState();
|
|
95
157
|
this.scheduler = new FairScheduler(task => this.deliver(task), { concurrency: Math.min(4, this.limits.event_concurrency ?? 4), quantum: this.limits.quantum ?? 4096, maxQueued: this.limits.delivery_items ?? 4096, maxBytes: Math.min(this.limits.delivery_bytes ?? 8 * 1024 * 1024, this.limit('body_bytes', 64 * 1024 * 1024), 64 * 1024 * 1024), perAidQueued: this.limits.per_aid_items ?? 512, perAidBytes: this.limits.per_aid_bytes ?? 2 * 1024 * 1024, costOverhead: this.limits.cost_overhead ?? 128, onDrop: (task, reason) => { this.deliveryLeases.delete(task.message_id); this.counter(task.event_name)[reason === 'failed' ? 'failed' : 'skipped_revoked']++; } });
|
|
96
158
|
this.scheduler.onCapacity(() => this.capacityChanged());
|
|
97
159
|
this.counters.clear();
|
|
160
|
+
this.publicationSummaries.clear();
|
|
161
|
+
this.publicationBytes = 0;
|
|
162
|
+
this.tokenKey = uuid();
|
|
98
163
|
this.since = this.clock();
|
|
99
164
|
this.closed = false;
|
|
100
165
|
this.running = true;
|
|
101
166
|
await this.scheduler.start();
|
|
102
167
|
return this;
|
|
103
168
|
}
|
|
104
|
-
saveState() { const value = copy({ service_aid: this.service_aid, catalog: this.catalog, allowlist: this.allowlist, issued_grants: [...this.issuedGrants], paused_cids: [...this.pausedCids] }); const write = this.stateWrite.then(async () => { await this.state?.save?.(value); }); this.stateWrite = write.catch(() => { }); return write; }
|
|
169
|
+
saveState() { const value = copy({ service_aid: this.service_aid, catalog: this.catalog, allowlist: this.allowlist, issued_grants: [...this.issuedGrants], paused_cids: [...this.pausedCids], leases: [...this.leases], subscriptions: [...this.subscriptions] }); const write = this.stateWrite.then(async () => { await this.state?.save?.(value); }); this.stateWrite = write.catch(() => { }); return write; }
|
|
105
170
|
async close() { if (this.starting) {
|
|
106
171
|
try {
|
|
107
172
|
await this.starting;
|
|
108
173
|
}
|
|
109
174
|
catch { }
|
|
110
175
|
} this.lifecycleGeneration++; this.closed = true; this.running = false; this.capacityChanged(); await this.fanoutTask; if (this.scheduler)
|
|
111
|
-
await this.scheduler.close(); this.scheduler = undefined; this.fanout.clear(); this.fanoutReady = []; this.fanoutCount = this.fanoutBytes = this.snapshotBytes = 0; this.snapshotRefs.clear(); this.deliveryLeases.clear(); this.leases.clear(); this.leaseSizes.clear(); this.leaseBytes = this.subscriptionBytes = 0; this.nextLeaseExpiry = Infinity; this.subscriptions.clear(); this.subscriptionsByEvent.clear(); this.snapshots.clear(); this.credentials?.clearVerified?.(); this.requests.clear(); this.published.clear(); await this.stateWrite; }
|
|
176
|
+
await this.scheduler.close(); this.scheduler = undefined; this.fanout.clear(); this.fanoutReady = []; this.fanoutCount = this.fanoutBytes = this.snapshotBytes = 0; this.snapshotRefs.clear(); this.deliveryLeases.clear(); this.leases.clear(); this.leaseSizes.clear(); this.leaseBytes = this.subscriptionBytes = 0; this.nextLeaseExpiry = Infinity; this.subscriptions.clear(); this.subscriptionsByEvent.clear(); this.snapshots.clear(); this.credentials?.clearVerified?.(); this.publisherProfiles.clear(); this.publisherLookups.clear(); this.requests.clear(); this.published.clear(); this.publicationSummaries.clear(); this.publicationBytes = 0; await this.stateWrite; }
|
|
112
177
|
async drain() { while (this.fanoutTask)
|
|
113
178
|
await this.fanoutTask; await this.scheduler?.drain(); }
|
|
114
179
|
setSend(send) { this.send = send; }
|
|
115
180
|
async updateConfig(config) { if (config.catalog) {
|
|
116
181
|
this.validateCatalog(config.catalog);
|
|
117
|
-
this.catalog = copy(config.catalog);
|
|
182
|
+
this.catalog = copy({ ...this.catalog, ...config.catalog });
|
|
118
183
|
this.refreshRevision();
|
|
119
184
|
} if (config.allowlist)
|
|
120
|
-
this.allowlist = copy(config.allowlist); for (const [id, lease] of this.leases) {
|
|
185
|
+
this.allowlist = copy({ ...this.allowlist, ...config.allowlist }); for (const [id, lease] of this.leases) {
|
|
121
186
|
lease.scope = lease.scope.filter(item => !!this.catalog[item.event_name] && (lease.auth_mode !== 'allowlist' || this.allowlisted(lease.aid, item)));
|
|
122
187
|
if (!lease.scope.length)
|
|
123
188
|
this.removeLease(id);
|
|
@@ -166,6 +231,8 @@ export class EventService {
|
|
|
166
231
|
return this.catalogOp(senderAid, request, undefined, undefined, local);
|
|
167
232
|
if (op === 'stats')
|
|
168
233
|
return this.statsOp(senderAid, request);
|
|
234
|
+
if (op === 'publications' || op === 'subscriptions')
|
|
235
|
+
return this.listingOp(senderAid, request, local);
|
|
169
236
|
const id = request.request_id;
|
|
170
237
|
if (!id)
|
|
171
238
|
throw new PubSubError('INVALID_REQUEST');
|
|
@@ -183,7 +250,8 @@ export class EventService {
|
|
|
183
250
|
throw new PubSubError('OVERLOADED');
|
|
184
251
|
const entry = { fingerprint: fp, expiresAt: request.expires_at ?? this.clock() + 60_000 };
|
|
185
252
|
this.requests.set(key, entry);
|
|
186
|
-
const promise = Promise.resolve().then(() => this.dispatch(senderAid, request, route))
|
|
253
|
+
const promise = Promise.resolve().then(async () => { const result = await this.dispatch(senderAid, request, route); if (this.state?.save && ['authorize', 'renew', 'subscribe', 'unsubscribe'].includes(op))
|
|
254
|
+
await this.saveState(); return result; });
|
|
187
255
|
entry.promise = promise;
|
|
188
256
|
try {
|
|
189
257
|
const result = await promise;
|
|
@@ -248,6 +316,7 @@ export class EventService {
|
|
|
248
316
|
let expires = this.clock() + 12_000_000;
|
|
249
317
|
let cid;
|
|
250
318
|
let proofExpires = expires;
|
|
319
|
+
let verificationContext;
|
|
251
320
|
if (mode === 'allowlist') {
|
|
252
321
|
if (scope.some(item => !this.allowlisted(sender, item)))
|
|
253
322
|
throw new PubSubError('FORBIDDEN');
|
|
@@ -262,13 +331,14 @@ export class EventService {
|
|
|
262
331
|
if (this.pausedCids.has(cid) || this.credentials.isRevoked?.(grant))
|
|
263
332
|
throw new PubSubError('REAUTH_REQUIRED');
|
|
264
333
|
proofExpires = Number(verified.proof_expires_at ?? expires);
|
|
334
|
+
verificationContext = verified.verification_context;
|
|
265
335
|
}
|
|
266
336
|
else
|
|
267
337
|
throw new PubSubError('INVALID_AUTH_MODE');
|
|
268
338
|
if (generation !== this.lifecycleGeneration || this.closed)
|
|
269
339
|
throw new PubSubError('SERVICE_CLOSED');
|
|
270
340
|
const grant = mode === 'credential' ? (request.grant ?? request.credential) : undefined;
|
|
271
|
-
const lease = { lease_id: uuid(), aid: sender, ...(route ? { route: copy(route) } : {}), scope, auth_mode: mode, expires_at: expires, authorization_until: Math.min(expires, proofExpires), ...(cid ? { cid } : {}), ...(grant ? { grant } : {}) };
|
|
341
|
+
const lease = { lease_id: uuid(), aid: sender, ...(route ? { route: copy(route) } : {}), scope, auth_mode: mode, expires_at: expires, authorization_until: Math.min(expires, proofExpires), ...(cid ? { cid } : {}), ...(grant ? { grant } : {}), ...(verificationContext !== undefined ? { verification_context: verificationContext } : {}) };
|
|
272
342
|
const size = this.leaseSize(lease);
|
|
273
343
|
if (this.leases.size >= this.limit('lease_items', 16384) || this.leaseBytes + size > this.limit('lease_bytes', 8 * 1024 * 1024))
|
|
274
344
|
throw new PubSubError('OVERLOADED');
|
|
@@ -305,7 +375,7 @@ export class EventService {
|
|
|
305
375
|
throw new PubSubError('REAUTH_REQUIRED');
|
|
306
376
|
}
|
|
307
377
|
catch (error) {
|
|
308
|
-
if (error instanceof PubSubError
|
|
378
|
+
if (!(error instanceof PubSubError) || !['FORBIDDEN', 'REAUTH_REQUIRED', 'CREDENTIAL_REVOKED', 'CREDENTIAL_EXPIRED'].includes(error.code))
|
|
309
379
|
throw error;
|
|
310
380
|
this.removeLease(lease.lease_id);
|
|
311
381
|
this.pruneSubscriptions();
|
|
@@ -313,7 +383,7 @@ export class EventService {
|
|
|
313
383
|
}
|
|
314
384
|
}
|
|
315
385
|
async renew(sender, request) { const old = this.lease(sender, request.lease_id); if (old.auth_mode === 'credential')
|
|
316
|
-
await this.credentials?.assertNotRevoked?.(old.grant ?? old.cid, request.expires_at); const now = this.clock(); let expires = now + 12_000_000, proofExpires = expires; let grant = old.grant, cid = old.cid; if (old.auth_mode === 'credential') {
|
|
386
|
+
await this.credentials?.assertNotRevoked?.(old.grant ?? old.cid, request.expires_at); const now = this.clock(); let expires = now + 12_000_000, proofExpires = expires; let grant = old.grant, cid = old.cid; let verificationContext = old.verification_context; if (old.auth_mode === 'credential') {
|
|
317
387
|
grant = request.grant ?? old.grant;
|
|
318
388
|
if (!this.credentials || !grant)
|
|
319
389
|
throw new PubSubError('REAUTH_REQUIRED');
|
|
@@ -321,18 +391,41 @@ export class EventService {
|
|
|
321
391
|
const verified = await this.credentials.verify(grant, sender, old.scope, request, request.expires_at);
|
|
322
392
|
expires = Math.min(expires, Number(verified.expires_at));
|
|
323
393
|
proofExpires = Number(verified.proof_expires_at ?? expires);
|
|
394
|
+
verificationContext = verified.verification_context;
|
|
324
395
|
cid = String(verified.cid ?? grant.cid ?? '');
|
|
325
396
|
if (Math.min(expires, proofExpires) <= now || this.pausedCids.has(cid) || this.credentials.isRevoked?.(grant))
|
|
326
397
|
throw new PubSubError('REAUTH_REQUIRED');
|
|
327
398
|
} if (this.lease(sender, request.lease_id) !== old)
|
|
328
|
-
throw new PubSubError('REAUTH_REQUIRED'); const updated = { ...old, grant, cid, expires_at: expires, authorization_until: Math.min(expires, proofExpires) }; const change = this.leaseSize(updated) - (this.leaseSizes.get(old.lease_id) ?? 0); if (this.leaseBytes + change > this.limit('lease_bytes', 8 * 1024 * 1024))
|
|
399
|
+
throw new PubSubError('REAUTH_REQUIRED'); const updated = { ...old, grant, cid, expires_at: expires, authorization_until: Math.min(expires, proofExpires), ...(verificationContext !== undefined ? { verification_context: verificationContext } : {}) }; const change = this.leaseSize(updated) - (this.leaseSizes.get(old.lease_id) ?? 0); if (this.leaseBytes + change > this.limit('lease_bytes', 8 * 1024 * 1024))
|
|
329
400
|
throw new PubSubError('OVERLOADED'); Object.assign(old, updated); this.leaseBytes += change; this.leaseSizes.set(old.lease_id, this.leaseSize(updated)); this.nextLeaseExpiry = Math.min(this.nextLeaseExpiry, expires); return this.leaseResult(old); }
|
|
330
|
-
events(request
|
|
331
|
-
|
|
401
|
+
events(request, lease, unsubscribe = false) {
|
|
402
|
+
const names = request.events ?? (request.event_name ? [request.event_name] : []);
|
|
403
|
+
if (!Array.isArray(names) || !names.length)
|
|
404
|
+
throw new PubSubError('UNKNOWN_EVENT');
|
|
405
|
+
const events = new Set();
|
|
406
|
+
for (const name of names) {
|
|
407
|
+
validatePattern(name);
|
|
408
|
+
if (!name.includes('*')) {
|
|
409
|
+
if (!this.catalog[name])
|
|
410
|
+
throw new PubSubError('UNKNOWN_EVENT');
|
|
411
|
+
this.lease(lease.aid, lease.lease_id, [{ event_name: name, action: 'subscribe' }]);
|
|
412
|
+
events.add(name);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const matches = unsubscribe ? [...this.subscriptions.values()].filter(sub => sub.leaseId === lease.lease_id && matchPattern(name, sub.event)).map(sub => sub.event) : Object.keys(this.catalog).filter(event => matchPattern(name, event));
|
|
416
|
+
if (!unsubscribe && !matches.length)
|
|
417
|
+
throw new PubSubError('UNKNOWN_EVENT');
|
|
418
|
+
const authorized = matches.filter(event => lease.scope.some(item => item.event_name === event && item.action === 'subscribe'));
|
|
419
|
+
if (!unsubscribe && !authorized.length)
|
|
420
|
+
throw new PubSubError('FORBIDDEN');
|
|
421
|
+
authorized.forEach(event => events.add(event));
|
|
422
|
+
}
|
|
423
|
+
return [...events];
|
|
424
|
+
}
|
|
332
425
|
subscriptionKey(aid, event, route) { return route ? json([aid, route.device_id, route.slot_id, event]) : `${aid}:${event}`; }
|
|
333
|
-
async subscribe(sender, request) { const
|
|
426
|
+
async subscribe(sender, request) { const filter = publisherFilter(request), lease = this.lease(sender, request.lease_id), events = this.events(request, lease); const additions = events.map(name => { const key = this.subscriptionKey(sender, name, lease.route); return { name, key, size: new TextEncoder().encode(key + sender + lease.lease_id + json(filter)).byteLength + 128 }; }); const added = additions.filter(item => !this.subscriptions.has(item.key)); const bytes = additions.reduce((sum, item) => sum + item.size - (this.subscriptions.get(item.key)?.size ?? 0), 0); if (this.subscriptions.size + added.length > this.limit('subscription_items', 16384) || this.subscriptionBytes + bytes > this.limit('subscription_bytes', 8 * 1024 * 1024))
|
|
334
427
|
throw new PubSubError('OVERLOADED'); for (const item of additions) {
|
|
335
|
-
const sub = { leaseId: lease.lease_id, generation: ++this.generation, aid: sender, event: item.name, size: item.size };
|
|
428
|
+
const sub = { leaseId: lease.lease_id, generation: ++this.generation, aid: sender, event: item.name, size: item.size, filter };
|
|
336
429
|
this.subscriptionBytes += item.size - (this.subscriptions.get(item.key)?.size ?? 0);
|
|
337
430
|
this.subscriptions.set(item.key, sub);
|
|
338
431
|
let view = this.subscriptionsByEvent.get(item.name);
|
|
@@ -343,14 +436,14 @@ export class EventService {
|
|
|
343
436
|
} return { status: 'subscribed', events }; }
|
|
344
437
|
removeSubscription(key, sub) { this.subscriptions.delete(key); this.subscriptionBytes -= sub.size; const view = this.subscriptionsByEvent.get(sub.event); view?.delete(key); if (!view?.size)
|
|
345
438
|
this.subscriptionsByEvent.delete(sub.event); this.snapshots.delete(sub.event); }
|
|
346
|
-
async unsubscribe(sender, request) { const
|
|
439
|
+
async unsubscribe(sender, request) { const lease = this.lease(sender, request.lease_id), events = this.events(request, lease, true); for (const name of events) {
|
|
347
440
|
const key = this.subscriptionKey(sender, name, lease.route), sub = this.subscriptions.get(key);
|
|
348
441
|
if (sub?.leaseId === lease.lease_id)
|
|
349
442
|
this.removeSubscription(key, sub);
|
|
350
443
|
} return { status: 'unsubscribed', events }; }
|
|
351
444
|
snapshot(event) { let snapshot = this.snapshots.get(event); if (!snapshot) {
|
|
352
|
-
const entries = [...(this.subscriptionsByEvent.get(event)?.entries() ?? [])].map(([key, sub]) => [sub.aid, { leaseId: sub.leaseId, generation: sub.generation, key }]);
|
|
353
|
-
snapshot = { entries, size: 64 + entries.reduce((sum, [target, sub]) => sum + new TextEncoder().encode(target + sub.leaseId + sub.key).byteLength + 128, 0) };
|
|
445
|
+
const entries = [...(this.subscriptionsByEvent.get(event)?.entries() ?? [])].map(([key, sub]) => [sub.aid, { leaseId: sub.leaseId, generation: sub.generation, key, filter: sub.filter }]);
|
|
446
|
+
snapshot = { entries, size: 64 + entries.reduce((sum, [target, sub]) => sum + new TextEncoder().encode(target + sub.leaseId + sub.key + json(sub.filter ?? {})).byteLength + 128, 0) };
|
|
354
447
|
this.snapshots.set(event, snapshot);
|
|
355
448
|
} return snapshot; }
|
|
356
449
|
pruneSubscriptions() { for (const [key, sub] of this.subscriptions) {
|
|
@@ -443,6 +536,140 @@ export class EventService {
|
|
|
443
536
|
} return true; }
|
|
444
537
|
counter(name) { const c = this.counters.get(name) ?? {}; for (const k of ['published', 'matched', 'submitted', 'failed', 'unconfirmed', 'deduplicated', 'rejected', 'skipped_expired', 'skipped_overloaded', 'skipped_revoked'])
|
|
445
538
|
c[k] ??= 0; this.counters.set(name, c); return c; }
|
|
539
|
+
async listingOp(sender, request, local) {
|
|
540
|
+
const generation = this.lifecycleGeneration, check = () => { if (generation !== this.lifecycleGeneration || this.closed || !this.running)
|
|
541
|
+
throw new PubSubError('SERVICE_CLOSED'); };
|
|
542
|
+
const action = request.op === 'publications' ? 'publish' : 'subscribe', lease = local ? undefined : this.lease(sender, request.lease_id);
|
|
543
|
+
let scope = local ? Object.keys(this.catalog).sort() : [...new Set(lease.scope.filter(item => item.action === action).map(item => item.event_name))].sort();
|
|
544
|
+
if (!local && !scope.length)
|
|
545
|
+
throw new PubSubError('FORBIDDEN');
|
|
546
|
+
const limit = request.limit ?? 50;
|
|
547
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
548
|
+
throw new PubSubError('INVALID_LIMIT');
|
|
549
|
+
let binding = digest([request.op, local, sender, lease?.lease_id ?? null, scope, request.event_name ?? null, this.since]), pageExpires = Infinity;
|
|
550
|
+
let last;
|
|
551
|
+
if (request.page_token !== undefined) {
|
|
552
|
+
try {
|
|
553
|
+
const envelope = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(request.page_token), value => value.charCodeAt(0))));
|
|
554
|
+
if (envelope.signature !== digest([this.tokenKey, envelope.token]) || envelope.token.binding !== binding || envelope.token.expires_at <= this.clock() || !Array.isArray(envelope.token.last) || envelope.token.last.length !== (action === 'publish' ? 2 : 4) || envelope.token.last.some((value) => typeof value !== 'string'))
|
|
555
|
+
throw new Error();
|
|
556
|
+
last = envelope.token.last;
|
|
557
|
+
pageExpires = envelope.token.expires_at;
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
throw new PubSubError('INVALID_PAGE_TOKEN');
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (action === 'subscribe') {
|
|
564
|
+
const checked = new Set();
|
|
565
|
+
for (const sub of this.subscriptions.values()) {
|
|
566
|
+
if ((!local && sub.leaseId !== lease.lease_id) || !scope.includes(sub.event) || request.event_name && sub.event !== request.event_name || checked.has(sub.leaseId))
|
|
567
|
+
continue;
|
|
568
|
+
checked.add(sub.leaseId);
|
|
569
|
+
const current = this.leases.get(sub.leaseId);
|
|
570
|
+
if (!current)
|
|
571
|
+
continue;
|
|
572
|
+
try {
|
|
573
|
+
await this.refreshProof(this.lease(current.aid, current.lease_id), request.expires_at);
|
|
574
|
+
}
|
|
575
|
+
catch (error) {
|
|
576
|
+
check();
|
|
577
|
+
if (!(error instanceof PubSubError) || !['REAUTH_REQUIRED', 'FORBIDDEN'].includes(error.code))
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
check();
|
|
581
|
+
}
|
|
582
|
+
this.pruneSubscriptions();
|
|
583
|
+
}
|
|
584
|
+
check();
|
|
585
|
+
const currentLease = local ? undefined : this.lease(sender, request.lease_id);
|
|
586
|
+
scope = local ? Object.keys(this.catalog).sort() : [...new Set(currentLease.scope.filter(item => item.action === action).map(item => item.event_name))].sort();
|
|
587
|
+
if (!local && !scope.length)
|
|
588
|
+
throw new PubSubError('FORBIDDEN');
|
|
589
|
+
const currentBinding = digest([request.op, local, sender, currentLease?.lease_id ?? null, scope, request.event_name ?? null, this.since]);
|
|
590
|
+
if (request.page_token !== undefined && (binding !== currentBinding || pageExpires <= this.clock()))
|
|
591
|
+
throw new PubSubError('INVALID_PAGE_TOKEN');
|
|
592
|
+
binding = currentBinding;
|
|
593
|
+
const key = (item) => action === 'publish' ? [String(item.event_name), String(item.publisher_aid)] : [String(item.event_name), String(item.subscriber_aid), String(item.device_id), String(item.slot_id)];
|
|
594
|
+
const compare = (a, b) => { for (let i = 0; i < a.length; i++) {
|
|
595
|
+
const left = Array.from(a[i], value => value.codePointAt(0)), right = Array.from(b[i], value => value.codePointAt(0));
|
|
596
|
+
for (let j = 0; j < Math.min(left.length, right.length); j++)
|
|
597
|
+
if (left[j] !== right[j])
|
|
598
|
+
return left[j] - right[j];
|
|
599
|
+
if (left.length !== right.length)
|
|
600
|
+
return left.length - right.length;
|
|
601
|
+
} return 0; };
|
|
602
|
+
const rows = action === 'publish' ? [...this.publicationSummaries.values()].filter(item => scope.includes(item.event_name) && (!request.event_name || item.event_name === request.event_name) && (local || item.publisher_aid === sender)).map(item => copy(item)) : [...this.subscriptions.values()].filter(sub => { const current = this.leases.get(sub.leaseId); return current && current.expires_at > this.clock() && (current.auth_mode !== 'credential' || current.authorization_until > this.clock()) && current.scope.some(item => item.event_name === sub.event && item.action === 'subscribe') && (local || sub.leaseId === currentLease.lease_id) && scope.includes(sub.event) && (!request.event_name || sub.event === request.event_name); }).map(sub => { const current = this.leases.get(sub.leaseId); return { subscriber_aid: sub.aid, event_name: sub.event, device_id: current.route?.device_id ?? '', slot_id: current.route?.slot_id ?? '', expires_at: current.expires_at }; });
|
|
603
|
+
const remaining = rows.sort((a, b) => compare(key(a), key(b))).filter(item => !last || compare(key(item), last) > 0), asOf = this.clock();
|
|
604
|
+
const result = (items) => {
|
|
605
|
+
const token = { binding, last: items.length ? key(items[items.length - 1]) : last, expires_at: Math.min(this.clock() + 60000, lease?.expires_at ?? Infinity) };
|
|
606
|
+
const next = items.length && items.length < remaining.length ? btoa(String.fromCharCode(...new TextEncoder().encode(JSON.stringify({ token, signature: digest([this.tokenKey, token]) })))) : null;
|
|
607
|
+
return { service_aid: this.service_aid, since: this.since, as_of: asOf, items, next_page_token: next };
|
|
608
|
+
};
|
|
609
|
+
const fits = (items) => new TextEncoder().encode(json({ type: 'app.pubsub.v1', op: `${request.op}.result`, service_aid: this.service_aid, request_id: request.request_id, expires_at: request.expires_at, ...(request.event_id ? { event_id: request.event_id } : {}), result: result(items) })).byteLength <= Math.min(this.limit('catalog_page_bytes', 65536), 65536);
|
|
610
|
+
const items = [];
|
|
611
|
+
for (const item of remaining.slice(0, limit)) {
|
|
612
|
+
if (!fits([...items, item])) {
|
|
613
|
+
if (!items.length)
|
|
614
|
+
throw new PubSubError('MESSAGE_TOO_LARGE');
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
items.push(item);
|
|
618
|
+
}
|
|
619
|
+
if (!fits(items))
|
|
620
|
+
throw new PubSubError('MESSAGE_TOO_LARGE');
|
|
621
|
+
return result(items);
|
|
622
|
+
}
|
|
623
|
+
waitPublisherProfile(lookup, deadline) {
|
|
624
|
+
return new Promise(resolve => { const timer = setTimeout(() => resolve(undefined), Math.max(0, deadline - this.clock())); lookup.then(profile => { clearTimeout(timer); resolve(profile); }, () => { clearTimeout(timer); resolve(undefined); }); });
|
|
625
|
+
}
|
|
626
|
+
publisherProfile(aid, deadline) {
|
|
627
|
+
const now = this.clock();
|
|
628
|
+
for (const [key, profile] of this.publisherProfiles)
|
|
629
|
+
if (profile.expiresAt <= now)
|
|
630
|
+
this.publisherProfiles.delete(key);
|
|
631
|
+
const cached = this.publisherProfiles.get(aid);
|
|
632
|
+
if (cached)
|
|
633
|
+
return Promise.resolve(cached);
|
|
634
|
+
if (deadline <= now)
|
|
635
|
+
return Promise.resolve(undefined);
|
|
636
|
+
const pending = this.publisherLookups.get(aid);
|
|
637
|
+
if (pending)
|
|
638
|
+
return this.waitPublisherProfile(pending, deadline);
|
|
639
|
+
if (!this.publisherResolver)
|
|
640
|
+
return Promise.resolve(undefined);
|
|
641
|
+
const generation = this.lifecycleGeneration;
|
|
642
|
+
const lookup = new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('publisher profile timeout')), 10000); Promise.resolve().then(() => this.publisherResolver(aid, 10000)).then(value => { clearTimeout(timer); resolve(value); }, error => { clearTimeout(timer); reject(error); }); }).then(value => {
|
|
643
|
+
const document = value;
|
|
644
|
+
if (generation !== this.lifecycleGeneration)
|
|
645
|
+
return undefined;
|
|
646
|
+
if (document?.verification?.status !== 'verified' || !document.frontmatter)
|
|
647
|
+
throw new Error('unverified publisher');
|
|
648
|
+
const profile = { ...(typeof document.frontmatter.type === 'string' ? { type: document.frontmatter.type } : {}), ...(typeof document.frontmatter.exttype === 'string' ? { exttype: document.frontmatter.exttype } : {}) };
|
|
649
|
+
if (this.publisherProfiles.size >= 1024)
|
|
650
|
+
this.publisherProfiles.delete(this.publisherProfiles.keys().next().value);
|
|
651
|
+
this.publisherProfiles.set(aid, { ...profile, expiresAt: this.clock() + 60000 });
|
|
652
|
+
return profile;
|
|
653
|
+
}).catch(() => { if (generation === this.lifecycleGeneration) {
|
|
654
|
+
if (this.publisherProfiles.size >= 1024)
|
|
655
|
+
this.publisherProfiles.delete(this.publisherProfiles.keys().next().value);
|
|
656
|
+
this.publisherProfiles.set(aid, { expiresAt: this.clock() + 5000 });
|
|
657
|
+
} return undefined; }).finally(() => { if (this.publisherLookups.get(aid) === lookup)
|
|
658
|
+
this.publisherLookups.delete(aid); });
|
|
659
|
+
this.publisherLookups.set(aid, lookup);
|
|
660
|
+
return this.waitPublisherProfile(lookup, deadline);
|
|
661
|
+
}
|
|
662
|
+
async publisherSnapshot(event, aid, deadline) {
|
|
663
|
+
const source = this.snapshot(event);
|
|
664
|
+
if (!source.entries.some(([, sub]) => Object.keys(sub.filter ?? {}).length))
|
|
665
|
+
return source;
|
|
666
|
+
const needsProfile = source.entries.some(([, sub]) => (sub.filter?.publisher_types || sub.filter?.publisher_exttypes) && (!sub.filter.publisher_aids || sub.filter.publisher_aids.some(pattern => matchPattern(pattern, aid)))), profile = needsProfile ? await this.publisherProfile(aid, deadline - 2000) : undefined;
|
|
667
|
+
const entries = this.snapshot(event).entries.filter(([, sub]) => {
|
|
668
|
+
const filter = sub.filter;
|
|
669
|
+
return (!filter?.publisher_aids || filter.publisher_aids.some(pattern => matchPattern(pattern, aid))) && (!filter?.publisher_types || typeof profile?.type === 'string' && filter.publisher_types.some(pattern => matchPattern(pattern, profile.type))) && (!filter?.publisher_exttypes || typeof profile?.exttype === 'string' && filter.publisher_exttypes.some(pattern => matchPattern(pattern, profile.exttype)));
|
|
670
|
+
});
|
|
671
|
+
return { entries, size: 64 + entries.reduce((sum, [target, sub]) => sum + new TextEncoder().encode(target + sub.leaseId + sub.key + json(sub.filter ?? {})).byteLength + 128, 0) };
|
|
672
|
+
}
|
|
446
673
|
async publish(sender, request, reservation) {
|
|
447
674
|
const event = request.event_name;
|
|
448
675
|
if (!event || !this.catalog[event])
|
|
@@ -467,7 +694,24 @@ export class EventService {
|
|
|
467
694
|
this.counter(event).deduplicated++;
|
|
468
695
|
return copy(prior.result);
|
|
469
696
|
}
|
|
470
|
-
const
|
|
697
|
+
const lifecycle = this.lifecycleGeneration, snapshotView = await this.publisherSnapshot(event, sender, deadline);
|
|
698
|
+
if (lifecycle !== this.lifecycleGeneration || !this.running)
|
|
699
|
+
throw new PubSubError('SERVICE_CLOSED');
|
|
700
|
+
this.lease(sender, request.lease_id, [{ event_name: event, action: 'publish' }]);
|
|
701
|
+
if (deadline - this.clock() < 1000)
|
|
702
|
+
throw new PubSubError('EXPIRED');
|
|
703
|
+
const duplicate = this.published.get(key);
|
|
704
|
+
if (duplicate) {
|
|
705
|
+
if (duplicate.fingerprint !== fingerprint)
|
|
706
|
+
throw new PubSubError('EVENT_CONFLICT');
|
|
707
|
+
this.counter(event).deduplicated++;
|
|
708
|
+
return copy(duplicate.result);
|
|
709
|
+
}
|
|
710
|
+
const summaryKey = json([sender, event]), previous = this.publicationSummaries.get(summaryKey), summary = { publisher_aid: sender, event_name: event, published: (previous?.published ?? 0) + 1, first_published_at: previous?.first_published_at ?? now, last_published_at: now };
|
|
711
|
+
const summaryBytes = new TextEncoder().encode(json(summary)).byteLength, previousBytes = previous ? new TextEncoder().encode(json(previous)).byteLength : 0;
|
|
712
|
+
if (!previous && this.publicationSummaries.size >= this.limit('publication_items', 8192) || this.publicationBytes + summaryBytes - previousBytes > this.limit('publication_bytes', 4 * 1024 * 1024))
|
|
713
|
+
throw new PubSubError('OVERLOADED');
|
|
714
|
+
const size = new TextEncoder().encode(json(payload)).byteLength + 128, queue = this.fanout.get(sender) ?? [], snapshot = snapshotView.entries, snapshotSize = snapshotView.size;
|
|
471
715
|
if (size - 128 > this.limit('event_bytes', 65536) || !this.publishCapacityAvailable(size, reservation?.bytes ?? 0) || queue.length >= this.limit('accepted_per_aid', 128) || this.published.size >= this.limit('dedup_items', 8192) || (!this.snapshotRefs.has(snapshot) && (this.snapshotRefs.size >= this.limit('snapshot_items', 256) || this.snapshotBytes + snapshotSize > this.limit('snapshot_bytes', 8 * 1024 * 1024))))
|
|
472
716
|
throw new PubSubError('OVERLOADED');
|
|
473
717
|
const pin = this.snapshotRefs.get(snapshot);
|
|
@@ -492,6 +736,8 @@ export class EventService {
|
|
|
492
736
|
this.published.set(key, { fingerprint, result, expiresAt: deadline });
|
|
493
737
|
this.counter(event).published++;
|
|
494
738
|
this.capacityChanged();
|
|
739
|
+
this.publicationSummaries.set(summaryKey, summary);
|
|
740
|
+
this.publicationBytes += summaryBytes - previousBytes;
|
|
495
741
|
if (!this.fanoutTask)
|
|
496
742
|
this.fanoutTask = new Promise(resolve => setTimeout(resolve, 0)).then(() => this.runFanout()).finally(() => { this.fanoutTask = undefined; });
|
|
497
743
|
return result;
|