amalgm 0.1.155 → 0.1.156

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.
@@ -0,0 +1,518 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Cloud-shared resource synchronization over the existing event tunnel.
5
+ *
6
+ * This module owns only the shared-resource lifecycle: durable local claims,
7
+ * cloud acknowledgements, recipient materialization, and reconnect hydration.
8
+ * The parent tunnel owns WebSocket transport, routing, and backpressure.
9
+ */
10
+
11
+ const crypto = require('crypto');
12
+ const http = require('http');
13
+
14
+ const CAPABILITY = 'shared.mutation.v1';
15
+ const CLAIM_LIMIT = 16;
16
+ const CLAIM_LEASE_MS = 30_000;
17
+ const RETRY_MS = 1_000;
18
+
19
+ function createSharedRealtimeTunnel({
20
+ send,
21
+ sendLarge,
22
+ localPort,
23
+ localHeaders,
24
+ warn,
25
+ }) {
26
+ const mutationInflight = new Map();
27
+ const promotionInflight = new Map();
28
+ const attachmentInflight = new Map();
29
+ const inboundChains = new Map();
30
+
31
+ let enabled = false;
32
+ let mutationDrain = null;
33
+ let promotionDrain = null;
34
+ let attachmentDrain = null;
35
+ let wakeRequest = null;
36
+ let wakeRetryTimer = null;
37
+ let mutationRetryTimer = null;
38
+ let promotionRetryTimer = null;
39
+ let attachmentRetryTimer = null;
40
+ let localFailures = 0;
41
+
42
+ function localStateJson(method, requestPath, body) {
43
+ const payload = body === undefined ? null : Buffer.from(JSON.stringify(body));
44
+ return new Promise((resolve, reject) => {
45
+ const headers = localHeaders({ accept: 'application/json' });
46
+ if (payload) {
47
+ headers['content-type'] = 'application/json';
48
+ headers['content-length'] = String(payload.length);
49
+ }
50
+ const req = http.request({
51
+ hostname: '127.0.0.1',
52
+ port: localPort(),
53
+ method,
54
+ path: requestPath,
55
+ headers,
56
+ }, (res) => {
57
+ const chunks = [];
58
+ res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
59
+ res.on('end', () => {
60
+ const text = Buffer.concat(chunks).toString('utf8');
61
+ let parsed = null;
62
+ try {
63
+ parsed = text ? JSON.parse(text) : null;
64
+ } catch {
65
+ reject(new Error(`local state returned invalid JSON (${res.statusCode || 502})`));
66
+ return;
67
+ }
68
+ if ((res.statusCode || 500) >= 400) {
69
+ const error = new Error(parsed?.error || `local state failed with ${res.statusCode || 500}`);
70
+ error.statusCode = res.statusCode || 500;
71
+ error.details = parsed;
72
+ reject(error);
73
+ return;
74
+ }
75
+ resolve(parsed);
76
+ });
77
+ });
78
+ req.on('error', reject);
79
+ if (payload) req.write(payload);
80
+ req.end();
81
+ });
82
+ }
83
+
84
+ function scheduleMutationDrain(delay = 0) {
85
+ if (!enabled || mutationRetryTimer) return;
86
+ mutationRetryTimer = setTimeout(() => {
87
+ mutationRetryTimer = null;
88
+ void drainMutations();
89
+ }, delay);
90
+ mutationRetryTimer.unref?.();
91
+ }
92
+
93
+ function schedulePromotionDrain(delay = 0) {
94
+ if (!enabled || promotionRetryTimer) return;
95
+ promotionRetryTimer = setTimeout(() => {
96
+ promotionRetryTimer = null;
97
+ void drainPromotions();
98
+ }, delay);
99
+ promotionRetryTimer.unref?.();
100
+ }
101
+
102
+ function scheduleAttachmentDrain(delay = 0) {
103
+ if (!enabled || attachmentRetryTimer) return;
104
+ attachmentRetryTimer = setTimeout(() => {
105
+ attachmentRetryTimer = null;
106
+ void drainAttachments();
107
+ }, delay);
108
+ attachmentRetryTimer.unref?.();
109
+ }
110
+
111
+ async function drainMutations() {
112
+ if (!enabled || mutationDrain) return;
113
+ mutationDrain = (async () => {
114
+ const result = await localStateJson('POST', '/state/mutations/claim', {
115
+ limit: CLAIM_LIMIT,
116
+ leaseMs: CLAIM_LEASE_MS,
117
+ });
118
+ for (const claim of Array.isArray(result?.mutations) ? result.mutations : []) {
119
+ const envelope = claim?.envelope;
120
+ if (!envelope?.resourceId || !envelope?.mutationId) continue;
121
+ const requestId = `shared_${crypto.randomBytes(12).toString('hex')}`;
122
+ if (!sendLarge({ type: 'shared.mutation.submit', request_id: requestId, envelope })) {
123
+ await failMutationClaim(claim, 'shared tunnel backpressure: mutation was not queued');
124
+ scheduleMutationDrain(RETRY_MS);
125
+ continue;
126
+ }
127
+ mutationInflight.set(requestId, {
128
+ resourceId: envelope.resourceId,
129
+ mutationId: envelope.mutationId,
130
+ });
131
+ }
132
+ localFailures = 0;
133
+ })().catch((error) => {
134
+ localFailures += 1;
135
+ if (localFailures === 1 || localFailures % 20 === 0) {
136
+ warn(`shared outbox unavailable (attempt ${localFailures}): ${error?.message || error}`);
137
+ }
138
+ scheduleMutationDrain(RETRY_MS);
139
+ }).finally(() => {
140
+ mutationDrain = null;
141
+ });
142
+ await mutationDrain;
143
+ }
144
+
145
+ async function failMutationClaim(claim, message) {
146
+ try {
147
+ await localStateJson('POST', '/state/mutations/fail', {
148
+ resourceId: claim.envelope.resourceId,
149
+ mutationId: claim.envelope.mutationId,
150
+ message,
151
+ retryable: true,
152
+ });
153
+ } catch {
154
+ // The durable lease makes the row claimable again after expiry.
155
+ }
156
+ }
157
+
158
+ async function drainPromotions() {
159
+ if (!enabled || promotionDrain) return;
160
+ promotionDrain = (async () => {
161
+ const result = await localStateJson('POST', '/state/shared/promotions/claim', {
162
+ limit: 2,
163
+ leaseMs: CLAIM_LEASE_MS,
164
+ });
165
+ for (const claim of Array.isArray(result?.promotions) ? result.promotions : []) {
166
+ const promotion = claim?.frame;
167
+ if (!promotion?.promotionId || !promotion?.resourceId) continue;
168
+ const requestId = `promote_${crypto.randomBytes(12).toString('hex')}`;
169
+ const queued = sendLarge({
170
+ type: 'shared.resource.promote',
171
+ request_id: requestId,
172
+ promotion_id: promotion.promotionId,
173
+ resource_id: promotion.resourceId,
174
+ contract: promotion.contract,
175
+ schema_version: promotion.schemaVersion,
176
+ snapshot_b64: promotion.snapshotBase64,
177
+ snapshot_checksum: promotion.snapshotChecksum,
178
+ snapshot_bytes: promotion.snapshotBytes,
179
+ content_bytes: promotion.contentBytes,
180
+ name: promotion.name,
181
+ media_type: promotion.mediaType,
182
+ });
183
+ if (!queued) {
184
+ await localStateJson('POST', '/state/shared/promotions/fail', {
185
+ promotionId: promotion.promotionId,
186
+ message: 'shared tunnel backpressure: promotion was not queued',
187
+ retryable: true,
188
+ }).catch(() => {});
189
+ schedulePromotionDrain(RETRY_MS);
190
+ continue;
191
+ }
192
+ promotionInflight.set(requestId, {
193
+ promotionId: promotion.promotionId,
194
+ resourceId: promotion.resourceId,
195
+ });
196
+ }
197
+ })().catch(() => {
198
+ schedulePromotionDrain(RETRY_MS);
199
+ }).finally(() => {
200
+ promotionDrain = null;
201
+ });
202
+ await promotionDrain;
203
+ }
204
+
205
+ async function drainAttachments() {
206
+ if (!enabled || attachmentDrain) return;
207
+ attachmentDrain = (async () => {
208
+ const result = await localStateJson('POST', '/state/shared/attachments/claim', {
209
+ limit: 2,
210
+ leaseMs: CLAIM_LEASE_MS,
211
+ });
212
+ for (const attachment of Array.isArray(result?.attachments) ? result.attachments : []) {
213
+ if (!attachment?.attachmentId || !attachment?.resourceId) continue;
214
+ const requestId = `attach_${crypto.randomBytes(12).toString('hex')}`;
215
+ if (!send({
216
+ type: 'shared.resource.attach',
217
+ request_id: requestId,
218
+ attachment_id: attachment.attachmentId,
219
+ resource_id: attachment.resourceId,
220
+ })) {
221
+ scheduleAttachmentDrain(RETRY_MS);
222
+ continue;
223
+ }
224
+ attachmentInflight.set(requestId, {
225
+ attachmentId: attachment.attachmentId,
226
+ resourceId: attachment.resourceId,
227
+ });
228
+ }
229
+ })().catch(() => {
230
+ scheduleAttachmentDrain(RETRY_MS);
231
+ }).finally(() => {
232
+ attachmentDrain = null;
233
+ });
234
+ await attachmentDrain;
235
+ }
236
+
237
+ async function joinReplicas() {
238
+ try {
239
+ const result = await localStateJson('GET', '/state/shared/replicas');
240
+ for (const replica of Array.isArray(result?.replicas) ? result.replicas : []) {
241
+ if (!replica?.resourceId) continue;
242
+ send({
243
+ type: 'shared.resource.join',
244
+ request_id: `join_${crypto.randomBytes(12).toString('hex')}`,
245
+ resource_id: replica.resourceId,
246
+ after_version: replica.appliedVersion || 0,
247
+ });
248
+ }
249
+ } catch {
250
+ // The local service can still be starting; wake reconnect retries.
251
+ }
252
+ }
253
+
254
+ function parseWakeChunk(state, chunk) {
255
+ state.buffer += chunk.toString('utf8').replace(/\r\n/g, '\n');
256
+ let boundary = state.buffer.indexOf('\n\n');
257
+ while (boundary >= 0) {
258
+ const block = state.buffer.slice(0, boundary);
259
+ state.buffer = state.buffer.slice(boundary + 2);
260
+ let eventType = 'message';
261
+ const data = [];
262
+ for (const line of block.split('\n')) {
263
+ if (line.startsWith('event:')) eventType = line.slice(6).trim();
264
+ if (line.startsWith('data:')) data.push(line.slice(5).trimStart());
265
+ }
266
+ if (eventType === 'state' && data.length > 0) {
267
+ try {
268
+ const event = JSON.parse(data.join('\n'));
269
+ if (event?.sharedResourceId) {
270
+ scheduleAttachmentDrain();
271
+ schedulePromotionDrain();
272
+ scheduleMutationDrain();
273
+ }
274
+ } catch {
275
+ // The stream is only a wake signal; durable queues remain truth.
276
+ }
277
+ }
278
+ boundary = state.buffer.indexOf('\n\n');
279
+ }
280
+ }
281
+
282
+ function startWakeStream() {
283
+ if (!enabled || wakeRequest) return;
284
+ const state = { buffer: '' };
285
+ const req = http.request({
286
+ hostname: '127.0.0.1',
287
+ port: localPort(),
288
+ method: 'GET',
289
+ path: '/state/stream?bootstrap=1&resources=',
290
+ headers: localHeaders({ accept: 'text/event-stream' }),
291
+ }, (res) => {
292
+ if ((res.statusCode || 500) >= 400) {
293
+ res.resume();
294
+ req.destroy(new Error(`shared wake stream failed with ${res.statusCode}`));
295
+ return;
296
+ }
297
+ res.on('data', (chunk) => parseWakeChunk(state, Buffer.from(chunk)));
298
+ res.on('end', finishWakeStream);
299
+ res.on('error', finishWakeStream);
300
+ });
301
+ wakeRequest = req;
302
+ req.on('error', finishWakeStream);
303
+ req.end();
304
+
305
+ function finishWakeStream() {
306
+ if (wakeRequest === req) wakeRequest = null;
307
+ scheduleWakeReconnect();
308
+ }
309
+ }
310
+
311
+ function scheduleWakeReconnect() {
312
+ if (!enabled || wakeRetryTimer) return;
313
+ wakeRetryTimer = setTimeout(() => {
314
+ wakeRetryTimer = null;
315
+ startPump();
316
+ }, RETRY_MS);
317
+ wakeRetryTimer.unref?.();
318
+ }
319
+
320
+ function startPump() {
321
+ if (!enabled) return;
322
+ startWakeStream();
323
+ void joinReplicas();
324
+ scheduleAttachmentDrain();
325
+ schedulePromotionDrain();
326
+ scheduleMutationDrain();
327
+ }
328
+
329
+ function handleAttached(frame) {
330
+ const identity = attachmentInflight.get(frame.request_id);
331
+ if (!identity) return;
332
+ void localStateJson('POST', '/state/shared/attachments/ack', {
333
+ attachmentId: identity.attachmentId,
334
+ resourceId: frame.resource_id,
335
+ authorityEpoch: frame.authority_epoch,
336
+ headVersion: frame.head_version,
337
+ snapshotVersion: frame.snapshot_version,
338
+ snapshotChecksum: frame.snapshot_checksum,
339
+ snapshotBase64: frame.snapshot_b64,
340
+ contract: frame.contract,
341
+ schemaVersion: frame.schema_version,
342
+ }).then(() => {
343
+ attachmentInflight.delete(frame.request_id);
344
+ void joinReplicas();
345
+ }).catch((error) => {
346
+ void localStateJson('POST', '/state/shared/attachments/fail', {
347
+ attachmentId: identity.attachmentId,
348
+ message: error?.message || 'local snapshot materialization failed',
349
+ retryable: false,
350
+ }).catch(() => {});
351
+ attachmentInflight.delete(frame.request_id);
352
+ });
353
+ }
354
+
355
+ function handlePromoted(frame) {
356
+ const identity = promotionInflight.get(frame.request_id);
357
+ if (!identity) return;
358
+ void localStateJson('POST', '/state/shared/promotions/ack', {
359
+ promotionId: identity.promotionId,
360
+ resourceId: frame.resource_id,
361
+ authorityEpoch: frame.authority_epoch,
362
+ headVersion: frame.head_version,
363
+ snapshotVersion: frame.snapshot_version,
364
+ snapshotKey: frame.snapshot_key,
365
+ snapshotChecksum: frame.snapshot_checksum,
366
+ }).then(() => {
367
+ promotionInflight.delete(frame.request_id);
368
+ void joinReplicas();
369
+ scheduleMutationDrain();
370
+ }).catch(() => {
371
+ setTimeout(() => enabled && handlePromoted(frame), RETRY_MS).unref?.();
372
+ });
373
+ }
374
+
375
+ function handleAck(frame) {
376
+ const identity = mutationInflight.get(frame.request_id) || {
377
+ resourceId: frame.resource_id,
378
+ mutationId: frame.mutation_id,
379
+ };
380
+ if (!identity.resourceId || !identity.mutationId) return;
381
+ void localStateJson('POST', '/state/mutations/ack', {
382
+ resourceId: identity.resourceId,
383
+ mutationId: identity.mutationId,
384
+ version: frame.version,
385
+ committedAt: frame.committed_at,
386
+ }).then(() => {
387
+ mutationInflight.delete(frame.request_id);
388
+ scheduleMutationDrain();
389
+ }).catch(() => {
390
+ // The cloud commit is official. Preserve this exact acknowledgement.
391
+ setTimeout(() => enabled && handleAck(frame), RETRY_MS).unref?.();
392
+ });
393
+ }
394
+
395
+ function handleError(frame) {
396
+ const attachment = attachmentInflight.get(frame.request_id);
397
+ if (attachment) {
398
+ void localStateJson('POST', '/state/shared/attachments/fail', {
399
+ attachmentId: attachment.attachmentId,
400
+ message: `${frame.code || 'shared_error'}: ${frame.message || 'attachment failed'}`,
401
+ retryable: frame.retryable !== false,
402
+ }).finally(() => {
403
+ attachmentInflight.delete(frame.request_id);
404
+ if (frame.retryable !== false) scheduleAttachmentDrain(RETRY_MS);
405
+ });
406
+ return;
407
+ }
408
+
409
+ const promotion = promotionInflight.get(frame.request_id);
410
+ if (promotion) {
411
+ void localStateJson('POST', '/state/shared/promotions/fail', {
412
+ promotionId: promotion.promotionId,
413
+ message: `${frame.code || 'shared_error'}: ${frame.message || 'promotion failed'}`,
414
+ retryable: frame.retryable !== false,
415
+ }).finally(() => {
416
+ promotionInflight.delete(frame.request_id);
417
+ if (frame.retryable !== false) schedulePromotionDrain(RETRY_MS);
418
+ });
419
+ return;
420
+ }
421
+
422
+ const identity = mutationInflight.get(frame.request_id) || {
423
+ resourceId: frame.resource_id,
424
+ mutationId: frame.mutation_id,
425
+ };
426
+ if (!identity.resourceId || !identity.mutationId) return;
427
+ void localStateJson('POST', '/state/mutations/fail', {
428
+ resourceId: identity.resourceId,
429
+ mutationId: identity.mutationId,
430
+ message: `${frame.code || 'shared_error'}: ${frame.message || 'shared mutation failed'}`,
431
+ retryable: frame.retryable !== false,
432
+ }).finally(() => {
433
+ mutationInflight.delete(frame.request_id);
434
+ if (frame.retryable !== false) scheduleMutationDrain(RETRY_MS);
435
+ });
436
+ }
437
+
438
+ function handleCommitted(frame) {
439
+ const resourceId = frame.resource_id || frame.envelope?.resourceId;
440
+ if (!resourceId) return;
441
+ const prior = inboundChains.get(resourceId) || Promise.resolve();
442
+ const next = prior.then(() => localStateJson('POST', '/state/mutations/ingest', {
443
+ envelope: frame.envelope,
444
+ version: frame.version,
445
+ committedAt: frame.committed_at,
446
+ actorUserId: frame.actor_user_id,
447
+ })).catch((error) => {
448
+ warn(`shared mutation ${frame.mutation_id || 'unknown'} was not materialized: ${error?.message || error}`);
449
+ setTimeout(() => enabled && void joinReplicas(), RETRY_MS).unref?.();
450
+ }).finally(() => {
451
+ if (inboundChains.get(resourceId) === next) inboundChains.delete(resourceId);
452
+ });
453
+ inboundChains.set(resourceId, next);
454
+ }
455
+
456
+ function handleFrame(frame) {
457
+ if (!enabled) return false;
458
+ switch (frame.type) {
459
+ case 'shared.mutation.ack':
460
+ handleAck(frame);
461
+ return true;
462
+ case 'shared.resource.promoted':
463
+ handlePromoted(frame);
464
+ return true;
465
+ case 'shared.resource.attached':
466
+ handleAttached(frame);
467
+ return true;
468
+ case 'shared.mutation.committed':
469
+ handleCommitted(frame);
470
+ return true;
471
+ case 'shared.error':
472
+ handleError(frame);
473
+ return true;
474
+ case 'shared.resource.joined':
475
+ void localStateJson('POST', '/state/shared/replicas/authority', {
476
+ resourceId: frame.resource_id,
477
+ authorityEpoch: frame.authority_epoch,
478
+ headVersion: frame.head_version,
479
+ contract: frame.contract,
480
+ schemaVersion: frame.schema_version,
481
+ }).catch(() => {});
482
+ return true;
483
+ default:
484
+ return false;
485
+ }
486
+ }
487
+
488
+ function enable(capabilities) {
489
+ if (!Array.isArray(capabilities) || !capabilities.includes(CAPABILITY)) {
490
+ stop();
491
+ return;
492
+ }
493
+ enabled = true;
494
+ startPump();
495
+ }
496
+
497
+ function stop() {
498
+ enabled = false;
499
+ mutationInflight.clear();
500
+ promotionInflight.clear();
501
+ attachmentInflight.clear();
502
+ inboundChains.clear();
503
+ if (wakeRequest) wakeRequest.destroy();
504
+ wakeRequest = null;
505
+ clearTimeout(wakeRetryTimer);
506
+ clearTimeout(mutationRetryTimer);
507
+ clearTimeout(promotionRetryTimer);
508
+ clearTimeout(attachmentRetryTimer);
509
+ wakeRetryTimer = null;
510
+ mutationRetryTimer = null;
511
+ promotionRetryTimer = null;
512
+ attachmentRetryTimer = null;
513
+ }
514
+
515
+ return { enable, handleFrame, stop };
516
+ }
517
+
518
+ module.exports = { CAPABILITY, createSharedRealtimeTunnel };
@@ -14,6 +14,7 @@ const {
14
14
  RUNTIME_STATE_FILE,
15
15
  } = require('./paths');
16
16
  const { runtimePort, runtimeServiceByName } = require('./runtime-manifest');
17
+ const { createSharedRealtimeTunnel } = require('./shared-realtime-tunnel');
17
18
 
18
19
  const DEFAULT_GATEWAY_PORT = runtimeServiceByName('local-gateway').defaultPort;
19
20
  const DEFAULT_PORT_MONITOR_PORT = runtimeServiceByName('port-monitor').defaultPort;
@@ -407,6 +408,28 @@ function createEventTunnel({ record, foreground = false }) {
407
408
  return sendPayload(JSON.stringify(frame));
408
409
  }
409
410
 
411
+ // Durable mutations larger than the control threshold use the same bounded
412
+ // bulk queue as responses. Their identities remain independent even when
413
+ // several frames share the queue.
414
+ function sendShared(frame) {
415
+ const payload = JSON.stringify(frame);
416
+ if (payload.length <= BULK_FRAME_BYTES) return sendPayload(payload);
417
+ if (!ws || ws.readyState !== WebSocket.OPEN) return false;
418
+ if (bulkQueuedBytes + payload.length > bulkQueueMaxBytes) return false;
419
+ bulkQueue.push(payloadItem(payload));
420
+ bulkQueuedBytes += payload.length;
421
+ drainBulk();
422
+ return true;
423
+ }
424
+
425
+ const sharedRealtime = createSharedRealtimeTunnel({
426
+ send,
427
+ sendLarge: sendShared,
428
+ localPort: defaultTargetPort,
429
+ localHeaders,
430
+ warn,
431
+ });
432
+
410
433
  // A bulk queue entry: `bytes` is the exact frame size (known up front, so
411
434
  // the queue budget applies BEFORE anything is materialized) and `build`
412
435
  // produces the JSON payload only when the frame is about to hit the wire.
@@ -793,8 +816,10 @@ function createEventTunnel({ record, foreground = false }) {
793
816
  if (!frame || typeof frame.type !== 'string') return;
794
817
  if (frame.type === 'hello_ack') {
795
818
  peerProtocolVersion = Number(frame.protocol_version) || 1;
819
+ sharedRealtime.enable(frame.capabilities);
796
820
  return;
797
821
  }
822
+ if (sharedRealtime.handleFrame(frame)) return;
798
823
  if (frame.type === 'ping') {
799
824
  send({ type: 'pong' });
800
825
  return;
@@ -882,6 +907,7 @@ function createEventTunnel({ record, foreground = false }) {
882
907
  connectedAt = 0;
883
908
  lastGatewayFrameAt = 0;
884
909
  peerProtocolVersion = 1;
910
+ sharedRealtime.stop();
885
911
  // Streams, sockets, and queued response frames belong to THIS gateway
886
912
  // connection: their req/conn ids mean nothing to the next one, and its
887
913
  // cancel frames will never reference them. Dropping them here is the
@@ -933,6 +959,7 @@ function createEventTunnel({ record, foreground = false }) {
933
959
  stopped = true;
934
960
  if (reconnectTimer) clearTimeout(reconnectTimer);
935
961
  if (routeTimer) clearInterval(routeTimer);
962
+ sharedRealtime.stop();
936
963
  stopWatchdog();
937
964
  resetBulkQueue('event tunnel stopped');
938
965
  dropActiveProxies('event tunnel stopped');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.155",
3
+ "version": "0.1.156",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const stateRest = require('../../state/rest');
4
+ const sharedRest = require('../../state/shared-rest');
4
5
 
5
6
  async function handleStateRoutes(ctx) {
6
7
  if (ctx.method === 'GET') {
@@ -24,6 +25,24 @@ async function handleStateRoutes(ctx) {
24
25
  await stateRest.handleResourceList(ctx.sendJson);
25
26
  return true;
26
27
  }
28
+ if (ctx.pathname === '/state/shared/replicas') {
29
+ await sharedRest.handleReplicaList(ctx.sendJson);
30
+ return true;
31
+ }
32
+ if (ctx.pathname.startsWith('/state/shared/promotions/')) {
33
+ await sharedRest.handlePromotionGet(
34
+ decodeURIComponent(ctx.pathname.slice('/state/shared/promotions/'.length)),
35
+ ctx.sendJson,
36
+ );
37
+ return true;
38
+ }
39
+ if (ctx.pathname.startsWith('/state/shared/attachments/')) {
40
+ await sharedRest.handleAttachmentGet(
41
+ decodeURIComponent(ctx.pathname.slice('/state/shared/attachments/'.length)),
42
+ ctx.sendJson,
43
+ );
44
+ return true;
45
+ }
27
46
  return false;
28
47
  }
29
48
 
@@ -52,6 +71,62 @@ async function handleStateRoutes(ctx) {
52
71
  await stateRest.handleDocWrite(await ctx.readJsonBody(), ctx.sendJson);
53
72
  return true;
54
73
  }
74
+ if (ctx.pathname === '/state/mutations/claim') {
75
+ await sharedRest.handleMutationClaim(await ctx.readJsonBody(), ctx.sendJson);
76
+ return true;
77
+ }
78
+ if (ctx.pathname === '/state/mutations/ack') {
79
+ await sharedRest.handleMutationAck(await ctx.readJsonBody(), ctx.sendJson);
80
+ return true;
81
+ }
82
+ if (ctx.pathname === '/state/mutations/fail') {
83
+ await sharedRest.handleMutationFail(await ctx.readJsonBody(), ctx.sendJson);
84
+ return true;
85
+ }
86
+ if (ctx.pathname === '/state/mutations/ingest') {
87
+ await sharedRest.handleMutationIngest(await ctx.readJsonBody(), ctx.sendJson);
88
+ return true;
89
+ }
90
+ if (ctx.pathname === '/state/shared/replicas/register') {
91
+ await sharedRest.handleReplicaRegister(await ctx.readJsonBody(), ctx.sendJson);
92
+ return true;
93
+ }
94
+ if (ctx.pathname === '/state/shared/replicas/authority') {
95
+ await sharedRest.handleReplicaAuthority(await ctx.readJsonBody(), ctx.sendJson);
96
+ return true;
97
+ }
98
+ if (ctx.pathname === '/state/shared/promote') {
99
+ await sharedRest.handlePromotionStart(await ctx.readJsonBody(), ctx.sendJson);
100
+ return true;
101
+ }
102
+ if (ctx.pathname === '/state/shared/promotions/claim') {
103
+ await sharedRest.handlePromotionClaim(await ctx.readJsonBody(), ctx.sendJson);
104
+ return true;
105
+ }
106
+ if (ctx.pathname === '/state/shared/promotions/ack') {
107
+ await sharedRest.handlePromotionAck(await ctx.readJsonBody(), ctx.sendJson);
108
+ return true;
109
+ }
110
+ if (ctx.pathname === '/state/shared/promotions/fail') {
111
+ await sharedRest.handlePromotionFail(await ctx.readJsonBody(), ctx.sendJson);
112
+ return true;
113
+ }
114
+ if (ctx.pathname === '/state/shared/attach') {
115
+ await sharedRest.handleAttachmentStart(await ctx.readJsonBody(), ctx.sendJson);
116
+ return true;
117
+ }
118
+ if (ctx.pathname === '/state/shared/attachments/claim') {
119
+ await sharedRest.handleAttachmentClaim(await ctx.readJsonBody(), ctx.sendJson);
120
+ return true;
121
+ }
122
+ if (ctx.pathname === '/state/shared/attachments/ack') {
123
+ await sharedRest.handleAttachmentAck(await ctx.readJsonBody(), ctx.sendJson);
124
+ return true;
125
+ }
126
+ if (ctx.pathname === '/state/shared/attachments/fail') {
127
+ await sharedRest.handleAttachmentFail(await ctx.readJsonBody(), ctx.sendJson);
128
+ return true;
129
+ }
55
130
  return false;
56
131
  }
57
132