@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.
- package/CHANGELOG.md +16 -0
- package/package.json +10 -10
- package/src/Platform.res +10 -6
- package/src/Platform.res.mjs +3 -0
- package/src/adapter/Api/LocalEvents_Server.res +324 -0
- package/src/adapter/Api/LocalEvents_Server.res.mjs +341 -0
- package/src/adapter/DomainGraphQL_Server.res +23 -0
- package/src/adapter/DomainGraphQL_Server.res.mjs +36 -14
- package/src/adapter/LocalBus.res +12 -1
- package/src/adapter/LocalBus.res.mjs +36 -0
- package/tests/adapter/LocalEvents_ServerTest.res +200 -0
- package/tests/adapter/LocalEvents_ServerTest.res.mjs +147 -0
|
@@ -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
|
-
}
|
|
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
|
-
}
|
|
187
|
+
}
|
|
188
|
+
if (path === "/__inmemory/login" && req.method === "POST") {
|
|
186
189
|
return handleLogin(req, res);
|
|
187
|
-
}
|
|
190
|
+
}
|
|
191
|
+
if (path === "/__inmemory/logout" && req.method === "POST") {
|
|
188
192
|
return handleLogout(req, res);
|
|
189
|
-
}
|
|
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
|
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -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
|
|
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,
|