@reventlessdev/reventless-local 3.0.0-alpha.317 → 3.0.0-alpha.319

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +10 -10
  3. package/src/LocalPlatformRegistry.res +26 -1
  4. package/src/LocalPlatformRegistry.res.mjs +25 -5
  5. package/src/Platform.res +27 -2
  6. package/src/Platform.res.mjs +24 -4
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
  11. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
  12. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
  13. package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
  14. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
  15. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
  16. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
  17. package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
  18. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
  19. package/src/adapter/LocalBus.res +32 -5
  20. package/src/adapter/LocalBus.res.mjs +27 -7
  21. package/src/adapter/LocalEventLogRead.res +477 -0
  22. package/src/adapter/LocalEventLogRead.res.mjs +507 -0
  23. package/src/adapter/LocalEventTap.res +18 -80
  24. package/src/adapter/LocalEventTap.res.mjs +19 -108
  25. package/src/adapter/LocalFeed.res +433 -0
  26. package/src/adapter/LocalFeed.res.mjs +419 -0
  27. package/src/adapter/LocalLogFeed.res +217 -0
  28. package/src/adapter/LocalLogFeed.res.mjs +295 -0
  29. package/src/adapter/LocalPgEventLogRead.res +85 -0
  30. package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
  31. package/src/adapter/LocalPlatformObserve.res +456 -0
  32. package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
  33. package/src/adapter/LocalTapLines.res +149 -0
  34. package/src/adapter/LocalTapLines.res.mjs +232 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
  38. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  39. package/src/components/LocalAutomationSchedules.res.mjs +1 -1
  40. package/src/components/LocalAutomationSweep.res.mjs +1 -1
  41. package/src/reexport/CommandTopic.res.mjs +5 -2
  42. package/tests/LocalPlatformRegistryTest.res +17 -0
  43. package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
  44. package/tests/LocalPlatformStartTest.res.mjs +1 -1
  45. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  46. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  47. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  48. package/tests/adapter/LocalFeedTest.res +314 -0
  49. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  50. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  51. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  52. package/tests/adapter/LocalTapLinesTest.res +207 -0
  53. package/tests/adapter/LocalTapLinesTest.res.mjs +274 -0
  54. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  55. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  56. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  57. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  58. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  59. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  60. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  61. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  62. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  63. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  64. package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
@@ -0,0 +1,232 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
5
+ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
6
+ import * as LogRedaction$Reventless from "@reventlessdev/reventless-spec/src/util/LogRedaction.res.mjs";
7
+ import * as ViewChange$ReventlessCore from "@reventlessdev/reventless-core/src/ViewChange.res.mjs";
8
+ import * as LocalLogFeed$ReventlessLocal from "./LocalLogFeed.res.mjs";
9
+ import * as LocalEventTap$ReventlessLocal from "./LocalEventTap.res.mjs";
10
+ import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
11
+
12
+ function str(prim) {
13
+ return prim;
14
+ }
15
+
16
+ function now() {
17
+ return new Date().toISOString();
18
+ }
19
+
20
+ function lineOf(fields) {
21
+ return JSON.stringify(Object.fromEntries(fields));
22
+ }
23
+
24
+ function optional(name, value) {
25
+ return Stdlib_Option.mapOr(value, [], v => [[
26
+ name,
27
+ v
28
+ ]]);
29
+ }
30
+
31
+ function compOf(r) {
32
+ return `CommandTopic(` + r.component + `)`;
33
+ }
34
+
35
+ function pluginOf(r) {
36
+ return Logger$ReventlessCore.resolvePlugin(compOf(r), undefined);
37
+ }
38
+
39
+ function outcomeLine(r) {
40
+ let plugin = pluginOf(r);
41
+ let head = [
42
+ [
43
+ "kind",
44
+ "commandOutcome"
45
+ ],
46
+ [
47
+ "component",
48
+ r.component
49
+ ]
50
+ ].concat(optional("plugin", plugin)).concat([[
51
+ "msgId",
52
+ r.reference
53
+ ]]).concat(optional("correlationId", r.correlationId)).concat(optional("causationId", r.causationId));
54
+ let match = r.outcome;
55
+ let body;
56
+ body = match.TAG === "OutcomeAccepted" ? [[
57
+ "outcome",
58
+ "accepted"
59
+ ]].concat(optional("entityId", match.entityId)).concat([[
60
+ "eventCount",
61
+ match.eventCount
62
+ ]]) : [
63
+ [
64
+ "outcome",
65
+ "rejected"
66
+ ],
67
+ [
68
+ "errorCode",
69
+ match.errorCode
70
+ ],
71
+ [
72
+ "cause",
73
+ CommandTopic_Helpers$ReventlessCore.refusalCauseToGraphQL(match.cause)
74
+ ]
75
+ ];
76
+ return lineOf(head.concat(body).concat([[
77
+ "ts",
78
+ new Date().toISOString()
79
+ ]]));
80
+ }
81
+
82
+ function rejectionRecord(r) {
83
+ let match = r.outcome;
84
+ if (match.TAG === "OutcomeAccepted") {
85
+ return;
86
+ }
87
+ let cause = match.cause;
88
+ let errorDetail = match.errorDetail;
89
+ let level;
90
+ switch (cause) {
91
+ case "InfrastructureFailure" :
92
+ level = "Error";
93
+ break;
94
+ case "DomainRejection" :
95
+ case "AccessRefusal" :
96
+ level = "Warn";
97
+ break;
98
+ }
99
+ let detailJson;
100
+ if (errorDetail === "") {
101
+ detailJson = [];
102
+ } else {
103
+ let parsed;
104
+ try {
105
+ parsed = JSON.parse(errorDetail);
106
+ } catch (exn) {
107
+ parsed = errorDetail;
108
+ }
109
+ detailJson = [[
110
+ "errorDetail",
111
+ Logger$ReventlessCore.truncateDetail(LogRedaction$Reventless.redactJson(parsed))
112
+ ]];
113
+ }
114
+ let annotations = Object.fromEntries([[
115
+ "msgId",
116
+ r.reference
117
+ ]]);
118
+ Stdlib_Option.forEach(r.correlationId, id => {
119
+ annotations["correlationId"] = id;
120
+ });
121
+ Stdlib_Option.forEach(r.causationId, id => {
122
+ annotations["causationId"] = id;
123
+ });
124
+ return {
125
+ time: new Date().toISOString(),
126
+ level: level,
127
+ message: "command rejected",
128
+ service: Logger$ReventlessCore.serviceName(),
129
+ plugin: pluginOf(r),
130
+ comp: compOf(r),
131
+ annotations: annotations,
132
+ detail: Object.fromEntries([
133
+ [
134
+ "component",
135
+ r.component
136
+ ],
137
+ [
138
+ "errorCode",
139
+ match.errorCode
140
+ ],
141
+ [
142
+ "cause",
143
+ CommandTopic_Helpers$ReventlessCore.refusalCauseToGraphQL(cause)
144
+ ]
145
+ ].concat(detailJson))
146
+ };
147
+ }
148
+
149
+ function onOutcome(r) {
150
+ Stdlib_Option.forEach(rejectionRecord(r), LocalLogFeed$ReventlessLocal.keepProblem);
151
+ if (LocalEventTap$ReventlessLocal.hasReaders()) {
152
+ return LocalEventTap$ReventlessLocal.publish({
153
+ line: outcomeLine(r),
154
+ facets: {
155
+ plugin: pluginOf(r),
156
+ correlationId: r.correlationId
157
+ }
158
+ });
159
+ }
160
+ }
161
+
162
+ function viewChangeLine(c) {
163
+ let side = (name, value) => Stdlib_Option.mapOr(value, [], v => [[
164
+ name,
165
+ Logger$ReventlessCore.truncateDetail(LogRedaction$Reventless.redactJson(v))
166
+ ]]);
167
+ return lineOf([[
168
+ "kind",
169
+ "viewChange"
170
+ ]].concat(ViewChange$ReventlessCore.summaryFields(c)).concat(optional("correlationId", c.correlationId)).concat(side("before", c.before)).concat(side("after", c.after)).concat([[
171
+ "ts",
172
+ new Date().toISOString()
173
+ ]]));
174
+ }
175
+
176
+ function onViewChange(c) {
177
+ if (LocalEventTap$ReventlessLocal.hasReaders()) {
178
+ return LocalEventTap$ReventlessLocal.publish({
179
+ line: viewChangeLine(c),
180
+ facets: {
181
+ plugin: Logger$ReventlessCore.resolvePlugin(`View(` + c.view + `)`, undefined),
182
+ correlationId: c.correlationId
183
+ }
184
+ });
185
+ }
186
+ }
187
+
188
+ let removals = {
189
+ contents: []
190
+ };
191
+
192
+ function install() {
193
+ if (Primitive_object.equal(removals.contents, [])) {
194
+ removals.contents = [
195
+ CommandTopic_Helpers$ReventlessCore.registerCommandOutcome(onOutcome),
196
+ ViewChange$ReventlessCore.register(onViewChange)
197
+ ];
198
+ return;
199
+ }
200
+ }
201
+
202
+ function resetForTests() {
203
+ removals.contents.forEach(remove => remove());
204
+ removals.contents = [];
205
+ }
206
+
207
+ let Logger;
208
+
209
+ let Outcome;
210
+
211
+ let ViewChange;
212
+
213
+ export {
214
+ Logger,
215
+ Outcome,
216
+ ViewChange,
217
+ str,
218
+ now,
219
+ lineOf,
220
+ optional,
221
+ compOf,
222
+ pluginOf,
223
+ outcomeLine,
224
+ rejectionRecord,
225
+ onOutcome,
226
+ viewChangeLine,
227
+ onViewChange,
228
+ removals,
229
+ install,
230
+ resetForTests,
231
+ }
232
+ /* Logger-ReventlessCore Not a pure module */
@@ -8,8 +8,8 @@
8
8
  // Does NOT include Relay-specific features (encodeGlobalId, node resolver)
9
9
  // since admin queries are not Relay-paginated.
10
10
  //
11
- // Authorization: every query AND mutation resolver registered here is
12
- // wrapped with `Auth_GraphqlContext.requireGroup(~group="Admin")` so
11
+ // Authorization: every query, mutation AND subscription resolver registered
12
+ // here is wrapped with `Auth_GraphqlContext.requireGroup(~group="Admin")` so
13
13
  // non-Admin identities are refused before the underlying resolver runs.
14
14
  // Mirrors AppSync's `@aws_cognito_user_pools(cognito_groups: ["Admin"])`
15
15
  // directive that gates Platform_* fields in the AWS adapter.
@@ -46,11 +46,44 @@ let wrapAdmin = (resolvers: dict<ReventlessGraphqlServer.GraphQL_ServerInstance.
46
46
  wrapped
47
47
  }
48
48
 
49
+ // A subscription field's resolver is an object: `subscribe` starts the stream,
50
+ // `resolve` maps each payload. The group check goes on `subscribe`, so a caller
51
+ // without the group is refused before anything is streamed to it.
52
+ type subscriptionResolver = {
53
+ subscribe: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn,
54
+ resolve?: JSON.t => JSON.t,
55
+ }
56
+ external asSubscription: ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn => subscriptionResolver =
57
+ "%identity"
58
+ external ofSubscription: subscriptionResolver => ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn =
59
+ "%identity"
60
+
61
+ let wrapAdminSubscriptions = (
62
+ resolvers: dict<ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn>,
63
+ ): dict<ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn> =>
64
+ resolvers
65
+ ->Dict.toArray
66
+ ->Array.map(((k, v)) => {
67
+ let sub = asSubscription(v)
68
+ (
69
+ k,
70
+ ofSubscription({
71
+ ...sub,
72
+ subscribe: Auth_GraphqlContext.requireGroup(
73
+ ~group=Reventless.Role.adminGroup(),
74
+ sub.subscribe,
75
+ ),
76
+ }),
77
+ )
78
+ })
79
+ ->Dict.fromArray
80
+
49
81
  let registerMutations = (~sdlFields, ~resolvers) =>
50
82
  instance.registerMutations(~sdlFields, ~resolvers=wrapAdmin(resolvers))
51
83
  let registerQueries = (~sdlFields, ~resolvers) =>
52
84
  instance.registerQueries(~sdlFields, ~resolvers=wrapAdmin(resolvers))
53
- let registerSubscriptions = instance.registerSubscriptions
85
+ let registerSubscriptions = (~sdlFields, ~resolvers) =>
86
+ instance.registerSubscriptions(~sdlFields, ~resolvers=wrapAdminSubscriptions(resolvers))
54
87
  let registerTypes = instance.registerTypes
55
88
  // Not admin-wrapped: a field resolver is only reached through a parent field
56
89
  // that already met the group check, so wrapping would re-refuse a caller the
@@ -14,6 +14,17 @@ function wrapAdmin(resolvers) {
14
14
  return wrapped;
15
15
  }
16
16
 
17
+ function wrapAdminSubscriptions(resolvers) {
18
+ return Object.fromEntries(Object.entries(resolvers).map(param => {
19
+ let v = param[1];
20
+ let newrecord = {...v};
21
+ return [
22
+ param[0],
23
+ (newrecord.subscribe = Auth_GraphqlContext$ReventlessLocal.requireGroup(Role$Reventless.adminGroup(), v.subscribe), newrecord)
24
+ ];
25
+ }));
26
+ }
27
+
17
28
  function registerMutations(sdlFields, resolvers) {
18
29
  instance.registerMutations(sdlFields, wrapAdmin(resolvers));
19
30
  }
@@ -22,7 +33,9 @@ function registerQueries(sdlFields, resolvers) {
22
33
  instance.registerQueries(sdlFields, wrapAdmin(resolvers));
23
34
  }
24
35
 
25
- let registerSubscriptions = instance.registerSubscriptions;
36
+ function registerSubscriptions(sdlFields, resolvers) {
37
+ instance.registerSubscriptions(sdlFields, wrapAdminSubscriptions(resolvers));
38
+ }
26
39
 
27
40
  let registerTypes = instance.registerTypes;
28
41
 
@@ -69,6 +82,7 @@ let asInterface = {
69
82
  export {
70
83
  instance,
71
84
  wrapAdmin,
85
+ wrapAdminSubscriptions,
72
86
  registerMutations,
73
87
  registerQueries,
74
88
  registerSubscriptions,
@@ -273,7 +273,7 @@ async function runCatchup(db, upperBound, dcbUpperBound, handlers) {
273
273
  if (checkpoint < bound) {
274
274
  let rows = missedRows(db, axis, checkpoint, bound);
275
275
  if (rows.length !== 0) {
276
- Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, undefined, `catch-up: delivering ` + rows.length.toString() + ` missed ` + axisLabel(axis) + ` event(s) (positions ` + checkpoint.toString() + `..` + bound.toString() + `] to ` + name));
276
+ Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, undefined, undefined, `catch-up: delivering ` + rows.length.toString() + ` missed ` + axisLabel(axis) + ` event(s) (positions ` + checkpoint.toString() + `..` + bound.toString() + `] to ` + name));
277
277
  }
278
278
  for (let j = 0, j_finish = rows.length; j < j_finish; ++j) {
279
279
  let match$2 = rows[j];
@@ -283,10 +283,10 @@ async function runCatchup(db, upperBound, dcbUpperBound, handlers) {
283
283
  try {
284
284
  await handler(envelope, undefined);
285
285
  } catch (exn) {
286
- Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `catch-up: ` + name + ` failed on stored ` + axisLabel(axis) + ` event at position ` + pos.toString() + ` — skipped`));
286
+ Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, undefined, `catch-up: ` + name + ` failed on stored ` + axisLabel(axis) + ` event at position ` + pos.toString() + ` — skipped`));
287
287
  }
288
288
  } else {
289
- Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, `catch-up: unreadable stored ` + axisLabel(axis) + ` event at position ` + pos.toString() + ` — skipped`));
289
+ Effect.runSync(EffectLogger$ReventlessCore.logWarn(comp, undefined, undefined, `catch-up: unreadable stored ` + axisLabel(axis) + ` event at position ` + pos.toString() + ` — skipped`));
290
290
  }
291
291
  }
292
292
  }
@@ -59,7 +59,7 @@ function Make(Bus) {
59
59
  return await ops.tick(key, at);
60
60
  });
61
61
  Stdlib_Promise.$$catch(TestRunner$ReventlessLocal.resolve(Component$ReventlessCore.operations(component)).then(ops => ops.reconcileSchedules()), exn => {
62
- Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, `the schedules could not be reconciled: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown")));
62
+ Effect.runSync(EffectLogger$ReventlessCore.logError(`AutomationSlice(` + Spec.name + `)`, undefined, undefined, `the schedules could not be reconciled: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown")));
63
63
  return Promise.resolve();
64
64
  });
65
65
  return component;
@@ -18,7 +18,7 @@ function timerName(slice, key) {
18
18
  }
19
19
 
20
20
  function tickFailed(slice, key, message) {
21
- Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSchedules", undefined, `tick ` + key + ` of ` + slice + ` failed: ` + message));
21
+ Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSchedules", undefined, undefined, `tick ` + key + ` of ` + slice + ` failed: ` + message));
22
22
  }
23
23
 
24
24
  function register(slice, tick) {
@@ -40,7 +40,7 @@ async function sweepOnce() {
40
40
  } catch (raw_exn) {
41
41
  let exn = Primitive_exceptions.internalToException(raw_exn);
42
42
  let message = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
43
- return Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSweep", undefined, `sweep of ` + param[0] + ` failed: ` + message));
43
+ return Effect.runSync(EffectLogger$ReventlessCore.logError("LocalAutomationSweep", undefined, undefined, `sweep of ` + param[0] + ` failed: ` + message));
44
44
  }
45
45
  });
46
46
  running.contents = false;
@@ -8,7 +8,9 @@ let NotPublishedToChannel = CommandTopic$ReventlessCore.NotPublishedToChannel;
8
8
 
9
9
  let refusalCauseToGraphQL = CommandTopic$ReventlessCore.refusalCauseToGraphQL;
10
10
 
11
- let commandOutcomeHook = CommandTopic$ReventlessCore.commandOutcomeHook;
11
+ let commandOutcomeHooks = CommandTopic$ReventlessCore.commandOutcomeHooks;
12
+
13
+ let nextOutcomeHookId = CommandTopic$ReventlessCore.nextOutcomeHookId;
12
14
 
13
15
  let registerCommandOutcome = CommandTopic$ReventlessCore.registerCommandOutcome;
14
16
 
@@ -50,7 +52,8 @@ export {
50
52
  componentType,
51
53
  NotPublishedToChannel,
52
54
  refusalCauseToGraphQL,
53
- commandOutcomeHook,
55
+ commandOutcomeHooks,
56
+ nextOutcomeHookId,
54
57
  registerCommandOutcome,
55
58
  clearCommandOutcome,
56
59
  fireCommandOutcome,
@@ -199,4 +199,21 @@ describe("LocalPlatformRegistry.tapPort", () => {
199
199
 
200
200
  expect(LocalPlatformRegistry.list(~cwd, ())->Array.map(e => e.tapPort))->toEqual([Some(4100)])
201
201
  })
202
+
203
+ testSync("names where the platform API is served, and keeps it when a port is published", () => {
204
+ let cwd = tempRoot()
205
+ let _ = LocalPlatformRegistry.write(
206
+ ~port=4000,
207
+ ~endpoint="http://localhost:4000/graphql",
208
+ ~loginEndpoint="http://localhost:4000/__inmemory/login",
209
+ ~store=memoryStore,
210
+ ~platformEndpoint="http://localhost:4001/graphql",
211
+ ~cwd,
212
+ )
213
+ LocalPlatformRegistry.publishTapPort(~port=4000, ~tapPort=4100, ~cwd)
214
+
215
+ expect(
216
+ LocalPlatformRegistry.list(~cwd, ())->Array.map(e => (e.platformEndpoint, e.tapPort)),
217
+ )->toEqual([(Some("http://localhost:4001/graphql"), Some(4100))])
218
+ })
202
219
  })
@@ -24,7 +24,7 @@ let memoryStore = {
24
24
 
25
25
  function writeAt(cwd, port, store, pidOpt) {
26
26
  let pid = pidOpt !== undefined ? pidOpt : process.pid;
27
- return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, pid, cwd);
27
+ return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, undefined, pid, cwd);
28
28
  }
29
29
 
30
30
  globalThis.describe("LocalPlatformRegistry", () => {
@@ -173,9 +173,21 @@ globalThis.describe("LocalPlatformRegistry.tapPort", () => {
173
173
  globalThis.test("ignores a platform that never published an entry", () => LocalPlatformRegistry$ReventlessLocal.publishTapPort(4000, 1, tempRoot()));
174
174
  globalThis.test("round-trips the port a platform serving a tap publishes", () => {
175
175
  let cwd = tempRoot();
176
- LocalPlatformRegistry$ReventlessLocal.write(4000, "http://localhost:4000/graphql", "http://localhost:4000/__inmemory/login", memoryStore, 4100, undefined, cwd);
176
+ LocalPlatformRegistry$ReventlessLocal.write(4000, "http://localhost:4000/graphql", "http://localhost:4000/__inmemory/login", memoryStore, 4100, undefined, undefined, cwd);
177
177
  globalThis.expect(LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined).map(e => e.tapPort)).toEqual([4100]);
178
178
  });
179
+ globalThis.test("names where the platform API is served, and keeps it when a port is published", () => {
180
+ let cwd = tempRoot();
181
+ LocalPlatformRegistry$ReventlessLocal.write(4000, "http://localhost:4000/graphql", "http://localhost:4000/__inmemory/login", memoryStore, undefined, "http://localhost:4001/graphql", undefined, cwd);
182
+ LocalPlatformRegistry$ReventlessLocal.publishTapPort(4000, 4100, cwd);
183
+ globalThis.expect(LocalPlatformRegistry$ReventlessLocal.list(cwd, undefined).map(e => [
184
+ e.platformEndpoint,
185
+ e.tapPort
186
+ ])).toEqual([[
187
+ "http://localhost:4001/graphql",
188
+ 4100
189
+ ]]);
190
+ });
179
191
  });
180
192
 
181
193
  let deadPid = 2147483646;
@@ -28,7 +28,7 @@ let memoryStore = {
28
28
 
29
29
  function writeAt(cwd, port, store, pidOpt) {
30
30
  let pid = pidOpt !== undefined ? pidOpt : process.pid;
31
- return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, pid, cwd);
31
+ return LocalPlatformRegistry$ReventlessLocal.write(port, `http://localhost:` + port.toString() + `/graphql`, `http://localhost:` + port.toString() + `/__inmemory/login`, store, undefined, undefined, pid, cwd);
32
32
  }
33
33
 
34
34
  function withoutBypass(f) {
@@ -0,0 +1,46 @@
1
+ // A graphql-ws client for the subscription tests, here rather than in an
2
+ // `Obj.magic` inside the test, per the repo convention that untyped reflection
3
+ // lives in a companion `.mjs`.
4
+ import { createClient } from 'graphql-ws'
5
+ import WebSocket from 'ws'
6
+
7
+ /** Subscribes, collects values until `count` arrive (or an error or `timeoutMs`),
8
+ calling `onStarted` once the server acknowledged the connection. Resolves
9
+ `{ values, error }`; `error` is the first error message, if any. */
10
+ export const collect = (url, query, connectionParams, count, timeoutMs, onStarted) =>
11
+ new Promise(resolve => {
12
+ const values = []
13
+ let settled = false
14
+ const client = createClient({
15
+ url,
16
+ webSocketImpl: WebSocket,
17
+ connectionParams,
18
+ lazy: false,
19
+ retryAttempts: 0,
20
+ on: { connected: () => onStarted() },
21
+ })
22
+ const finish = error => {
23
+ if (settled) return
24
+ settled = true
25
+ clearTimeout(timer)
26
+ dispose()
27
+ client.dispose()
28
+ resolve({ values, error })
29
+ }
30
+ const timer = setTimeout(() => finish(undefined), timeoutMs)
31
+ const dispose = client.subscribe(
32
+ { query },
33
+ {
34
+ next: msg => {
35
+ if (msg.errors && msg.errors.length > 0) return finish(String(msg.errors[0].message))
36
+ values.push(Object.values(msg.data)[0])
37
+ if (values.length >= count) finish(undefined)
38
+ },
39
+ error: err => {
40
+ const m = Array.isArray(err) ? err[0]?.message : err?.message ?? err?.reason ?? String(err)
41
+ finish(String(m))
42
+ },
43
+ complete: () => finish(undefined),
44
+ },
45
+ )
46
+ })