@agentunion/fastaun 0.5.31 → 0.5.33
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 +69 -1
- package/_packed_docs/CHANGELOG.md +69 -1
- package/_packed_docs/sdk/0.5.32-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +350 -0
- package/_packed_docs/sdk/0.5.32-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +319 -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 +316 -0
- 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 +304 -0
- package/_packed_docs/sdk/09-storage-rpc-manual.md +33 -16
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +12 -5
- 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 +459 -0
- 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 +554 -0
- package/_packed_docs/sdk/INDEX.md +27 -5
- package/_packed_docs/sdk//345/255/220AID/344/277/256/346/224/271agent.md/351/200/232/347/237/245Owner/345/234/272/346/231/257/346/214/207/345/215/227.md +234 -0
- package/dist/agent-md.d.ts +2 -1
- package/dist/agent-md.js +17 -0
- package/dist/agent-md.js.map +1 -1
- package/dist/client/control-messages.d.ts +22 -6
- package/dist/client/control-messages.js +210 -148
- package/dist/client/control-messages.js.map +1 -1
- package/dist/client/delivery.d.ts +3 -0
- package/dist/client/delivery.js +67 -3
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.d.ts +3 -0
- package/dist/client/group-state.js +48 -14
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.js +3 -2
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +9 -3
- package/dist/client/rpc-pipeline.js +80 -27
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +13 -1
- package/dist/client/v2-e2ee.js +32 -19
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +13 -5
- package/dist/client.js +121 -37
- package/dist/client.js.map +1 -1
- package/dist/group-fs.js +1 -1
- package/dist/group-fs.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/ownership-workflow.d.ts +23 -3
- package/dist/ownership-workflow.js +39 -16
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/pubsub/binding.d.ts +54 -0
- package/dist/pubsub/binding.js +343 -0
- package/dist/pubsub/binding.js.map +1 -0
- package/dist/pubsub/channel.d.ts +45 -0
- package/dist/pubsub/channel.js +137 -0
- package/dist/pubsub/channel.js.map +1 -0
- package/dist/pubsub/client.d.ts +93 -0
- package/dist/pubsub/client.js +318 -0
- package/dist/pubsub/client.js.map +1 -0
- package/dist/pubsub/credentials.d.ts +51 -0
- package/dist/pubsub/credentials.js +360 -0
- package/dist/pubsub/credentials.js.map +1 -0
- package/dist/pubsub/errors.d.ts +5 -0
- package/dist/pubsub/errors.js +6 -0
- package/dist/pubsub/errors.js.map +1 -0
- package/dist/pubsub/index.d.ts +48 -0
- package/dist/pubsub/index.js +152 -0
- package/dist/pubsub/index.js.map +1 -0
- package/dist/pubsub/scheduler.d.ts +58 -0
- package/dist/pubsub/scheduler.js +249 -0
- package/dist/pubsub/scheduler.js.map +1 -0
- package/dist/pubsub/service.d.ts +100 -0
- package/dist/pubsub/service.js +650 -0
- package/dist/pubsub/service.js.map +1 -0
- package/dist/pubsub/types.d.ts +119 -0
- package/dist/pubsub/types.js +2 -0
- package/dist/pubsub/types.js.map +1 -0
- package/dist/storage/lowlevel.d.ts +4 -2
- package/dist/storage/lowlevel.js +46 -7
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/read-cache.d.ts +7 -3
- package/dist/storage/read-cache.js +149 -18
- package/dist/storage/read-cache.js.map +1 -1
- package/dist/storage/vfs.d.ts +5 -2
- package/dist/storage/vfs.js +13 -3
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +8 -2
- package/dist/tools/cross-sdk-agent.js +81 -16
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/tools/pubsub-actor.d.ts +27 -0
- package/dist/tools/pubsub-actor.js +260 -0
- package/dist/tools/pubsub-actor.js.map +1 -0
- package/dist/transport.d.ts +25 -1
- package/dist/transport.js +175 -10
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { PubSubError } from './errors.js';
|
|
2
|
+
import { FairScheduler } from './scheduler.js';
|
|
3
|
+
import { digest } from '../org-protocol.js';
|
|
4
|
+
const copy = (v) => v === undefined ? v : JSON.parse(JSON.stringify(v));
|
|
5
|
+
const json = (v) => { if (v === null || typeof v !== 'object')
|
|
6
|
+
return JSON.stringify(v); if (Array.isArray(v))
|
|
7
|
+
return `[${v.map(json).join(',')}]`; return `{${Object.keys(v).sort().map(key => `${JSON.stringify(key)}:${json(v[key])}`).join(',')}}`; };
|
|
8
|
+
const uuid = () => crypto.randomUUID();
|
|
9
|
+
const routeKey = (route) => route ? json([route.device_id, route.slot_id]) : '';
|
|
10
|
+
export class EventService {
|
|
11
|
+
service_aid;
|
|
12
|
+
catalog;
|
|
13
|
+
allowlist;
|
|
14
|
+
send;
|
|
15
|
+
credentials;
|
|
16
|
+
clock;
|
|
17
|
+
limits;
|
|
18
|
+
state;
|
|
19
|
+
deliveryLeases = new Map();
|
|
20
|
+
lifecycleGeneration = 0;
|
|
21
|
+
generation = 0;
|
|
22
|
+
issuedGrants = new Map();
|
|
23
|
+
pausedCids = new Set();
|
|
24
|
+
tokenKey = uuid();
|
|
25
|
+
stateWrite = Promise.resolve();
|
|
26
|
+
leases = new Map();
|
|
27
|
+
leaseSizes = new Map();
|
|
28
|
+
leaseBytes = 0;
|
|
29
|
+
nextLeaseExpiry = Infinity;
|
|
30
|
+
subscriptions = new Map();
|
|
31
|
+
subscriptionBytes = 0;
|
|
32
|
+
subscriptionsByEvent = new Map();
|
|
33
|
+
requests = new Map();
|
|
34
|
+
published = new Map();
|
|
35
|
+
counters = new Map();
|
|
36
|
+
scheduler;
|
|
37
|
+
snapshots = new Map();
|
|
38
|
+
snapshotRefs = new Map();
|
|
39
|
+
snapshotBytes = 0;
|
|
40
|
+
fanout = new Map();
|
|
41
|
+
fanoutReady = [];
|
|
42
|
+
fanoutCount = 0;
|
|
43
|
+
fanoutBytes = 0;
|
|
44
|
+
fanoutTask;
|
|
45
|
+
capacityListeners = new Set();
|
|
46
|
+
fanoutWaiters = new Set();
|
|
47
|
+
capacityVersion = 0;
|
|
48
|
+
ingressBodyBytes = 0;
|
|
49
|
+
publishCapacityAvailable(size = 0, reservedBytes = 0) { return this.running && this.fanoutCount < Math.min(this.limit('accepted_items', 64), 64) && this.fanoutBytes + size <= Math.min(this.limit('accepted_bytes', 8 * 1024 * 1024), 64 * 1024 * 1024) && (this.credentials?.retainedBodyBytes?.() ?? 0) + this.ingressBodyBytes - reservedBytes + this.fanoutBytes + (this.scheduler?.retainedBytes() ?? 0) + size <= Math.min(this.limit('body_bytes', 64 * 1024 * 1024), 64 * 1024 * 1024); }
|
|
50
|
+
reserveIngressBody(size) { if ((this.credentials?.retainedBodyBytes?.() ?? 0) + this.ingressBodyBytes + this.fanoutBytes + (this.scheduler?.retainedBytes() ?? 0) + size > Math.min(this.limit('body_bytes', 64 * 1024 * 1024), 64 * 1024 * 1024))
|
|
51
|
+
return false; this.ingressBodyBytes += size; return true; }
|
|
52
|
+
releaseIngressBody(size) { this.ingressBodyBytes -= size; this.capacityChanged(); }
|
|
53
|
+
onPublishCapacity(listener) { this.capacityListeners.add(listener); return () => { this.capacityListeners.delete(listener); }; }
|
|
54
|
+
capacityChanged() { this.capacityVersion++; for (const wake of this.fanoutWaiters)
|
|
55
|
+
wake(); for (const listener of this.capacityListeners)
|
|
56
|
+
listener(); }
|
|
57
|
+
async waitFanoutCapacity(version, deadline) {
|
|
58
|
+
if (!this.running || version !== this.capacityVersion)
|
|
59
|
+
return;
|
|
60
|
+
await new Promise(resolve => { const wake = () => { clearTimeout(timer); this.fanoutWaiters.delete(wake); resolve(); }; const timer = setTimeout(wake, Math.max(0, deadline - this.clock() - 999)); this.fanoutWaiters.add(wake); });
|
|
61
|
+
}
|
|
62
|
+
starting;
|
|
63
|
+
running = false;
|
|
64
|
+
revision = '';
|
|
65
|
+
since;
|
|
66
|
+
closed = false;
|
|
67
|
+
constructor(serviceAid, catalog, allowlist, options) {
|
|
68
|
+
this.service_aid = serviceAid;
|
|
69
|
+
this.catalog = copy(catalog);
|
|
70
|
+
this.allowlist = copy(allowlist);
|
|
71
|
+
this.send = options.send;
|
|
72
|
+
this.credentials = options.credentials;
|
|
73
|
+
this.clock = options.clock ?? (() => Date.now());
|
|
74
|
+
this.limits = options.limits ?? {};
|
|
75
|
+
this.state = options.state;
|
|
76
|
+
this.since = this.clock();
|
|
77
|
+
this.credentials?.setBodyBudget?.(delta => (this.credentials?.retainedBodyBytes?.() ?? 0) + this.ingressBodyBytes + this.fanoutBytes + (this.scheduler?.retainedBytes() ?? 0) + delta <= Math.min(this.limit('body_bytes', 64 * 1024 * 1024), 64 * 1024 * 1024));
|
|
78
|
+
this.refreshRevision();
|
|
79
|
+
}
|
|
80
|
+
async start() { if (this.starting)
|
|
81
|
+
return this.starting; if (this.running)
|
|
82
|
+
return this; this.starting = this.initialize().finally(() => { this.starting = undefined; }); return this.starting; }
|
|
83
|
+
async initialize() {
|
|
84
|
+
const saved = await this.state?.load?.();
|
|
85
|
+
if (saved && saved.service_aid === this.service_aid) {
|
|
86
|
+
this.catalog = copy(saved.catalog ?? this.catalog);
|
|
87
|
+
this.allowlist = copy(saved.allowlist ?? this.allowlist);
|
|
88
|
+
this.issuedGrants = new Map(saved.issued_grants ?? []);
|
|
89
|
+
this.pausedCids = new Set(saved.paused_cids ?? []);
|
|
90
|
+
}
|
|
91
|
+
this.validateCatalog(this.catalog);
|
|
92
|
+
this.refreshRevision();
|
|
93
|
+
await this.saveState();
|
|
94
|
+
await this.credentials?.refreshRevocations?.();
|
|
95
|
+
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
|
+
this.scheduler.onCapacity(() => this.capacityChanged());
|
|
97
|
+
this.counters.clear();
|
|
98
|
+
this.since = this.clock();
|
|
99
|
+
this.closed = false;
|
|
100
|
+
this.running = true;
|
|
101
|
+
await this.scheduler.start();
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
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; }
|
|
105
|
+
async close() { if (this.starting) {
|
|
106
|
+
try {
|
|
107
|
+
await this.starting;
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
} 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; }
|
|
112
|
+
async drain() { while (this.fanoutTask)
|
|
113
|
+
await this.fanoutTask; await this.scheduler?.drain(); }
|
|
114
|
+
setSend(send) { this.send = send; }
|
|
115
|
+
async updateConfig(config) { if (config.catalog) {
|
|
116
|
+
this.validateCatalog(config.catalog);
|
|
117
|
+
this.catalog = copy(config.catalog);
|
|
118
|
+
this.refreshRevision();
|
|
119
|
+
} if (config.allowlist)
|
|
120
|
+
this.allowlist = copy(config.allowlist); for (const [id, lease] of this.leases) {
|
|
121
|
+
lease.scope = lease.scope.filter(item => !!this.catalog[item.event_name] && (lease.auth_mode !== 'allowlist' || this.allowlisted(lease.aid, item)));
|
|
122
|
+
if (!lease.scope.length)
|
|
123
|
+
this.removeLease(id);
|
|
124
|
+
} this.leaseBytes = 0; for (const [id, lease] of this.leases) {
|
|
125
|
+
const size = this.leaseSize(lease);
|
|
126
|
+
this.leaseSizes.set(id, size);
|
|
127
|
+
this.leaseBytes += size;
|
|
128
|
+
} this.pruneSubscriptions(); await this.saveState(); }
|
|
129
|
+
async handle(senderAid, request, reservation, route) {
|
|
130
|
+
try {
|
|
131
|
+
return await this.handleRequest(senderAid, request, reservation, route);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (request.op === 'publish' && request.event_name && this.catalog[request.event_name])
|
|
135
|
+
this.counter(request.event_name).rejected++;
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async _handleLocal(request) { if (!this.running)
|
|
140
|
+
throw new PubSubError('SERVICE_CLOSED'); return this.handleRequest(this.service_aid, request, undefined, undefined, true); }
|
|
141
|
+
async handleRequest(senderAid, request, reservation, route, local = false) {
|
|
142
|
+
if (this.closed)
|
|
143
|
+
throw new PubSubError('SERVICE_CLOSED');
|
|
144
|
+
this.validateRequest(senderAid, request);
|
|
145
|
+
const op = request.op;
|
|
146
|
+
this.cleanup(op !== 'publish');
|
|
147
|
+
const generation = this.lifecycleGeneration;
|
|
148
|
+
if (!local && ['issue_grant', 'revoke_grant', 'stats'].includes(op))
|
|
149
|
+
throw new PubSubError('FORBIDDEN');
|
|
150
|
+
if (route && (typeof route.device_id !== 'string' || !route.device_id || typeof route.slot_id !== 'string' || !route.slot_id))
|
|
151
|
+
throw new PubSubError('INVALID_REQUEST');
|
|
152
|
+
if (request.lease_id) {
|
|
153
|
+
const bound = this.leases.get(request.lease_id);
|
|
154
|
+
if (bound && routeKey(bound.route) !== routeKey(route))
|
|
155
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
156
|
+
}
|
|
157
|
+
if (request.lease_id && op !== 'renew')
|
|
158
|
+
await this.refreshProof(this.lease(senderAid, request.lease_id), request.expires_at);
|
|
159
|
+
if (request.lease_id && op !== 'renew')
|
|
160
|
+
this.lease(senderAid, request.lease_id);
|
|
161
|
+
if (generation !== this.lifecycleGeneration || this.closed)
|
|
162
|
+
throw new PubSubError('SERVICE_CLOSED');
|
|
163
|
+
if (op === 'publish')
|
|
164
|
+
return this.publish(senderAid, request, reservation);
|
|
165
|
+
if (op === 'catalog')
|
|
166
|
+
return this.catalogOp(senderAid, request, undefined, undefined, local);
|
|
167
|
+
if (op === 'stats')
|
|
168
|
+
return this.statsOp(senderAid, request);
|
|
169
|
+
const id = request.request_id;
|
|
170
|
+
if (!id)
|
|
171
|
+
throw new PubSubError('INVALID_REQUEST');
|
|
172
|
+
const key = `${senderAid}:${routeKey(route)}:${id}`;
|
|
173
|
+
const fp = json(request);
|
|
174
|
+
const old = this.requests.get(key);
|
|
175
|
+
if (old) {
|
|
176
|
+
if (old.fingerprint !== fp)
|
|
177
|
+
throw new PubSubError('REQUEST_CONFLICT');
|
|
178
|
+
if (old.promise)
|
|
179
|
+
return old.promise;
|
|
180
|
+
return copy(old.result);
|
|
181
|
+
}
|
|
182
|
+
if (this.requests.size >= this.limit('request_items', 4096))
|
|
183
|
+
throw new PubSubError('OVERLOADED');
|
|
184
|
+
const entry = { fingerprint: fp, expiresAt: request.expires_at ?? this.clock() + 60_000 };
|
|
185
|
+
this.requests.set(key, entry);
|
|
186
|
+
const promise = Promise.resolve().then(() => this.dispatch(senderAid, request, route));
|
|
187
|
+
entry.promise = promise;
|
|
188
|
+
try {
|
|
189
|
+
const result = await promise;
|
|
190
|
+
entry.result = copy(result);
|
|
191
|
+
delete entry.promise;
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
if (this.requests.get(key) === entry)
|
|
196
|
+
this.requests.delete(key);
|
|
197
|
+
throw e;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
limit(name, fallback) { return Number(this.limits[name] ?? fallback); }
|
|
201
|
+
cleanup(includeLeases) { const now = this.clock(); for (const [key, record] of this.requests)
|
|
202
|
+
if (record.expiresAt <= now && !record.promise)
|
|
203
|
+
this.requests.delete(key); for (const [key, record] of this.published)
|
|
204
|
+
if (record.expiresAt <= now)
|
|
205
|
+
this.published.delete(key); if (includeLeases && this.nextLeaseExpiry <= now) {
|
|
206
|
+
this.nextLeaseExpiry = Infinity;
|
|
207
|
+
for (const [key, lease] of this.leases) {
|
|
208
|
+
if (lease.expires_at <= now)
|
|
209
|
+
this.removeLease(key);
|
|
210
|
+
else
|
|
211
|
+
this.nextLeaseExpiry = Math.min(this.nextLeaseExpiry, lease.expires_at);
|
|
212
|
+
}
|
|
213
|
+
this.pruneSubscriptions();
|
|
214
|
+
} }
|
|
215
|
+
leaseSize(lease) { return new TextEncoder().encode(json(lease)).byteLength + 128; }
|
|
216
|
+
removeLease(id) { const lease = this.leases.get(id); if (lease) {
|
|
217
|
+
this.leaseBytes -= this.leaseSizes.get(id) ?? 0;
|
|
218
|
+
this.leaseSizes.delete(id);
|
|
219
|
+
this.leases.delete(id);
|
|
220
|
+
} }
|
|
221
|
+
async dispatch(sender, request, route) { switch (request.op) {
|
|
222
|
+
case 'authorize': return this.authorize(sender, request, route);
|
|
223
|
+
case 'renew': return this.renew(sender, request);
|
|
224
|
+
case 'subscribe': return this.subscribe(sender, request);
|
|
225
|
+
case 'unsubscribe': return this.unsubscribe(sender, request);
|
|
226
|
+
case 'issue_grant': return this.issueGrant(sender, request);
|
|
227
|
+
case 'revoke_grant': return this.revokeGrant(sender, request);
|
|
228
|
+
default: throw new PubSubError('INVALID_OPERATION');
|
|
229
|
+
} }
|
|
230
|
+
validateCatalog(catalog) { for (const [name, item] of Object.entries(catalog)) {
|
|
231
|
+
const d = item.default_ttl_seconds ?? 30, m = item.max_ttl_seconds ?? 3600;
|
|
232
|
+
if (!name || !Number.isInteger(d) || !Number.isInteger(m) || d < 1 || d > m || m > 3600 || new TextEncoder().encode(json({ event_name: name, ...item, allowed_actions: ['publish', 'subscribe'] })).byteLength > this.limit('catalog_item_bytes', 49152))
|
|
233
|
+
throw new PubSubError('INVALID_CATALOG');
|
|
234
|
+
} }
|
|
235
|
+
refreshRevision() { this.revision = digest(this.catalog); }
|
|
236
|
+
validateRequest(sender, request) { const now = this.clock(); if (!sender || request.type !== 'app.pubsub.v1' || (request.service_aid && request.service_aid !== this.service_aid))
|
|
237
|
+
throw new PubSubError('INVALID_REQUEST'); if ((request.expires_at ?? now + 60000) <= now || (request.issued_at ?? now) > now + 30000)
|
|
238
|
+
throw new PubSubError('EXPIRED'); }
|
|
239
|
+
scope(request) { const items = request.scope ?? (request.events ?? []).flatMap(event => (request.actions ?? []).map((action) => ({ event_name: event, action }))); if (!Array.isArray(items) || !items.length)
|
|
240
|
+
throw new PubSubError('INVALID_SCOPE'); return items.map(item => { if (!item || typeof item.event_name !== 'string' || !['publish', 'subscribe'].includes(item.action))
|
|
241
|
+
throw new PubSubError('INVALID_SCOPE'); if (!this.catalog[item.event_name])
|
|
242
|
+
throw new PubSubError('UNKNOWN_EVENT'); return { event_name: item.event_name, action: item.action }; }).filter((item, index, all) => all.findIndex(other => other.event_name === item.event_name && other.action === item.action) === index); }
|
|
243
|
+
allowlisted(aid, item) { const entries = [this.allowlist[item.event_name], this.allowlist['*']]; return entries.some(entry => { const list = item.action === 'publish' ? entry?.publish_aids : entry?.subscribe_aids; return !!list?.some(value => value === '*' || value === aid); }); }
|
|
244
|
+
leaseResult(lease) { return { lease_id: lease.lease_id, aid: lease.aid, scope: copy(lease.scope), auth_mode: lease.auth_mode, expires_at: lease.expires_at, authorization_until: lease.authorization_until, renew_interval_seconds: 3600, renew_after_seconds: 3600, ...(lease.cid ? { cid: lease.cid } : {}) }; }
|
|
245
|
+
async authorize(sender, request, route) {
|
|
246
|
+
const generation = this.lifecycleGeneration, scope = this.scope(request);
|
|
247
|
+
const mode = request.auth_mode ?? 'allowlist';
|
|
248
|
+
let expires = this.clock() + 12_000_000;
|
|
249
|
+
let cid;
|
|
250
|
+
let proofExpires = expires;
|
|
251
|
+
if (mode === 'allowlist') {
|
|
252
|
+
if (scope.some(item => !this.allowlisted(sender, item)))
|
|
253
|
+
throw new PubSubError('FORBIDDEN');
|
|
254
|
+
}
|
|
255
|
+
else if (mode === 'credential') {
|
|
256
|
+
const grant = request.grant ?? request.credential;
|
|
257
|
+
if (!this.credentials || !grant)
|
|
258
|
+
throw new PubSubError('CREDENTIAL_UNAVAILABLE');
|
|
259
|
+
const verified = await this.credentials.verify(grant, sender, scope, request, request.expires_at);
|
|
260
|
+
expires = Math.min(expires, Number(verified.expires_at ?? expires));
|
|
261
|
+
cid = String(verified.cid ?? grant.cid ?? '');
|
|
262
|
+
if (this.pausedCids.has(cid) || this.credentials.isRevoked?.(grant))
|
|
263
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
264
|
+
proofExpires = Number(verified.proof_expires_at ?? expires);
|
|
265
|
+
}
|
|
266
|
+
else
|
|
267
|
+
throw new PubSubError('INVALID_AUTH_MODE');
|
|
268
|
+
if (generation !== this.lifecycleGeneration || this.closed)
|
|
269
|
+
throw new PubSubError('SERVICE_CLOSED');
|
|
270
|
+
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 } : {}) };
|
|
272
|
+
const size = this.leaseSize(lease);
|
|
273
|
+
if (this.leases.size >= this.limit('lease_items', 16384) || this.leaseBytes + size > this.limit('lease_bytes', 8 * 1024 * 1024))
|
|
274
|
+
throw new PubSubError('OVERLOADED');
|
|
275
|
+
const result = this.leaseResult(lease);
|
|
276
|
+
result.catalog = this.catalogOp(sender, { ...request, event_name: undefined, limit: undefined, page_token: undefined }, lease, result);
|
|
277
|
+
this.leases.set(lease.lease_id, lease);
|
|
278
|
+
this.leaseSizes.set(lease.lease_id, size);
|
|
279
|
+
this.leaseBytes += size;
|
|
280
|
+
this.nextLeaseExpiry = Math.min(this.nextLeaseExpiry, expires);
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
lease(sender, id, needed = []) { if (typeof id !== 'string')
|
|
284
|
+
throw new PubSubError('REAUTH_REQUIRED'); const lease = this.leases.get(id); const now = this.clock(); if (!lease || lease.aid !== sender || lease.expires_at <= now || !lease.scope.length || (lease.cid && (this.pausedCids.has(lease.cid) || this.credentials?.isRevoked?.(lease.grant ?? lease.cid))) || needed.some(item => !lease.scope.some(x => x.event_name === item.event_name && x.action === item.action)))
|
|
285
|
+
throw new PubSubError('REAUTH_REQUIRED'); if (lease.auth_mode === 'allowlist' && (needed.length ? needed : lease.scope).some(item => !this.allowlisted(sender, item)))
|
|
286
|
+
throw new PubSubError('FORBIDDEN'); return lease; }
|
|
287
|
+
async refreshProof(lease, deadline) {
|
|
288
|
+
if (lease.auth_mode !== 'credential')
|
|
289
|
+
return;
|
|
290
|
+
try {
|
|
291
|
+
await this.credentials?.assertNotRevoked?.(lease.grant ?? lease.cid, deadline);
|
|
292
|
+
if (lease.authorization_until > this.clock()) {
|
|
293
|
+
if (lease.grant)
|
|
294
|
+
await this.credentials?.checkVerified?.(lease.grant, lease.aid, lease.scope);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!this.credentials || !lease.grant)
|
|
298
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
299
|
+
const verified = await this.credentials.verify(lease.grant, lease.aid, lease.scope, undefined, deadline);
|
|
300
|
+
if (this.leases.get(lease.lease_id) !== lease || lease.cid && (this.pausedCids.has(lease.cid) || this.credentials.isRevoked?.(lease.grant ?? lease.cid)))
|
|
301
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
302
|
+
lease.expires_at = Math.min(lease.expires_at, Number(verified.expires_at));
|
|
303
|
+
lease.authorization_until = Math.min(lease.expires_at, Number(verified.proof_expires_at ?? this.clock()));
|
|
304
|
+
if (lease.authorization_until <= this.clock())
|
|
305
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (error instanceof PubSubError && error.code === 'TIMEOUT')
|
|
309
|
+
throw error;
|
|
310
|
+
this.removeLease(lease.lease_id);
|
|
311
|
+
this.pruneSubscriptions();
|
|
312
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
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') {
|
|
317
|
+
grant = request.grant ?? old.grant;
|
|
318
|
+
if (!this.credentials || !grant)
|
|
319
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
320
|
+
await this.credentials.assertNotRevoked?.(grant, request.expires_at);
|
|
321
|
+
const verified = await this.credentials.verify(grant, sender, old.scope, request, request.expires_at);
|
|
322
|
+
expires = Math.min(expires, Number(verified.expires_at));
|
|
323
|
+
proofExpires = Number(verified.proof_expires_at ?? expires);
|
|
324
|
+
cid = String(verified.cid ?? grant.cid ?? '');
|
|
325
|
+
if (Math.min(expires, proofExpires) <= now || this.pausedCids.has(cid) || this.credentials.isRevoked?.(grant))
|
|
326
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
327
|
+
} 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))
|
|
329
|
+
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) { const names = request.events ?? (request.event_name ? [request.event_name] : []); if (!names.length || names.some(name => typeof name !== 'string' || !this.catalog[name]))
|
|
331
|
+
throw new PubSubError('UNKNOWN_EVENT'); return [...new Set(names)]; }
|
|
332
|
+
subscriptionKey(aid, event, route) { return route ? json([aid, route.device_id, route.slot_id, event]) : `${aid}:${event}`; }
|
|
333
|
+
async subscribe(sender, request) { const events = this.events(request); const lease = this.lease(sender, request.lease_id, events.map(event_name => ({ event_name, action: 'subscribe' }))); 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).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
|
+
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 };
|
|
336
|
+
this.subscriptionBytes += item.size - (this.subscriptions.get(item.key)?.size ?? 0);
|
|
337
|
+
this.subscriptions.set(item.key, sub);
|
|
338
|
+
let view = this.subscriptionsByEvent.get(item.name);
|
|
339
|
+
if (!view)
|
|
340
|
+
this.subscriptionsByEvent.set(item.name, view = new Map());
|
|
341
|
+
view.set(item.key, sub);
|
|
342
|
+
this.snapshots.delete(item.name);
|
|
343
|
+
} return { status: 'subscribed', events }; }
|
|
344
|
+
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
|
+
this.subscriptionsByEvent.delete(sub.event); this.snapshots.delete(sub.event); }
|
|
346
|
+
async unsubscribe(sender, request) { const events = this.events(request); const lease = this.lease(sender, request.lease_id, events.map(event_name => ({ event_name, action: 'subscribe' }))); for (const name of events) {
|
|
347
|
+
const key = this.subscriptionKey(sender, name, lease.route), sub = this.subscriptions.get(key);
|
|
348
|
+
if (sub?.leaseId === lease.lease_id)
|
|
349
|
+
this.removeSubscription(key, sub);
|
|
350
|
+
} return { status: 'unsubscribed', events }; }
|
|
351
|
+
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) };
|
|
354
|
+
this.snapshots.set(event, snapshot);
|
|
355
|
+
} return snapshot; }
|
|
356
|
+
pruneSubscriptions() { for (const [key, sub] of this.subscriptions) {
|
|
357
|
+
const aid = sub.aid, event = sub.event;
|
|
358
|
+
const lease = this.leases.get(sub.leaseId);
|
|
359
|
+
if (!lease || lease.expires_at <= this.clock() || (lease.cid && (this.pausedCids.has(lease.cid) || this.credentials?.isRevoked?.(lease.grant ?? lease.cid))) || !lease.scope.some(x => x.event_name === event && x.action === 'subscribe') || (lease.auth_mode === 'allowlist' && !this.allowlisted(aid, { event_name: event, action: 'subscribe' })))
|
|
360
|
+
this.removeSubscription(key, sub);
|
|
361
|
+
} }
|
|
362
|
+
async issueGrant(sender, request) { if (!this.credentials || !request.subject_aid)
|
|
363
|
+
throw new PubSubError('CREDENTIAL_UNAVAILABLE'); const scope = this.scope(request); const expires = Number(request.grant_expires_at ?? request.expires_at); if (!Number.isInteger(expires) || expires <= this.clock())
|
|
364
|
+
throw new PubSubError('INVALID_REQUEST'); const operation = digest([sender, request.request_id]), fingerprint = json([request.subject_aid, scope, expires]); const prior = [...this.issuedGrants.values()].find(grant => grant.operation === operation); if (prior) {
|
|
365
|
+
if (prior.fingerprint !== fingerprint)
|
|
366
|
+
throw new PubSubError('REQUEST_CONFLICT');
|
|
367
|
+
return copy(prior.result);
|
|
368
|
+
} const result = await this.credentials.issue(request.subject_aid, scope, expires, operation); if (typeof result.cid !== 'string')
|
|
369
|
+
throw new PubSubError('CREDENTIAL_UNAVAILABLE'); this.issuedGrants.set(result.cid, { scope, operation, fingerprint, result: copy(result) }); await this.saveState(); return result; }
|
|
370
|
+
async revokeGrant(sender, request) { if (!this.credentials?.revoke || typeof request.cid !== 'string')
|
|
371
|
+
throw new PubSubError('CREDENTIAL_UNAVAILABLE'); const scope = this.issuedGrants.get(request.cid)?.scope ?? await this.credentials.getIssuedScope?.(request.cid); if (!scope)
|
|
372
|
+
throw new PubSubError('FORBIDDEN'); this.pausedCids.add(request.cid); for (const [id, lease] of this.leases)
|
|
373
|
+
if (lease.cid === request.cid)
|
|
374
|
+
this.removeLease(id); this.pruneSubscriptions(); await this.saveState(); const result = await this.credentials.revoke(request.cid, digest([sender, request.request_id])); await this.saveState(); return result; }
|
|
375
|
+
visible(sender, request) { const lease = this.lease(sender, request.lease_id); return [...new Set(lease.scope.map(x => x.event_name))]; }
|
|
376
|
+
catalogOp(sender, request, leaseView, authorizeResult, local = false) {
|
|
377
|
+
let token;
|
|
378
|
+
if (request.page_token) {
|
|
379
|
+
try {
|
|
380
|
+
const envelope = JSON.parse(atob(request.page_token));
|
|
381
|
+
if (envelope.signature !== digest([this.tokenKey, envelope.token]))
|
|
382
|
+
throw new Error();
|
|
383
|
+
token = envelope.token;
|
|
384
|
+
if (token.aid !== sender || token.event !== digest(request.event_name ?? null) || token.expires_at <= this.clock() || !Number.isInteger(token.offset) || token.offset < 0 || token.lease_id !== undefined && (typeof token.lease_id !== 'string' || token.lease_id !== request.lease_id))
|
|
385
|
+
throw new Error();
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
throw new PubSubError('INVALID_PAGE_TOKEN');
|
|
389
|
+
}
|
|
390
|
+
if (token.revision !== this.revision)
|
|
391
|
+
throw new PubSubError('CATALOG_CHANGED');
|
|
392
|
+
if (token.lease_id !== undefined)
|
|
393
|
+
leaseView = this.lease(sender, token.lease_id);
|
|
394
|
+
}
|
|
395
|
+
const visible = leaseView ? [...new Set(leaseView.scope.map(item => item.event_name))] : local ? Object.keys(this.catalog) : this.visible(sender, request);
|
|
396
|
+
if (request.event_name && !visible.includes(request.event_name))
|
|
397
|
+
throw new PubSubError('FORBIDDEN');
|
|
398
|
+
const names = visible.filter(name => !!this.catalog[name] && (!request.event_name || name === request.event_name)).sort();
|
|
399
|
+
const limit = request.limit ?? 50;
|
|
400
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
401
|
+
throw new PubSubError('INVALID_LIMIT');
|
|
402
|
+
const actions = Object.fromEntries(names.map(name => [name, !leaseView && local ? ['publish', 'subscribe'] : (leaseView ?? this.lease(sender, request.lease_id)).scope.filter(x => x.event_name === name).map(x => x.action).sort()]));
|
|
403
|
+
const binding = digest(actions), offset = token?.offset ?? 0;
|
|
404
|
+
if (token && (token.scope !== binding || offset > names.length))
|
|
405
|
+
throw new PubSubError('INVALID_PAGE_TOKEN');
|
|
406
|
+
const result = (items) => { const token = { aid: sender, scope: binding, event: digest(request.event_name ?? null), revision: this.revision, offset: offset + items.length, expires_at: Math.min(this.clock() + 60_000, leaseView?.expires_at ?? Infinity), ...(leaseView ? { lease_id: leaseView.lease_id } : {}) }; const next = offset + items.length < names.length ? btoa(JSON.stringify({ token, signature: digest([this.tokenKey, token]) })) : undefined; return { service_aid: this.service_aid, catalog_revision: this.revision, revision: this.revision, items, ...(next ? { next_page_token: next } : {}) }; };
|
|
407
|
+
const fits = (items) => { const page = result(items); return (!authorizeResult || new TextEncoder().encode(json(page)).byteLength <= 2048) && 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: authorizeResult ? { ...authorizeResult, catalog: page } : page })).byteLength <= Math.min(this.limit('catalog_page_bytes', 65536), 65536); };
|
|
408
|
+
const items = [];
|
|
409
|
+
for (const name of names.slice(offset, offset + limit)) {
|
|
410
|
+
const item = { event_name: name, ...copy(this.catalog[name]), allowed_actions: actions[name] };
|
|
411
|
+
if (!fits([...items, item])) {
|
|
412
|
+
if (!items.length && !authorizeResult)
|
|
413
|
+
throw new PubSubError('OVERLOADED');
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
items.push(item);
|
|
417
|
+
}
|
|
418
|
+
if (!fits(items))
|
|
419
|
+
throw new PubSubError('OVERLOADED');
|
|
420
|
+
return result(items);
|
|
421
|
+
}
|
|
422
|
+
validateContent(data, schema) { if (!schema || typeof schema !== 'object')
|
|
423
|
+
throw new PubSubError('INVALID_SCHEMA'); if (schema.type) {
|
|
424
|
+
const ok = schema.type === 'string' ? typeof data === 'string' : schema.type === 'number' ? typeof data === 'number' && !Number.isNaN(data) : schema.type === 'integer' ? Number.isInteger(data) : schema.type === 'boolean' ? typeof data === 'boolean' : schema.type === 'object' ? !!data && typeof data === 'object' && !Array.isArray(data) : schema.type === 'array' ? Array.isArray(data) : false;
|
|
425
|
+
if (!ok)
|
|
426
|
+
throw new PubSubError('INVALID_CONTENT');
|
|
427
|
+
if (schema.type === 'object')
|
|
428
|
+
for (const key of schema.required ?? [])
|
|
429
|
+
if (!(key in data))
|
|
430
|
+
throw new PubSubError('INVALID_CONTENT');
|
|
431
|
+
if (schema.type === 'object')
|
|
432
|
+
for (const [key, child] of Object.entries(schema.properties ?? {}))
|
|
433
|
+
if (key in data)
|
|
434
|
+
this.validateContent(data[key], child);
|
|
435
|
+
if (schema.type === 'array' && schema.items)
|
|
436
|
+
for (const item of data)
|
|
437
|
+
this.validateContent(item, schema.items);
|
|
438
|
+
return true;
|
|
439
|
+
} for (const [key, value] of Object.entries(schema)) {
|
|
440
|
+
if (!data || typeof data !== 'object' || Array.isArray(data) || !(key in data))
|
|
441
|
+
throw new PubSubError('INVALID_CONTENT');
|
|
442
|
+
this.validateContent(data[key], typeof value === 'string' ? { type: value } : value);
|
|
443
|
+
} return true; }
|
|
444
|
+
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
|
+
c[k] ??= 0; this.counters.set(name, c); return c; }
|
|
446
|
+
async publish(sender, request, reservation) {
|
|
447
|
+
const event = request.event_name;
|
|
448
|
+
if (!event || !this.catalog[event])
|
|
449
|
+
throw new PubSubError('UNKNOWN_EVENT');
|
|
450
|
+
const lease = this.lease(sender, request.lease_id, [{ event_name: event, action: 'publish' }]);
|
|
451
|
+
const entry = this.catalog[event];
|
|
452
|
+
if (request.schema_version !== (entry.schema_version ?? 1))
|
|
453
|
+
throw new PubSubError('UNSUPPORTED_SCHEMA_VERSION');
|
|
454
|
+
if (!request.data || typeof request.data !== 'object' || Array.isArray(request.data))
|
|
455
|
+
throw new PubSubError('INVALID_CONTENT');
|
|
456
|
+
this.validateContent(request.data, entry.content_schema ?? {});
|
|
457
|
+
const now = this.clock(), deadline = request.expires_at;
|
|
458
|
+
if (!Number.isInteger(deadline) || deadline - now < 1000 || deadline - now > (entry.max_ttl_seconds ?? 3600) * 1000)
|
|
459
|
+
throw new PubSubError('INVALID_TTL');
|
|
460
|
+
if (!request.event_id || !['none', 'accepted'].includes(request.reply_mode ?? 'none'))
|
|
461
|
+
throw new PubSubError('INVALID_REQUEST');
|
|
462
|
+
const payload = { type: 'app.pubsub.v1', op: 'event', event_id: request.event_id, event_name: event, publisher_aid: sender, schema_version: request.schema_version, expires_at: deadline, data: reservation ? request.data : copy(request.data) };
|
|
463
|
+
const key = `${sender}:${request.event_id}`, fingerprint = digest(payload), prior = this.published.get(key);
|
|
464
|
+
if (prior) {
|
|
465
|
+
if (prior.fingerprint !== fingerprint)
|
|
466
|
+
throw new PubSubError('EVENT_CONFLICT');
|
|
467
|
+
this.counter(event).deduplicated++;
|
|
468
|
+
return copy(prior.result);
|
|
469
|
+
}
|
|
470
|
+
const size = new TextEncoder().encode(json(payload)).byteLength + 128, queue = this.fanout.get(sender) ?? [], snapshotView = this.snapshot(event), snapshot = snapshotView.entries, snapshotSize = snapshotView.size;
|
|
471
|
+
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
|
+
throw new PubSubError('OVERLOADED');
|
|
473
|
+
const pin = this.snapshotRefs.get(snapshot);
|
|
474
|
+
if (pin)
|
|
475
|
+
pin.refs++;
|
|
476
|
+
else {
|
|
477
|
+
this.snapshotRefs.set(snapshot, { refs: 1, size: snapshotSize });
|
|
478
|
+
this.snapshotBytes += snapshotSize;
|
|
479
|
+
}
|
|
480
|
+
if (reservation) {
|
|
481
|
+
this.ingressBodyBytes -= reservation.bytes;
|
|
482
|
+
reservation.bytes = 0;
|
|
483
|
+
}
|
|
484
|
+
const result = { status: 'accepted', event_id: request.event_id };
|
|
485
|
+
if (!this.fanout.has(sender)) {
|
|
486
|
+
this.fanout.set(sender, queue);
|
|
487
|
+
this.fanoutReady.push(sender);
|
|
488
|
+
}
|
|
489
|
+
queue.push({ publisher: sender, payload, publisherLease: lease.lease_id, snapshot, size, cursor: 0 });
|
|
490
|
+
this.fanoutCount++;
|
|
491
|
+
this.fanoutBytes += size;
|
|
492
|
+
this.published.set(key, { fingerprint, result, expiresAt: deadline });
|
|
493
|
+
this.counter(event).published++;
|
|
494
|
+
this.capacityChanged();
|
|
495
|
+
if (!this.fanoutTask)
|
|
496
|
+
this.fanoutTask = new Promise(resolve => setTimeout(resolve, 0)).then(() => this.runFanout()).finally(() => { this.fanoutTask = undefined; });
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
async runFanout() {
|
|
500
|
+
let stalled = 0, blockedDeadline = Infinity;
|
|
501
|
+
while (this.running && this.fanoutReady.length) {
|
|
502
|
+
const version = this.capacityVersion;
|
|
503
|
+
let blocked = false;
|
|
504
|
+
const publisher = this.fanoutReady.shift(), queue = this.fanout.get(publisher), work = queue[0], event = String(work.payload.event_name);
|
|
505
|
+
const stop = Math.min(work.snapshot.length, work.cursor + Math.max(1, this.limit('fanout_batch', 32)));
|
|
506
|
+
while (this.running && work.cursor < stop) {
|
|
507
|
+
const [target, sub] = work.snapshot[work.cursor], lease = this.leases.get(sub.leaseId), current = this.subscriptions.get(sub.key);
|
|
508
|
+
const counter = this.counter(event);
|
|
509
|
+
if (!lease || lease.expires_at <= this.clock() || current?.leaseId !== sub.leaseId || current.generation !== sub.generation) {
|
|
510
|
+
counter.matched++;
|
|
511
|
+
counter.skipped_revoked++;
|
|
512
|
+
work.cursor++;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const deadline = Math.min(Number(work.payload.expires_at), lease.expires_at), payload = { ...work.payload, expires_at: deadline };
|
|
516
|
+
if (deadline - this.clock() < 1000) {
|
|
517
|
+
counter.matched++;
|
|
518
|
+
counter.skipped_expired++;
|
|
519
|
+
work.cursor++;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (this.scheduler?.capacityReason(target, publisher, payload) === 'global' || (this.credentials?.retainedBodyBytes?.() ?? 0) + this.ingressBodyBytes + this.fanoutBytes + (this.scheduler?.retainedBytes() ?? 0) + new TextEncoder().encode(JSON.stringify(payload)).byteLength + (this.limits.cost_overhead ?? 128) > Math.min(this.limit('body_bytes', 64 * 1024 * 1024), 64 * 1024 * 1024)) {
|
|
523
|
+
blocked = true;
|
|
524
|
+
blockedDeadline = Math.min(blockedDeadline, deadline);
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
const messageId = `${this.service_aid}:${publisher}:${work.payload.event_id}:${target}${lease.route ? `:${digest(routeKey(lease.route))}` : ''}`;
|
|
528
|
+
counter.matched++;
|
|
529
|
+
this.deliveryLeases.set(messageId, { subscriber: sub.leaseId, publisher: work.publisherLease, generation: sub.generation, key: sub.key });
|
|
530
|
+
if (!this.scheduler?.offer(target, publisher, event, payload, deadline, messageId)) {
|
|
531
|
+
this.deliveryLeases.delete(messageId);
|
|
532
|
+
counter.skipped_overloaded++;
|
|
533
|
+
}
|
|
534
|
+
work.cursor++;
|
|
535
|
+
}
|
|
536
|
+
if (work.cursor === work.snapshot.length) {
|
|
537
|
+
queue.shift();
|
|
538
|
+
this.fanoutCount--;
|
|
539
|
+
this.fanoutBytes -= work.size;
|
|
540
|
+
const pin = this.snapshotRefs.get(work.snapshot);
|
|
541
|
+
if (!--pin.refs) {
|
|
542
|
+
this.snapshotRefs.delete(work.snapshot);
|
|
543
|
+
this.snapshotBytes -= pin.size;
|
|
544
|
+
}
|
|
545
|
+
this.capacityChanged();
|
|
546
|
+
}
|
|
547
|
+
if (queue.length)
|
|
548
|
+
this.fanoutReady.push(publisher);
|
|
549
|
+
else
|
|
550
|
+
this.fanout.delete(publisher);
|
|
551
|
+
stalled = blocked ? stalled + 1 : 0;
|
|
552
|
+
if (stalled && stalled >= this.fanoutReady.length) {
|
|
553
|
+
const deadline = Math.min(blockedDeadline, ...this.fanoutReady.map(aid => Number(this.fanout.get(aid)[0].payload.expires_at)));
|
|
554
|
+
await this.waitFanoutCapacity(version, deadline);
|
|
555
|
+
stalled = 0;
|
|
556
|
+
blockedDeadline = Infinity;
|
|
557
|
+
}
|
|
558
|
+
else
|
|
559
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async deliver(task) {
|
|
563
|
+
const leases = this.deliveryLeases.get(task.message_id), counter = this.counter(task.event_name);
|
|
564
|
+
let unknown = false;
|
|
565
|
+
let deadline = task.deadline;
|
|
566
|
+
try {
|
|
567
|
+
for (let attempt = 0; attempt < this.limit('send_attempts', 2); attempt++) {
|
|
568
|
+
if (!this.running)
|
|
569
|
+
return;
|
|
570
|
+
let subscriber;
|
|
571
|
+
try {
|
|
572
|
+
const current = this.subscriptions.get(leases?.key ?? '');
|
|
573
|
+
if (!leases || current?.leaseId !== leases.subscriber || current.generation !== leases.generation)
|
|
574
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
575
|
+
await this.refreshProof(this.lease(task.publisher, leases.publisher, [{ event_name: task.event_name, action: 'publish' }]), deadline);
|
|
576
|
+
subscriber = this.lease(task.target, leases.subscriber, [{ event_name: task.event_name, action: 'subscribe' }]);
|
|
577
|
+
await this.refreshProof(subscriber, deadline);
|
|
578
|
+
this.lease(task.publisher, leases.publisher);
|
|
579
|
+
this.lease(task.target, leases.subscriber);
|
|
580
|
+
const checked = this.subscriptions.get(leases?.key ?? '');
|
|
581
|
+
if (checked?.leaseId !== leases.subscriber || checked.generation !== leases.generation)
|
|
582
|
+
throw new PubSubError('REAUTH_REQUIRED');
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
if (!unknown) {
|
|
586
|
+
if (deadline <= this.clock())
|
|
587
|
+
counter.skipped_expired++;
|
|
588
|
+
else
|
|
589
|
+
counter.skipped_revoked++;
|
|
590
|
+
}
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
deadline = Math.min(deadline, subscriber.expires_at);
|
|
594
|
+
const ttl = Math.floor((deadline - this.clock()) / 1000);
|
|
595
|
+
if (ttl < 1) {
|
|
596
|
+
if (!unknown)
|
|
597
|
+
counter.skipped_expired++;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const sendDeadline = Math.min(deadline, this.clock() + this.limit('send_timeout_seconds', 10) * 1000);
|
|
601
|
+
try {
|
|
602
|
+
const result = await this.send(task.target, { ...task.payload, expires_at: deadline }, ttl, task.message_id, true, sendDeadline, subscriber.route);
|
|
603
|
+
if (result?.storage && result.storage !== 'volatile')
|
|
604
|
+
throw new Error('unexpected message storage');
|
|
605
|
+
if (unknown)
|
|
606
|
+
counter.unconfirmed--;
|
|
607
|
+
counter.submitted++;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
const e = error;
|
|
612
|
+
const code = String(e?.stringCode || e?.code || e?.name || '');
|
|
613
|
+
if ((['TIMEOUT', 'CONNECTION_ERROR', 'AUNTimeoutError', 'AUNConnectionError', 'TimeoutError', 'ConnectionError'].includes(code) || ['TimeoutError', 'ConnectionError'].includes(e?.name)) || /timeout|connection/i.test(String(e?.message ?? ''))) {
|
|
614
|
+
if (!unknown) {
|
|
615
|
+
counter.unconfirmed++;
|
|
616
|
+
unknown = true;
|
|
617
|
+
}
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (unknown)
|
|
621
|
+
counter.unconfirmed--;
|
|
622
|
+
counter.failed++;
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
finally {
|
|
628
|
+
this.deliveryLeases.delete(task.message_id);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async statsOp(sender, request) {
|
|
632
|
+
for (const lease of this.leases.values()) {
|
|
633
|
+
try {
|
|
634
|
+
await this.refreshProof(this.lease(lease.aid, lease.lease_id), request.expires_at);
|
|
635
|
+
}
|
|
636
|
+
catch { }
|
|
637
|
+
}
|
|
638
|
+
this.pruneSubscriptions();
|
|
639
|
+
const allowed = Object.keys(this.catalog);
|
|
640
|
+
if (request.event_name && !allowed.includes(request.event_name))
|
|
641
|
+
throw new PubSubError('FORBIDDEN');
|
|
642
|
+
const names = request.event_name ? [request.event_name] : allowed, scope = new Set(names);
|
|
643
|
+
const counts = {};
|
|
644
|
+
for (const key of ['published', 'matched', 'submitted', 'failed', 'unconfirmed', 'deduplicated', 'rejected', 'skipped_expired', 'skipped_overloaded', 'skipped_revoked'])
|
|
645
|
+
counts[key] = names.reduce((n, name) => n + (this.counter(name)[key] ?? 0), 0);
|
|
646
|
+
const subscriptions = [...this.subscriptions.values()].filter(sub => scope.has(sub.event)), leases = [...this.leases.values()].filter(lease => lease.expires_at > this.clock() && (!lease.cid || !this.pausedCids.has(lease.cid) && !this.credentials?.isRevoked?.(lease.grant ?? lease.cid)) && lease.scope.some(item => scope.has(item.event_name))), fanout = [...this.fanout.values()].flat().filter(work => scope.has(String(work.payload.event_name))), queued = this.scheduler?.queuedCounts(scope);
|
|
647
|
+
return { service_aid: this.service_aid, scope: names.sort(), since: this.since, as_of: this.clock(), counts: { ...counts, active_publishers: new Set(leases.filter(lease => lease.scope.some(item => item.action === 'publish' && scope.has(item.event_name))).map(lease => lease.aid)).size, active_subscribers: new Set(subscriptions.map(sub => sub.aid)).size, active_subscriptions: subscriptions.length, active_leases: leases.length, ...(queued ?? {}), queued_events: fanout.length, queued_bytes: (queued?.queued_bytes ?? 0) + fanout.reduce((sum, work) => sum + work.size, 0) + [...new Set(fanout.map(work => work.snapshot))].reduce((sum, snapshot) => sum + (this.snapshotRefs.get(snapshot)?.size ?? 0), 0) } };
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
//# sourceMappingURL=service.js.map
|