@reventlessdev/reventless-local 3.0.0-alpha.174 → 3.0.0-alpha.176

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,341 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Ws from "ws";
4
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
8
+ import * as LocalAuth$ReventlessLocal from "../Auth/LocalAuth.res.mjs";
9
+
10
+ let log = Logger$ReventlessCore.fromEnv();
11
+
12
+ function pathSegment(s) {
13
+ return s.replace(/[^A-Za-z0-9-]/g, "-");
14
+ }
15
+
16
+ let clientChannelPrefix = "/client/";
17
+
18
+ function channelMatches(subscription, channel) {
19
+ if (!subscription.endsWith("/*")) {
20
+ return subscription === channel;
21
+ }
22
+ let prefix = subscription.slice(0, subscription.length - 1 | 0);
23
+ return channel.startsWith(prefix);
24
+ }
25
+
26
+ let connections = {
27
+ contents: []
28
+ };
29
+
30
+ let nextConnectionId = {
31
+ contents: 0
32
+ };
33
+
34
+ function addConnection(send) {
35
+ nextConnectionId.contents = nextConnectionId.contents + 1 | 0;
36
+ let conn_id = nextConnectionId.contents;
37
+ let conn_subscriptions = {};
38
+ let conn = {
39
+ id: conn_id,
40
+ send: send,
41
+ subscriptions: conn_subscriptions
42
+ };
43
+ connections.contents.push(conn);
44
+ return conn;
45
+ }
46
+
47
+ function removeConnection(conn) {
48
+ connections.contents = connections.contents.filter(c => c.id !== conn.id);
49
+ }
50
+
51
+ function resetConnections() {
52
+ connections.contents = [];
53
+ }
54
+
55
+ function frame(fields) {
56
+ return JSON.stringify(Object.fromEntries(fields));
57
+ }
58
+
59
+ let connectionAckFrame = frame([
60
+ [
61
+ "type",
62
+ "connection_ack"
63
+ ],
64
+ [
65
+ "connectionTimeoutMs",
66
+ 300000
67
+ ]
68
+ ]);
69
+
70
+ function dataFrame(subscriptionId, event) {
71
+ return frame([
72
+ [
73
+ "type",
74
+ "data"
75
+ ],
76
+ [
77
+ "id",
78
+ subscriptionId
79
+ ],
80
+ [
81
+ "event",
82
+ event
83
+ ]
84
+ ]);
85
+ }
86
+
87
+ function broadcast(channel, event) {
88
+ connections.contents.forEach(conn => {
89
+ Object.entries(conn.subscriptions).forEach(param => {
90
+ if (channelMatches(param[1], channel)) {
91
+ return conn.send(dataFrame(param[0], event));
92
+ }
93
+ });
94
+ });
95
+ }
96
+
97
+ function broadcastStateChange(name, descriptor) {
98
+ let entityKey = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(descriptor), o => o["id"]), Stdlib_JSON.Decode.string), "");
99
+ if (entityKey === "") {
100
+ return;
101
+ }
102
+ let channel = `/default/` + pathSegment(name) + `/` + pathSegment(entityKey);
103
+ broadcast(channel, JSON.stringify(descriptor));
104
+ }
105
+
106
+ function decodeStringField(json, field) {
107
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o[field]), Stdlib_JSON.Decode.string);
108
+ }
109
+
110
+ function handleFrame(conn, text) {
111
+ let parsed;
112
+ try {
113
+ parsed = JSON.parse(text);
114
+ } catch (exn) {
115
+ parsed = undefined;
116
+ }
117
+ if (parsed === undefined) {
118
+ return;
119
+ }
120
+ let match = decodeStringField(parsed, "type");
121
+ if (match === undefined) {
122
+ return;
123
+ }
124
+ switch (match) {
125
+ case "connection_init" :
126
+ return conn.send(connectionAckFrame);
127
+ case "subscribe" :
128
+ let match$1 = decodeStringField(parsed, "id");
129
+ let match$2 = decodeStringField(parsed, "channel");
130
+ if (match$1 !== undefined && match$2 !== undefined) {
131
+ conn.subscriptions[match$1] = match$2;
132
+ return conn.send(frame([
133
+ [
134
+ "type",
135
+ "subscribe_success"
136
+ ],
137
+ [
138
+ "id",
139
+ match$1
140
+ ]
141
+ ]));
142
+ } else {
143
+ return;
144
+ }
145
+ case "unsubscribe" :
146
+ let id = decodeStringField(parsed, "id");
147
+ if (id !== undefined) {
148
+ Stdlib_Dict.$$delete(conn.subscriptions, id);
149
+ return conn.send(frame([
150
+ [
151
+ "type",
152
+ "unsubscribe_success"
153
+ ],
154
+ [
155
+ "id",
156
+ id
157
+ ]
158
+ ]));
159
+ } else {
160
+ return;
161
+ }
162
+ default:
163
+ return;
164
+ }
165
+ }
166
+
167
+ function stripBearer(token) {
168
+ if (token.startsWith("Bearer ")) {
169
+ return token.slice(7, token.length).trim();
170
+ } else {
171
+ return token.trim();
172
+ }
173
+ }
174
+
175
+ function tokenIsInvalid(token) {
176
+ if (token !== undefined) {
177
+ return Stdlib_Option.isNone(LocalAuth$ReventlessLocal.Login.verifyAndDecode(stripBearer(token)));
178
+ } else {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ function authFromSubprotocolHeader(headerValue) {
184
+ return Stdlib_Option.flatMap(headerValue.split(",").map(prim => prim.trim()).find(p => p.startsWith("header-")), p => {
185
+ let blob = p.slice(7, p.length);
186
+ try {
187
+ return decodeStringField(JSON.parse(Buffer.from(blob, "base64url").toString("utf8")), "Authorization");
188
+ } catch (exn) {
189
+ return;
190
+ }
191
+ });
192
+ }
193
+
194
+ function jsonError(message) {
195
+ return Object.fromEntries([[
196
+ "error",
197
+ message
198
+ ]]);
199
+ }
200
+
201
+ function handlePublish(authorization, body) {
202
+ if (tokenIsInvalid(authorization)) {
203
+ return [
204
+ 401,
205
+ jsonError("Invalid token")
206
+ ];
207
+ }
208
+ let parsed;
209
+ try {
210
+ parsed = JSON.parse(body);
211
+ } catch (exn) {
212
+ parsed = undefined;
213
+ }
214
+ if (parsed === undefined) {
215
+ return [
216
+ 400,
217
+ jsonError("Invalid JSON body")
218
+ ];
219
+ }
220
+ let channel = decodeStringField(parsed, "channel");
221
+ let events = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(parsed), o => o["events"]), Stdlib_JSON.Decode.array);
222
+ if (channel === undefined) {
223
+ return [
224
+ 400,
225
+ jsonError("Body must carry `channel` and a non-empty `events` array")
226
+ ];
227
+ }
228
+ if (events === undefined) {
229
+ return [
230
+ 400,
231
+ jsonError("Body must carry `channel` and a non-empty `events` array")
232
+ ];
233
+ }
234
+ if (events.length === 0) {
235
+ return [
236
+ 400,
237
+ jsonError("Body must carry `channel` and a non-empty `events` array")
238
+ ];
239
+ }
240
+ if (!channel.startsWith(clientChannelPrefix)) {
241
+ return [
242
+ 403,
243
+ jsonError(`Clients may only publish under ` + clientChannelPrefix)
244
+ ];
245
+ }
246
+ let successful = [];
247
+ let failed = [];
248
+ events.forEach((event, index) => {
249
+ let entry = Object.fromEntries([[
250
+ "index",
251
+ index
252
+ ]]);
253
+ let s = Stdlib_JSON.Decode.string(event);
254
+ if (s !== undefined) {
255
+ let valid;
256
+ try {
257
+ JSON.parse(s);
258
+ valid = true;
259
+ } catch (exn) {
260
+ valid = false;
261
+ }
262
+ if (valid) {
263
+ broadcast(channel, s);
264
+ successful.push(entry);
265
+ } else {
266
+ failed.push(entry);
267
+ }
268
+ return;
269
+ }
270
+ failed.push(entry);
271
+ });
272
+ return [
273
+ 200,
274
+ Object.fromEntries([
275
+ [
276
+ "successful",
277
+ successful
278
+ ],
279
+ [
280
+ "failed",
281
+ failed
282
+ ]
283
+ ])
284
+ ];
285
+ }
286
+
287
+ let realtimePath = "/events/realtime";
288
+
289
+ let subprotocol = "aws-appsync-event-ws";
290
+
291
+ function attach(server, port) {
292
+ let wss = new Ws.WebSocketServer({
293
+ server: server,
294
+ path: realtimePath,
295
+ handleProtocols: (_protocols, _req) => subprotocol
296
+ });
297
+ wss.on("connection", (ws, req) => {
298
+ let auth = Stdlib_Option.flatMap(req.headers["sec-websocket-protocol"], authFromSubprotocolHeader);
299
+ if (tokenIsInvalid(auth)) {
300
+ ws.close(4401, "Invalid token");
301
+ return;
302
+ }
303
+ let conn = addConnection(f => {
304
+ ws.send(f);
305
+ });
306
+ ws.on("message", data => handleFrame(conn, data.toString("utf8")));
307
+ ws.on("close", () => removeConnection(conn));
308
+ });
309
+ log.info("Events:Local", undefined, `events transport on ws://localhost:` + port.toString() + realtimePath + ` (publish: POST /events)`);
310
+ }
311
+
312
+ let YG;
313
+
314
+ export {
315
+ YG,
316
+ log,
317
+ pathSegment,
318
+ clientChannelPrefix,
319
+ channelMatches,
320
+ connections,
321
+ nextConnectionId,
322
+ addConnection,
323
+ removeConnection,
324
+ resetConnections,
325
+ frame,
326
+ connectionAckFrame,
327
+ dataFrame,
328
+ broadcast,
329
+ broadcastStateChange,
330
+ decodeStringField,
331
+ handleFrame,
332
+ stripBearer,
333
+ tokenIsInvalid,
334
+ authFromSubprotocolHeader,
335
+ jsonError,
336
+ handlePublish,
337
+ realtimePath,
338
+ subprotocol,
339
+ attach,
340
+ }
341
+ /* log Not a pure module */
@@ -234,6 +234,26 @@ let _dispatch = (req: nodeRequest, res: nodeResponse, yoga: YG.yoga, getSdl: uni
234
234
  handleLogout(req, res)
235
235
  } else if path == uploadPresignPath && req.method == "POST" {
236
236
  handleUploadPresign(req, res)
237
+ } else if path == "/events" && req.method == "POST" {
238
+ // Local AppSync Events publish endpoint (`/client/**` only). The
239
+ // Authorization header carries the token raw (AWS Cognito-publish shape),
240
+ // so it bypasses `_isInvalidBearer` above and is verified inside.
241
+ let authorization = req.headers->Dict.get("authorization")
242
+ readBody(req, body => {
243
+ let (status, json) = LocalEvents_Server.handlePublish(~authorization, ~body)
244
+ _writeJson(res, ~status, json)
245
+ })
246
+ } else if path == "/events" && req.method == "OPTIONS" {
247
+ // Preflight for the cross-origin browser publish (Authorization header).
248
+ res->writeHead(
249
+ 204,
250
+ {
251
+ "Access-Control-Allow-Origin": "*",
252
+ "Access-Control-Allow-Methods": "POST,OPTIONS",
253
+ "Access-Control-Allow-Headers": "*",
254
+ },
255
+ )
256
+ res->endEmpty
237
257
  } else if req.method == "OPTIONS" && LocalObjectStore.servedKey(path)->Option.isSome {
238
258
  // Preflight for the cross-origin browser PUT to a served-object path.
239
259
  res->writeHead(204, _corsWriteHeaders)
@@ -692,6 +712,9 @@ let start = (~port: int=4000, ~contextFactory as _: option<YG.contextFactory>=?,
692
712
  server->YG.listen(port, () =>
693
713
  log.info(~comp="GraphQL:Domain", `listening on http://localhost:${port->Int.toString}/graphql (SDL: /sdl)`)
694
714
  )
715
+ // Local AppSync Events transport (subscribe WS). Attached here so every
716
+ // start mode (split, unified, replay) carries it.
717
+ LocalEvents_Server.attach(~server, ~port)
695
718
  activeServer.contents = Some(server)
696
719
  }
697
720
 
@@ -19,6 +19,7 @@ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/
19
19
  import * as LocalAuth$ReventlessLocal from "./Auth/LocalAuth.res.mjs";
20
20
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
21
21
  import * as LocalObjectStore$ReventlessLocal from "./LocalObjectStore.res.mjs";
22
+ import * as LocalEvents_Server$ReventlessLocal from "./Api/LocalEvents_Server.res.mjs";
22
23
  import * as Auth_GraphqlContext$ReventlessLocal from "./Auth/Auth_GraphqlContext.res.mjs";
23
24
 
24
25
  let log = Logger$ReventlessCore.fromEnv();
@@ -175,30 +176,50 @@ function _dispatch(req, res, yoga, getSdl) {
175
176
  "error",
176
177
  "Invalid bearer token"
177
178
  ]]));
178
- } else if (path === "/sdl") {
179
+ }
180
+ if (path === "/sdl") {
179
181
  res.writeHead(200, {
180
182
  "Content-Type": "text/plain",
181
183
  "Access-Control-Allow-Origin": "*"
182
184
  });
183
185
  res.end(getSdl());
184
186
  return;
185
- } else if (path === "/__inmemory/login" && req.method === "POST") {
187
+ }
188
+ if (path === "/__inmemory/login" && req.method === "POST") {
186
189
  return handleLogin(req, res);
187
- } else if (path === "/__inmemory/logout" && req.method === "POST") {
190
+ }
191
+ if (path === "/__inmemory/logout" && req.method === "POST") {
188
192
  return handleLogout(req, res);
189
- } else if (path === uploadPresignPath && req.method === "POST") {
193
+ }
194
+ if (path === uploadPresignPath && req.method === "POST") {
190
195
  return handleUploadPresign(req, res);
191
- } else if (req.method === "OPTIONS" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
192
- res.writeHead(204, _corsWriteHeaders);
193
- res.end(null);
194
- return;
195
- } else if (req.method === "PUT" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
196
- return handleObjectPut(req, res, Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.servedKey(path), ""));
197
- } else if (req.method === "GET" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
198
- return handleObjectGet(res, Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.servedKey(path), ""));
199
- } else {
200
- return yoga(req, res);
201
196
  }
197
+ if (path !== "/events" || req.method !== "POST") {
198
+ if (path === "/events" && req.method === "OPTIONS") {
199
+ res.writeHead(204, {
200
+ "Access-Control-Allow-Origin": "*",
201
+ "Access-Control-Allow-Methods": "POST,OPTIONS",
202
+ "Access-Control-Allow-Headers": "*"
203
+ });
204
+ res.end(null);
205
+ return;
206
+ } else if (req.method === "OPTIONS" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
207
+ res.writeHead(204, _corsWriteHeaders);
208
+ res.end(null);
209
+ return;
210
+ } else if (req.method === "PUT" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
211
+ return handleObjectPut(req, res, Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.servedKey(path), ""));
212
+ } else if (req.method === "GET" && Stdlib_Option.isSome(LocalObjectStore$ReventlessLocal.servedKey(path))) {
213
+ return handleObjectGet(res, Stdlib_Option.getOr(LocalObjectStore$ReventlessLocal.servedKey(path), ""));
214
+ } else {
215
+ return yoga(req, res);
216
+ }
217
+ }
218
+ let authorization = req.headers["authorization"];
219
+ readBody(req, body => {
220
+ let match = LocalEvents_Server$ReventlessLocal.handlePublish(authorization, body);
221
+ _writeJson(res, match[0], match[1]);
222
+ });
202
223
  }
203
224
 
204
225
  function encodeGlobalId(typeName, localId) {
@@ -572,6 +593,7 @@ function start(portOpt, param, param$1) {
572
593
  Stdlib_JsError.throwWithMessage(`DomainGraphQL_Server could not bind port ` + port.toString() + `: ` + detail);
573
594
  });
574
595
  server.listen(port, () => log.info("GraphQL:Domain", undefined, `listening on http://localhost:` + port.toString() + `/graphql (SDL: /sdl)`));
596
+ LocalEvents_Server$ReventlessLocal.attach(server, port);
575
597
  activeServer.contents = Primitive_option.some(server);
576
598
  }
577
599
 
@@ -216,6 +216,10 @@ module type T = {
216
216
  // "Removed". `position` is omitted (Phase 3 deferred).
217
217
  let publishStateChange: (~name: string, ~descriptor: JSON.t) => unit
218
218
  let subscribeToStateChanges: (string, JSON.t => unit) => unit
219
+ // All-changes variant: receives every publishStateChange with its read-model
220
+ // name. Used by the local Events transport to bridge descriptors onto
221
+ // `/default/{name}/{id}` channels without enumerating read models up front.
222
+ let subscribeToAllStateChanges: ((~name: string, ~descriptor: JSON.t) => unit) => unit
219
223
 
220
224
  // Cross-plugin EP subscription registry.
221
225
  // EventCollectors register their handler once resolved; makePlatform subscribes
@@ -463,17 +467,23 @@ module Impl = (C: BusConfig): T => {
463
467
 
464
468
  // Source B state-change hook — per QueryDb name, zero or more listeners.
465
469
  let stateChangeListeners: ref<dict<array<JSON.t => unit>>> = ref(Dict.make())
470
+ let allStateChangeListeners: ref<array<(~name: string, ~descriptor: JSON.t) => unit>> = ref([])
466
471
 
467
472
  let subscribeToStateChanges = (name, callback) => {
468
473
  let listeners = stateChangeListeners.contents->Dict.get(name)->Option.getOr([])
469
474
  stateChangeListeners.contents->Dict.set(name, Array.concat(listeners, [callback]))
470
475
  }
471
476
 
472
- let publishStateChange = (~name, ~descriptor) =>
477
+ let subscribeToAllStateChanges = callback =>
478
+ allStateChangeListeners.contents->Array.push(callback)
479
+
480
+ let publishStateChange = (~name, ~descriptor) => {
473
481
  stateChangeListeners.contents
474
482
  ->Dict.get(name)
475
483
  ->Option.getOr([])
476
484
  ->Array.forEach(cb => cb(descriptor))
485
+ allStateChangeListeners.contents->Array.forEach(cb => cb(~name, ~descriptor))
486
+ }
477
487
 
478
488
  let eventCollectorHandlers: ref<dict<(JSON.t, unit) => promise<unit>>> = ref(Dict.make())
479
489
  let eventCollectorPendingTopics: ref<dict<array<string>>> = ref(Dict.make())
@@ -541,6 +551,7 @@ module Impl = (C: BusConfig): T => {
541
551
  eventLogReplayRegistry := Dict.make()
542
552
  dcbEventLogReadRegistry := Dict.make()
543
553
  stateChangeListeners := Dict.make()
554
+ allStateChangeListeners := []
544
555
  eventCollectorHandlers := Dict.make()
545
556
  eventCollectorPendingTopics := Dict.make()
546
557
  projectionCatchupRegistry := Dict.make()
@@ -259,12 +259,19 @@ function Impl(C) {
259
259
  let stateChangeListeners = {
260
260
  contents: {}
261
261
  };
262
+ let allStateChangeListeners = {
263
+ contents: []
264
+ };
262
265
  let subscribeToStateChanges = (name, callback) => {
263
266
  let listeners = Stdlib_Option.getOr(stateChangeListeners.contents[name], []);
264
267
  stateChangeListeners.contents[name] = listeners.concat([callback]);
265
268
  };
269
+ let subscribeToAllStateChanges = callback => {
270
+ allStateChangeListeners.contents.push(callback);
271
+ };
266
272
  let publishStateChange = (name, descriptor) => {
267
273
  Stdlib_Option.getOr(stateChangeListeners.contents[name], []).forEach(cb => cb(descriptor));
274
+ allStateChangeListeners.contents.forEach(cb => cb(name, descriptor));
268
275
  };
269
276
  let eventCollectorHandlers = {
270
277
  contents: {}
@@ -320,6 +327,7 @@ function Impl(C) {
320
327
  eventLogReplayRegistry.contents = {};
321
328
  dcbEventLogReadRegistry.contents = {};
322
329
  stateChangeListeners.contents = {};
330
+ allStateChangeListeners.contents = [];
323
331
  eventCollectorHandlers.contents = {};
324
332
  eventCollectorPendingTopics.contents = {};
325
333
  projectionCatchupRegistry.contents = {};
@@ -346,6 +354,7 @@ function Impl(C) {
346
354
  getDcbEventLogRead: getDcbEventLogRead,
347
355
  publishStateChange: publishStateChange,
348
356
  subscribeToStateChanges: subscribeToStateChanges,
357
+ subscribeToAllStateChanges: subscribeToAllStateChanges,
349
358
  registerEventCollectorHandler: registerEventCollectorHandler,
350
359
  subscribeEventCollectorToTopic: subscribeEventCollectorToTopic,
351
360
  registerProjectionCatchupHandler: registerProjectionCatchupHandler,
@@ -525,12 +534,19 @@ function Make($star) {
525
534
  let stateChangeListeners = {
526
535
  contents: {}
527
536
  };
537
+ let allStateChangeListeners = {
538
+ contents: []
539
+ };
528
540
  let subscribeToStateChanges = (name, callback) => {
529
541
  let listeners = Stdlib_Option.getOr(stateChangeListeners.contents[name], []);
530
542
  stateChangeListeners.contents[name] = listeners.concat([callback]);
531
543
  };
544
+ let subscribeToAllStateChanges = callback => {
545
+ allStateChangeListeners.contents.push(callback);
546
+ };
532
547
  let publishStateChange = (name, descriptor) => {
533
548
  Stdlib_Option.getOr(stateChangeListeners.contents[name], []).forEach(cb => cb(descriptor));
549
+ allStateChangeListeners.contents.forEach(cb => cb(name, descriptor));
534
550
  };
535
551
  let eventCollectorHandlers = {
536
552
  contents: {}
@@ -586,6 +602,7 @@ function Make($star) {
586
602
  eventLogReplayRegistry.contents = {};
587
603
  dcbEventLogReadRegistry.contents = {};
588
604
  stateChangeListeners.contents = {};
605
+ allStateChangeListeners.contents = [];
589
606
  eventCollectorHandlers.contents = {};
590
607
  eventCollectorPendingTopics.contents = {};
591
608
  projectionCatchupRegistry.contents = {};
@@ -612,6 +629,7 @@ function Make($star) {
612
629
  getDcbEventLogRead: getDcbEventLogRead,
613
630
  publishStateChange: publishStateChange,
614
631
  subscribeToStateChanges: subscribeToStateChanges,
632
+ subscribeToAllStateChanges: subscribeToAllStateChanges,
615
633
  registerEventCollectorHandler: registerEventCollectorHandler,
616
634
  subscribeEventCollectorToTopic: subscribeEventCollectorToTopic,
617
635
  registerProjectionCatchupHandler: registerProjectionCatchupHandler,
@@ -791,12 +809,19 @@ function MakeSilent($star) {
791
809
  let stateChangeListeners = {
792
810
  contents: {}
793
811
  };
812
+ let allStateChangeListeners = {
813
+ contents: []
814
+ };
794
815
  let subscribeToStateChanges = (name, callback) => {
795
816
  let listeners = Stdlib_Option.getOr(stateChangeListeners.contents[name], []);
796
817
  stateChangeListeners.contents[name] = listeners.concat([callback]);
797
818
  };
819
+ let subscribeToAllStateChanges = callback => {
820
+ allStateChangeListeners.contents.push(callback);
821
+ };
798
822
  let publishStateChange = (name, descriptor) => {
799
823
  Stdlib_Option.getOr(stateChangeListeners.contents[name], []).forEach(cb => cb(descriptor));
824
+ allStateChangeListeners.contents.forEach(cb => cb(name, descriptor));
800
825
  };
801
826
  let eventCollectorHandlers = {
802
827
  contents: {}
@@ -852,6 +877,7 @@ function MakeSilent($star) {
852
877
  eventLogReplayRegistry.contents = {};
853
878
  dcbEventLogReadRegistry.contents = {};
854
879
  stateChangeListeners.contents = {};
880
+ allStateChangeListeners.contents = [];
855
881
  eventCollectorHandlers.contents = {};
856
882
  eventCollectorPendingTopics.contents = {};
857
883
  projectionCatchupRegistry.contents = {};
@@ -878,6 +904,7 @@ function MakeSilent($star) {
878
904
  getDcbEventLogRead: getDcbEventLogRead,
879
905
  publishStateChange: publishStateChange,
880
906
  subscribeToStateChanges: subscribeToStateChanges,
907
+ subscribeToAllStateChanges: subscribeToAllStateChanges,
881
908
  registerEventCollectorHandler: registerEventCollectorHandler,
882
909
  subscribeEventCollectorToTopic: subscribeEventCollectorToTopic,
883
910
  registerProjectionCatchupHandler: registerProjectionCatchupHandler,
@@ -1055,12 +1082,19 @@ function MakeBounded(C) {
1055
1082
  let stateChangeListeners = {
1056
1083
  contents: {}
1057
1084
  };
1085
+ let allStateChangeListeners = {
1086
+ contents: []
1087
+ };
1058
1088
  let subscribeToStateChanges = (name, callback) => {
1059
1089
  let listeners = Stdlib_Option.getOr(stateChangeListeners.contents[name], []);
1060
1090
  stateChangeListeners.contents[name] = listeners.concat([callback]);
1061
1091
  };
1092
+ let subscribeToAllStateChanges = callback => {
1093
+ allStateChangeListeners.contents.push(callback);
1094
+ };
1062
1095
  let publishStateChange = (name, descriptor) => {
1063
1096
  Stdlib_Option.getOr(stateChangeListeners.contents[name], []).forEach(cb => cb(descriptor));
1097
+ allStateChangeListeners.contents.forEach(cb => cb(name, descriptor));
1064
1098
  };
1065
1099
  let eventCollectorHandlers = {
1066
1100
  contents: {}
@@ -1116,6 +1150,7 @@ function MakeBounded(C) {
1116
1150
  eventLogReplayRegistry.contents = {};
1117
1151
  dcbEventLogReadRegistry.contents = {};
1118
1152
  stateChangeListeners.contents = {};
1153
+ allStateChangeListeners.contents = [];
1119
1154
  eventCollectorHandlers.contents = {};
1120
1155
  eventCollectorPendingTopics.contents = {};
1121
1156
  projectionCatchupRegistry.contents = {};
@@ -1142,6 +1177,7 @@ function MakeBounded(C) {
1142
1177
  getDcbEventLogRead: getDcbEventLogRead,
1143
1178
  publishStateChange: publishStateChange,
1144
1179
  subscribeToStateChanges: subscribeToStateChanges,
1180
+ subscribeToAllStateChanges: subscribeToAllStateChanges,
1145
1181
  registerEventCollectorHandler: registerEventCollectorHandler,
1146
1182
  subscribeEventCollectorToTopic: subscribeEventCollectorToTopic,
1147
1183
  registerProjectionCatchupHandler: registerProjectionCatchupHandler,