@reventlessdev/reventless-local 3.0.0-alpha.248 → 3.0.0-alpha.250
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 +14 -14
- package/src/BakedManifest.res +5 -6
- package/src/EmitCapabilities.res +3 -1
- package/src/LocalPlatformRegistry.res +3 -1
- package/src/LocalPlatformStart.res +7 -6
- package/src/LocalSeedTarget.res +3 -1
- package/src/Platform.res +291 -215
- package/src/ShellConfig.res +10 -17
- package/src/UiHints.res +8 -14
- package/src/UiSlots.res +10 -17
- package/src/adapter/Api/LocalEvents_Server.res +5 -3
- package/src/adapter/Api/LocalGraphQL_Adapter.res +4 -4
- package/src/adapter/Auth/LocalAuth.res +11 -15
- package/src/adapter/Auth/UserStore.res +5 -7
- package/src/adapter/Backend.res +1 -2
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +42 -26
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +13 -3
- package/src/adapter/CommandTopic/LocalCommandTopicChannel.res +15 -17
- package/src/adapter/CommandTopic/LocalCommandTopicRemoteChannel.res +18 -22
- package/src/adapter/Counter/LocalCounterHandler.res +1 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +33 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +57 -57
- package/src/adapter/DomainGraphQL_Server.res +35 -34
- package/src/adapter/DomainMCP_Server.res +45 -41
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +47 -36
- package/src/adapter/EventHistory/EventHistoryResolvers_GraphQL.res +15 -12
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +9 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +36 -24
- package/src/adapter/EventTopic/LocalEventTopicPublisher.res +9 -3
- package/src/adapter/Heartbeat/LocalHeartbeatRunner.res +9 -14
- package/src/adapter/LocalBus.res +12 -12
- package/src/adapter/LocalCapabilities.res.mjs +1 -1
- package/src/adapter/LocalGeocodeResolvers.res +1 -4
- package/src/adapter/LocalSideEffectHandler.res +1 -1
- package/src/adapter/LocalStateChangeDescriptor.res +2 -1
- package/src/adapter/MCP_ServerInstance.res +51 -39
- package/src/adapter/ObjectStore/LocalObjectStore.res +5 -2
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +1 -2
- package/src/adapter/PlatformGraphQL_Server.res +9 -5
- package/src/adapter/ProjectionCheckpoint.res +8 -1
- package/src/adapter/ProjectionPending.res +1 -3
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +2 -1
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +367 -269
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +5 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +63 -40
- package/src/adapter/QueryDb/SortKey_Filter.res +14 -6
- package/src/adapter/Runtime/LocalAggregateRuntime_Builder.res +12 -10
- package/src/adapter/Runtime/LocalEventCollectorRuntime_Builder.res +8 -2
- package/src/adapter/Runtime/LocalPluginRuntime_Builder.res +1 -4
- package/src/adapter/Scheduler/LocalScheduledPublisher.res +12 -12
- package/src/adapter/SqliteDriver.res +16 -4
- package/src/adapter/Task/TaskBucket_Sqlite.res +5 -6
- package/src/components/Aggregate_Builder.res +6 -1
- package/src/components/Counter_Builder.res +1 -5
- package/src/components/DcbEventLog_Builder.res +5 -3
- package/src/components/ExtensionPoint_Builder.res +13 -15
- package/src/components/InboundTranslationSlice_Builder.res +3 -2
- package/src/components/OutboundTranslationSlice_Builder.res +3 -2
- package/src/components/ReadModel_Builder.res +18 -20
- package/src/components/StateViewSlice_Builder.res +3 -2
- package/src/components/Task_Builder.res +12 -13
- package/src/reset/LocalSeedReset.res +2 -5
- package/src/test/Mocks/MockDcbEventLogStorage.res +5 -1
- package/src/test/Mocks/MockEventLogStorage.res +8 -3
- package/src/test/Mocks/MockQueryDbStorage.res +3 -2
- package/src/test/TestRunner.res +1 -1
- package/tests/BakedManifestFilesTest.res +7 -10
- package/tests/LocalEventTapTest.res +26 -21
- package/tests/LocalPlatformStartTest.res +33 -28
- package/tests/PluginEventDecodeTest.res +3 -3
- package/tests/ShellConfigTest.res +30 -21
- package/tests/SplitApiFixtures.res +34 -10
- package/tests/SplitApiTest.res +92 -62
- package/tests/adapter/BackendParityTest.res +180 -130
- package/tests/adapter/CollectNEventsTest.res +6 -4
- package/tests/adapter/CommandAuthorizationTest.res +47 -41
- package/tests/adapter/CommandTopicChannelTest.res +163 -139
- package/tests/adapter/CounterHandlerTest.res +95 -77
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +171 -168
- package/tests/adapter/DcbEventLogStorageTest.res +264 -207
- package/tests/adapter/DomainGraphQL_ServerTest.res +53 -43
- package/tests/adapter/EventCollectorChannelTest.res +153 -124
- package/tests/adapter/EventHistoryResolverTest.res +46 -18
- package/tests/adapter/EventLogSnapshotParityTest.res +57 -45
- package/tests/adapter/EventLogStorageSqliteTest.res +9 -10
- package/tests/adapter/EventTapTest.res +37 -20
- package/tests/adapter/EventTopicPublisherTest.res +74 -50
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +4 -3
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +666 -568
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +105 -96
- package/tests/adapter/HeartbeatRunnerTest.res +99 -81
- package/tests/adapter/InboundTranslationMutationTest.res +4 -1
- package/tests/adapter/LocalAuthHttpTest.res +79 -85
- package/tests/adapter/LocalAuthLoginTest.res +42 -50
- package/tests/adapter/LocalAuthTest.res +8 -16
- package/tests/adapter/LocalAuthUserStoreTest.res +2 -7
- package/tests/adapter/LocalBusBoundedTest.res +187 -117
- package/tests/adapter/LocalBusPubSubTest.res +118 -76
- package/tests/adapter/LocalDispatchContextTest.res +1 -2
- package/tests/adapter/LocalEvents_ServerTest.res +242 -187
- package/tests/adapter/PgProjectionCatchupTest.res +44 -10
- package/tests/adapter/ProjectionCheckpointTest.res +261 -206
- package/tests/adapter/QueryDbGsiTtlTest.res +123 -83
- package/tests/adapter/QueryDbListPushdownParityTest.res +107 -67
- package/tests/adapter/QueryDbListResolverTest.res +225 -208
- package/tests/adapter/QueryDbResolvedFieldsTest.res +17 -14
- package/tests/adapter/QueryDbStorageSqliteTest.res +33 -49
- package/tests/adapter/QueryDbStorageTest.res +163 -117
- package/tests/adapter/QueryDbUnionRoundTripTest.res +8 -10
- package/tests/adapter/QueryEngineTest.res +179 -126
- package/tests/adapter/ScheduledPublisherTest.res +171 -135
- package/tests/adapter/ServedBucketHttpTest.res +65 -59
- package/tests/adapter/SortKey_FilterTest.res +19 -3
- package/tests/adapter/SqliteDriverTest.res +13 -10
- package/tests/adapter/TaskBucketTest.res +94 -80
- package/tests/components/aggregate/AggregateTest.res +31 -18
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +259 -222
- package/tests/components/automationslice/AutomationSliceFixtures.res +4 -2
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +38 -38
- package/tests/components/automationslice/MixedSourceAutomationSliceFixtures.res +12 -14
- package/tests/components/automationslice/MixedSourceAutomationSliceTest.res +8 -32
- package/tests/components/commandgenerator/CommandGeneratorFixtures.res +7 -5
- package/tests/components/commandgenerator/CommandGeneratorTest.res +40 -31
- package/tests/components/commandtopic/CommandTopicFixtures.res +3 -4
- package/tests/components/commandtopic/CommandTopicStreamFixtures.res +7 -5
- package/tests/components/commandtopic/CommandTopicTest.res +24 -15
- package/tests/components/counter/CounterFixtures.res +6 -8
- package/tests/components/dcb/DcbCrossPartitionFixtures.res +8 -2
- package/tests/components/dcb/DcbCrossPartitionReadTest.res +20 -13
- package/tests/components/dcb/DcbEventLogAppendStreamTest.res +12 -8
- package/tests/components/dcb/DcbEventLogStreamTest.res +88 -61
- package/tests/components/dcb/DcbFixtures.res +4 -1
- package/tests/components/dcb/DcbTest.res +9 -6
- package/tests/components/eventcollector/EventCollectorFixtures.res +0 -1
- package/tests/components/eventcollector/EventCollectorTest.res +6 -3
- package/tests/components/eventlog/EventLogAppendStreamTest.res +88 -68
- package/tests/components/eventlog/EventLogConflictTest.res +8 -4
- package/tests/components/eventlog/EventLogFixtures.res +3 -3
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res +26 -13
- package/tests/components/eventlog/EventLogStreamTest.res +119 -56
- package/tests/components/eventlog/EventLogTest.res +20 -4
- package/tests/components/eventtopic/EventTopicFixtures.res +3 -3
- package/tests/components/eventtopic/EventTopicStreamFixtures.res +1 -2
- package/tests/components/eventtopic/EventTopicStreamTest.res +7 -1
- package/tests/components/extensionpoint/ExtensionPointFixtures.res +14 -9
- package/tests/components/extensionpoint/ExtensionPointTest.res +3 -2
- package/tests/components/heartbeat/HeartbeatFixtures.res +5 -6
- package/tests/components/heartbeat/HeartbeatTest.res +56 -47
- package/tests/components/inboundtranslationslice/InboundTranslationSliceCallbackTest.res +271 -249
- package/tests/components/inboundtranslationslice/InboundTranslationSliceFixtures.res +3 -2
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +361 -276
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceFixtures.res +8 -10
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePlatformFixtures.res +37 -43
- package/tests/components/querydb/QueryDbFixtures.res +1 -5
- package/tests/components/querydb/QueryDbTest.res +2 -4
- package/tests/components/readmodel/DcbReadModelE2EFixtures.res +15 -18
- package/tests/components/readmodel/MixedSourceReadModelFixtures.res +5 -7
- package/tests/components/readmodel/ReadModelFixtures.res +8 -8
- package/tests/components/scheduler/SchedulerTest.res +76 -58
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res +4 -4
- package/tests/components/stateviewslice/StateViewSliceDisplayNameFixtures.res +5 -7
- package/tests/components/stateviewslice/StateViewSliceFixtures.res +8 -6
- package/tests/components/stateviewslice/StateViewSliceSubIdFixtures.res +17 -11
- package/tests/components/stateviewslice/StateViewSliceSubIdTest.res +12 -2
- package/tests/components/task/TaskFixtures.res +5 -5
- package/tests/components/task/TaskTest.res +95 -80
- package/tests/plugin/PluginsProjection_GWT.res +3 -1
- package/tests/plugin/UiFragments_GWT.res +6 -1
- package/tests/reset/LocalSeedResetTest.res +1 -0
|
@@ -10,11 +10,11 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
10
10
|
type channelParts = unit
|
|
11
11
|
type runtimeParts = LocalRuntimeEnvironment.parts
|
|
12
12
|
|
|
13
|
-
let make: ReventlessCore.EventCollector_Adapter.channelMaker<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
) => {
|
|
13
|
+
let make: ReventlessCore.EventCollector_Adapter.channelMaker<
|
|
14
|
+
callbackEvent,
|
|
15
|
+
'context,
|
|
16
|
+
channelParts,
|
|
17
|
+
> = (~name as _, ~eventTopics, ~owner as _, ~opts as _) => {
|
|
18
18
|
// Collect all event topic resources as our channel resources
|
|
19
19
|
let eventTopicResources =
|
|
20
20
|
eventTopics
|
|
@@ -26,8 +26,8 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
26
26
|
resources: eventTopicResources,
|
|
27
27
|
enqueueEvent: ((_, _, _) => Promise.resolve())->Pulumi.Output.make,
|
|
28
28
|
handleChannelEvent: (handleEvents: ReventlessCore.EventCollector.jsonEventsHandler) =>
|
|
29
|
-
(
|
|
30
|
-
handleEvents(Stream.fromIterable([json]))->Effect.map(_ => ())
|
|
29
|
+
(
|
|
30
|
+
(json: JSON.t, _ctx) => handleEvents(Stream.fromIterable([json]))->Effect.map(_ => ())
|
|
31
31
|
)->Pulumi.Output.make,
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -43,43 +43,54 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
43
43
|
let _reg =
|
|
44
44
|
runtime.parts.handlerDeferred
|
|
45
45
|
->Deferred.await_
|
|
46
|
-
->Effect.flatMap(handler =>
|
|
46
|
+
->Effect.flatMap(handler =>
|
|
47
|
+
Effect.sync(() => Bus.registerEventCollectorHandler(name, handler))
|
|
48
|
+
)
|
|
47
49
|
->Effect.runPromise
|
|
48
50
|
->ignore
|
|
49
51
|
|
|
50
|
-
channelSpecs->Array.forEach((
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
channelSpecs->Array.forEach((
|
|
53
|
+
{eventTopics}: ReventlessCore.EventCollector_Adapter.channelSpec<
|
|
54
|
+
callbackEvent,
|
|
55
|
+
'context,
|
|
56
|
+
channelParts,
|
|
57
|
+
>,
|
|
58
|
+
) => {
|
|
55
59
|
eventTopics
|
|
56
60
|
->Dict.valuesToArray
|
|
57
61
|
->Array.forEach((topicOutputs: ReventlessCore.EventTopic.outputs) => {
|
|
58
|
-
topicOutputs.resources->Array.forEach(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
62
|
+
topicOutputs.resources->Array.forEach(
|
|
63
|
+
resource => {
|
|
64
|
+
// resource.name is the bus topic key set by LocalEventTopicPublisher
|
|
65
|
+
let _ = resource.name->Pulumi.Output.apply(
|
|
66
|
+
topicName => {
|
|
67
|
+
// Stream-based drain: subscribeToEventStream returns a scoped Effect that
|
|
68
|
+
// yields Stream<queuedEvent>. done_ is run explicitly after each handler call
|
|
69
|
+
// to unblock publishEvent.
|
|
70
|
+
let drainEffect = Effect.scoped(
|
|
71
|
+
Bus.subscribeToEventStream(topicName)->Effect.flatMap(
|
|
72
|
+
stream =>
|
|
73
|
+
stream->Stream.runForEach(
|
|
74
|
+
msg =>
|
|
75
|
+
Effect.promise(
|
|
76
|
+
async () => {
|
|
77
|
+
let handler = await runtime.parts.handlerDeferred
|
|
78
|
+
->Deferred.await_
|
|
79
|
+
->Effect.runPromise
|
|
80
|
+
await handler(msg.json, ())
|
|
81
|
+
},
|
|
82
|
+
)->Effect.zipRight(msg.done_),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
74
85
|
)
|
|
75
|
-
|
|
86
|
+
let _ = Effect.runFork(drainEffect)
|
|
87
|
+
// Signal that this topic's subscription is registered.
|
|
88
|
+
// Latch.open_ is idempotent — calling it for multiple topics is safe.
|
|
89
|
+
runtime.parts.subscriptionLatch->Latch.open_->Effect.runPromise->ignore
|
|
90
|
+
},
|
|
76
91
|
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Latch.open_ is idempotent — calling it for multiple topics is safe.
|
|
80
|
-
runtime.parts.subscriptionLatch->Latch.open_->Effect.runPromise->ignore
|
|
81
|
-
})
|
|
82
|
-
})
|
|
92
|
+
},
|
|
93
|
+
)
|
|
83
94
|
})
|
|
84
95
|
})
|
|
85
96
|
[]
|
|
@@ -123,10 +123,12 @@ let readFilter = (args: JSON.t): filter => {
|
|
|
123
123
|
entityId: ?s("entityId"),
|
|
124
124
|
tagKey: ?s("tagKey"),
|
|
125
125
|
tagValue: ?s("tagValue"),
|
|
126
|
-
eventTypes: ?
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
eventTypes: ?(
|
|
127
|
+
f
|
|
128
|
+
->Dict.get("eventTypes")
|
|
129
|
+
->Option.flatMap(JSON.Decode.array)
|
|
130
|
+
->Option.map(a => a->Array.filterMap(JSON.Decode.string))
|
|
131
|
+
),
|
|
130
132
|
user: ?s("user"),
|
|
131
133
|
timeFrom: ?s("timeFrom"),
|
|
132
134
|
timeTo: ?s("timeTo"),
|
|
@@ -193,7 +195,10 @@ let matchesTag = (r: record, f: filter): bool =>
|
|
|
193
195
|
|
|
194
196
|
let matchesFilter = (r: record, f: filter): bool => {
|
|
195
197
|
let userOf = () =>
|
|
196
|
-
r.meta
|
|
198
|
+
r.meta
|
|
199
|
+
->JSON.Decode.object
|
|
200
|
+
->Option.flatMap(d => d->Dict.get("user"))
|
|
201
|
+
->Option.flatMap(JSON.Decode.string)
|
|
197
202
|
let timeOf = () =>
|
|
198
203
|
r.meta
|
|
199
204
|
->JSON.Decode.object
|
|
@@ -309,8 +314,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
309
314
|
envelopes->Array.mapWithIndex((json, i) => {
|
|
310
315
|
let obj = json->JSON.Decode.object->Option.getOr(Dict.make())
|
|
311
316
|
let event = obj->Dict.get("event")->Option.getOr(JSON.Encode.null)
|
|
312
|
-
let id =
|
|
313
|
-
obj->Dict.get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr(entityId)
|
|
317
|
+
let id = obj->Dict.get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr(entityId)
|
|
314
318
|
let meta = metaJsonFromEnvelope(obj)
|
|
315
319
|
let time =
|
|
316
320
|
meta
|
|
@@ -386,11 +390,10 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
386
390
|
// not disagree about arguments or return type. (The supporting types
|
|
387
391
|
// reach the server separately, via `schemaTypeRegistrationHook` over
|
|
388
392
|
// the fragment's `types`.)
|
|
389
|
-
let sdlFields =
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
).queryFields
|
|
393
|
+
let sdlFields = Plugin_EventQuerySchema.generate(
|
|
394
|
+
~plugin=params.pluginName,
|
|
395
|
+
~eventLogEntries=[entry],
|
|
396
|
+
).queryFields
|
|
394
397
|
|
|
395
398
|
server.registerQueries(~sdlFields, ~resolvers=Dict.fromArray([(fieldName, resolver)]))
|
|
396
399
|
}
|
|
@@ -50,8 +50,9 @@ let makeMemoryStorage = (~name as _name, ~opts as _) => {
|
|
|
50
50
|
let replay: ReventlessCore.EventLog.replay<string, JSON.t> = id =>
|
|
51
51
|
replayStream(id)->Stream.runCollect->Effect.runPromise
|
|
52
52
|
|
|
53
|
-
let latestSnapshot: ReventlessCore.EventLog.latestSnapshot<string> = async id =>
|
|
54
|
-
|
|
53
|
+
let latestSnapshot: ReventlessCore.EventLog.latestSnapshot<string> = async id => Ok(
|
|
54
|
+
snapshots->Dict.get(id),
|
|
55
|
+
)
|
|
55
56
|
|
|
56
57
|
let writeSnapshot: ReventlessCore.EventLog.writeSnapshot<string> = async (id, snap) => {
|
|
57
58
|
snapshots->Dict.set(id, snap)
|
|
@@ -60,7 +61,11 @@ let makeMemoryStorage = (~name as _name, ~opts as _) => {
|
|
|
60
61
|
|
|
61
62
|
// Appends each stream item sequentially to storage.
|
|
62
63
|
// Node.js is single-threaded so a plain ref is safe for the seqNr counter.
|
|
63
|
-
let appendStream: ReventlessCore.EventLog.appendStream<string, JSON.t> = (
|
|
64
|
+
let appendStream: ReventlessCore.EventLog.appendStream<string, JSON.t> = (
|
|
65
|
+
startingSeqNr,
|
|
66
|
+
id,
|
|
67
|
+
stream,
|
|
68
|
+
) => {
|
|
64
69
|
let seqNrRef = ref(startingSeqNr)
|
|
65
70
|
stream->Stream.runForEach(json =>
|
|
66
71
|
Stm.TRef.modify(eventsRef, events => {
|
|
@@ -78,7 +83,7 @@ let makeMemoryStorage = (~name as _name, ~opts as _) => {
|
|
|
78
83
|
switch result {
|
|
79
84
|
| Ok() =>
|
|
80
85
|
seqNrRef := seqNrRef.contents + 1
|
|
81
|
-
Effect.succeed(
|
|
86
|
+
Effect.succeed()
|
|
82
87
|
| Error(msg) => Effect.fail(msg)
|
|
83
88
|
}
|
|
84
89
|
)
|
|
@@ -58,27 +58,32 @@ let countAll = (db: SqliteDriver.t): int => {
|
|
|
58
58
|
let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
59
59
|
ensureSchema(db)
|
|
60
60
|
|
|
61
|
-
let insertStmt =
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
let insertStmt =
|
|
62
|
+
db->SqliteDriver.prepare(
|
|
63
|
+
"INSERT INTO event_log(log_name, aggregate_id, seq_nr, payload) VALUES(?,?,?,?)",
|
|
64
|
+
)
|
|
64
65
|
// Expected next seq_nr for an aggregate. Events are always appended
|
|
65
66
|
// contiguously from seq 0 (the OCC check below forbids gaps), so
|
|
66
67
|
// `MAX(seq_nr)+1` equals the row count — but it reads the rightmost leaf of the
|
|
67
68
|
// (log_name, aggregate_id, seq_nr) PK index in O(log n) instead of `COUNT(*)`
|
|
68
69
|
// scanning every row for the aggregate. `COALESCE(…, -1)+1` yields 0 when empty.
|
|
69
|
-
let nextSeqStmt =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
let nextSeqStmt =
|
|
71
|
+
db->SqliteDriver.prepare(
|
|
72
|
+
"SELECT COALESCE(MAX(seq_nr), -1) + 1 AS c FROM event_log WHERE log_name=? AND aggregate_id=?",
|
|
73
|
+
)
|
|
74
|
+
let selectByIdStmt =
|
|
75
|
+
db->SqliteDriver.prepare(
|
|
76
|
+
"SELECT payload FROM event_log WHERE log_name=? AND aggregate_id=? AND seq_nr >= ? ORDER BY seq_nr ASC",
|
|
77
|
+
)
|
|
75
78
|
let lastRowidStmt = db->SqliteDriver.prepare("SELECT last_insert_rowid() AS r")
|
|
76
|
-
let upsertSnapshotStmt =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
let upsertSnapshotStmt =
|
|
80
|
+
db->SqliteDriver.prepare(
|
|
81
|
+
"INSERT OR REPLACE INTO snapshot(log_name, aggregate_id, seq_nr, state, schema_hash) VALUES(?,?,?,?,?)",
|
|
82
|
+
)
|
|
83
|
+
let selectSnapshotStmt =
|
|
84
|
+
db->SqliteDriver.prepare(
|
|
85
|
+
"SELECT seq_nr, state, schema_hash FROM snapshot WHERE log_name=? AND aggregate_id=?",
|
|
86
|
+
)
|
|
82
87
|
|
|
83
88
|
let expectedNextSeq = (id: string): int =>
|
|
84
89
|
switch nextSeqStmt->SqliteDriver.get([JSON.Encode.string(name), JSON.Encode.string(id)]) {
|
|
@@ -118,6 +123,7 @@ let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
|
118
123
|
| None => 0
|
|
119
124
|
}
|
|
120
125
|
})
|
|
126
|
+
|
|
121
127
|
// Register the committed batch as appended-but-not-yet-published for the
|
|
122
128
|
// projection checkpoint low-watermark; Platform's afterPublish hook
|
|
123
129
|
// resolves the msgIds once the publish cycle completes. A transaction's
|
|
@@ -160,11 +166,7 @@ let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
|
160
166
|
|
|
161
167
|
let replayArray = (id: string, ~fromSeq=0): array<JSON.t> =>
|
|
162
168
|
selectByIdStmt
|
|
163
|
-
->SqliteDriver.all([
|
|
164
|
-
JSON.Encode.string(name),
|
|
165
|
-
JSON.Encode.string(id),
|
|
166
|
-
JSON.Encode.int(fromSeq),
|
|
167
|
-
])
|
|
169
|
+
->SqliteDriver.all([JSON.Encode.string(name), JSON.Encode.string(id), JSON.Encode.int(fromSeq)])
|
|
168
170
|
->Array.map(decodePayload)
|
|
169
171
|
|
|
170
172
|
let replay: EventLog.replay<string, JSON.t> = async id => replayArray(id)
|
|
@@ -172,7 +174,7 @@ let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
|
172
174
|
// node:sqlite's iterate() could back a lazy stream, but the current Stream
|
|
173
175
|
// API only has array-based fromIterable. Small events, dev-only backend →
|
|
174
176
|
// materialise via all() and wrap. Swap to a lazy adapter if it matters.
|
|
175
|
-
let replayStream = (id, ~fromSeq=?) => replayArray(id, ~fromSeq
|
|
177
|
+
let replayStream = (id, ~fromSeq=?) => replayArray(id, ~fromSeq?)->Stream.fromIterable
|
|
176
178
|
|
|
177
179
|
let latestSnapshot: EventLog.latestSnapshot<string> = async id =>
|
|
178
180
|
try {
|
|
@@ -197,7 +199,10 @@ let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
|
197
199
|
} catch {
|
|
198
200
|
| exn =>
|
|
199
201
|
Error(
|
|
200
|
-
exn
|
|
202
|
+
exn
|
|
203
|
+
->JsExn.fromException
|
|
204
|
+
->Option.flatMap(JsExn.message)
|
|
205
|
+
->Option.getOr("snapshot read error"),
|
|
201
206
|
)
|
|
202
207
|
}
|
|
203
208
|
|
|
@@ -230,7 +235,9 @@ let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
|
230
235
|
stream
|
|
231
236
|
->Stream.runCollect
|
|
232
237
|
->Effect.flatMap(jsons =>
|
|
233
|
-
Effect.promise(() =>
|
|
238
|
+
Effect.promise(() =>
|
|
239
|
+
appendTracked(~track=false, startingSeqNr, id, jsons)
|
|
240
|
+
)->Effect.flatMap(result =>
|
|
234
241
|
switch result {
|
|
235
242
|
| Ok() => Effect.succeed()
|
|
236
243
|
// appendStream's error channel is a string; map the typed append error.
|
|
@@ -257,7 +264,12 @@ let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
|
257
264
|
)
|
|
258
265
|
}
|
|
259
266
|
|
|
260
|
-
module Make = (
|
|
267
|
+
module Make = (
|
|
268
|
+
Bus: LocalBus.T,
|
|
269
|
+
DbProvider: {
|
|
270
|
+
let db: SqliteDriver.t
|
|
271
|
+
},
|
|
272
|
+
) => {
|
|
261
273
|
let make: EventLog_Adapter.storageMaker = (~name, ~owner as _, ~opts) => {
|
|
262
274
|
let (storageName, replay, storage) = makeStorage(~db=DbProvider.db, ~name, ~opts)
|
|
263
275
|
Bus.registerEventLogReplay(storageName, replay)
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
// The resource name doubles as the bus topic key that EventCollectorChannel subscribes to.
|
|
3
3
|
|
|
4
4
|
module Make = (Bus: LocalBus.T) => {
|
|
5
|
-
let make: ReventlessCore.EventTopic_Adapter.publisherMaker = (
|
|
5
|
+
let make: ReventlessCore.EventTopic_Adapter.publisherMaker = (
|
|
6
|
+
~name,
|
|
7
|
+
~storageResources as _,
|
|
8
|
+
~owner as _,
|
|
9
|
+
~opts as _,
|
|
10
|
+
) => {
|
|
6
11
|
let publishJson = (service, meta, json) => Bus.publishEvent(name, service, meta, json)
|
|
7
12
|
let publishJsonStream: ReventlessInfra.EventTopic.publishJsonStream = stream =>
|
|
8
13
|
stream
|
|
@@ -10,8 +15,9 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
10
15
|
->Stream.runForEach(items =>
|
|
11
16
|
Effect.promise(() =>
|
|
12
17
|
items
|
|
13
|
-
->Array.map(
|
|
14
|
-
|
|
18
|
+
->Array.map(
|
|
19
|
+
({ReventlessInfra.EventTopic.service: service, meta, json}) =>
|
|
20
|
+
publishJson(service, meta, json),
|
|
15
21
|
)
|
|
16
22
|
->Promise.all
|
|
17
23
|
->Promise.thenResolve(_ => ())
|
|
@@ -32,20 +32,15 @@ let make: ReventlessCore.Heartbeat_Adapter.runnerMaker<runtimeParts> = (
|
|
|
32
32
|
) => {
|
|
33
33
|
let handlerDeferred = runtime.parts.handlerDeferred
|
|
34
34
|
let intervalMs = timeout * 60 * 1000
|
|
35
|
-
let handle = setIntervalJs(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
->Effect.runPromise
|
|
45
|
-
->ignore
|
|
46
|
-
},
|
|
47
|
-
intervalMs,
|
|
48
|
-
)
|
|
35
|
+
let handle = setIntervalJs(() => {
|
|
36
|
+
// Await the handler Deferred on each tick. Since the Deferred is completed
|
|
37
|
+
// during make() setup, this resolves immediately after the first tick.
|
|
38
|
+
handlerDeferred
|
|
39
|
+
->Deferred.await_
|
|
40
|
+
->Effect.flatMap(handler => Effect.promise(() => handler(Obj.magic(), ())))
|
|
41
|
+
->Effect.runPromise
|
|
42
|
+
->ignore
|
|
43
|
+
}, intervalMs)
|
|
49
44
|
activeTimers.contents->Dict.set(name, handle)
|
|
50
45
|
{resources: []}
|
|
51
46
|
}
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -66,6 +66,7 @@ let emitEventTap = (~topic: string, ~service: string, ~payload: JSON.t) => {
|
|
|
66
66
|
// later sees is the event's ordinal rather than the ordinal of its own arrival.
|
|
67
67
|
eventTapSeq := eventTapSeq.contents + 1
|
|
68
68
|
let toStdout = LocalEventTap.stdoutEnabled()
|
|
69
|
+
|
|
69
70
|
// The line is built only for somebody: serialising every event for no reader is
|
|
70
71
|
// pure cost on the publish path.
|
|
71
72
|
if toStdout || LocalEventTap.hasReaders() {
|
|
@@ -296,8 +297,8 @@ module Impl = (C: BusConfig): T => {
|
|
|
296
297
|
// draining, and run `done_` via `ensuring` so the countdown always
|
|
297
298
|
// advances regardless of outcome.
|
|
298
299
|
Effect.tryPromise(~catch=e => e, () => handler(msg.service, msg.meta, msg.json))
|
|
299
|
-
->Effect.catchAll(
|
|
300
|
-
Effect.sync(() => Console.error2("[LocalBus] subscriber handler failed:", err))
|
|
300
|
+
->Effect.catchAll(
|
|
301
|
+
err => Effect.sync(() => Console.error2("[LocalBus] subscriber handler failed:", err)),
|
|
301
302
|
)
|
|
302
303
|
->Effect.ensuring(msg.done_)
|
|
303
304
|
)
|
|
@@ -404,11 +405,10 @@ module Impl = (C: BusConfig): T => {
|
|
|
404
405
|
let pending = queue.contents
|
|
405
406
|
queue.contents = []
|
|
406
407
|
pending->Array.forEach(json => {
|
|
407
|
-
let _ =
|
|
408
|
-
handler
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
})
|
|
408
|
+
let _ = handler(json, ())->Promise.catch(e => {
|
|
409
|
+
Console.error2("[LocalBus] parked command handler failed:", e)
|
|
410
|
+
Promise.resolve()
|
|
411
|
+
})
|
|
412
412
|
})
|
|
413
413
|
| None => ()
|
|
414
414
|
}
|
|
@@ -461,14 +461,14 @@ module Impl = (C: BusConfig): T => {
|
|
|
461
461
|
// Cross-plugin subscriptions are fire-and-forget: the handler is started asynchronously
|
|
462
462
|
// and done_ is called immediately. This prevents a deadlock where publishEvent on the
|
|
463
463
|
// EP topic would block until the downstream aggregate command chain completes.
|
|
464
|
-
let makeFireAndForgetHandler = handler =>
|
|
465
|
-
|
|
466
|
-
handler(json, ())->Promise.catch(e => {
|
|
464
|
+
let makeFireAndForgetHandler = handler =>
|
|
465
|
+
(_, _, json) => {
|
|
466
|
+
let _ = handler(json, ())->Promise.catch(e => {
|
|
467
467
|
Console.error2("[LocalBus] fire-and-forget handler failed:", e)
|
|
468
468
|
Promise.resolve()
|
|
469
469
|
})
|
|
470
|
-
|
|
471
|
-
|
|
470
|
+
Promise.resolve()
|
|
471
|
+
}
|
|
472
472
|
|
|
473
473
|
let registerEventCollectorHandler = (ecName, handler) => {
|
|
474
474
|
eventCollectorHandlers.contents->Dict.set(ecName, handler)
|
|
@@ -56,8 +56,5 @@ let register = (server: ReventlessGraphqlServer.GraphQL_ServerInstance.t): unit
|
|
|
56
56
|
}
|
|
57
57
|
})
|
|
58
58
|
server.registerTypes(~sdlTypes=ReventlessCore.Platform_AdminApi.geocodeTypes)
|
|
59
|
-
server.registerQueries(
|
|
60
|
-
~sdlFields=ReventlessCore.Platform_AdminApi.geocodeQueryFields,
|
|
61
|
-
~resolvers,
|
|
62
|
-
)
|
|
59
|
+
server.registerQueries(~sdlFields=ReventlessCore.Platform_AdminApi.geocodeQueryFields, ~resolvers)
|
|
63
60
|
}
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
Over the cap the state is dropped and the downgrade logged — a metadata-only
|
|
26
26
|
descriptor still tells the client to refetch, where a failed publish would
|
|
27
27
|
tell it nothing. */
|
|
28
|
-
let maxStateChars =
|
|
28
|
+
let maxStateChars =
|
|
29
|
+
60 * 1024
|
|
29
30
|
|
|
30
31
|
/** The `@retired` field of a registered read model, or `None`.
|
|
31
32
|
|
|
@@ -26,9 +26,21 @@ type diagnostics = {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
type t = {
|
|
29
|
-
registerTool: (
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
registerTool: (
|
|
30
|
+
~name: string,
|
|
31
|
+
~definition: ReventlessCore.MCP_SchemaGenerator.mcpToolDefinition,
|
|
32
|
+
~handler: toolHandler,
|
|
33
|
+
) => unit,
|
|
34
|
+
registerResource: (
|
|
35
|
+
~name: string,
|
|
36
|
+
~definition: ReventlessCore.MCP_SchemaGenerator.mcpResourceDefinition,
|
|
37
|
+
~handler: resourceHandler,
|
|
38
|
+
) => unit,
|
|
39
|
+
registerResourceTemplate: (
|
|
40
|
+
~name: string,
|
|
41
|
+
~definition: ReventlessCore.MCP_SchemaGenerator.mcpResourceDefinition,
|
|
42
|
+
~handler: resourceHandler,
|
|
43
|
+
) => unit,
|
|
32
44
|
registerToolsFromEntries: (
|
|
33
45
|
~pluginName: string,
|
|
34
46
|
~mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry>,
|
|
@@ -248,9 +260,7 @@ let make = (~label: string="MCP"): t => {
|
|
|
248
260
|
}
|
|
249
261
|
await handler(uri)
|
|
250
262
|
| None => {
|
|
251
|
-
McpSdk.contents: [
|
|
252
|
-
{McpSdk.uri, text: `{"error": "Resource not found: ${uri}"}`},
|
|
253
|
-
],
|
|
263
|
+
McpSdk.contents: [{McpSdk.uri, text: `{"error": "Resource not found: ${uri}"}`}],
|
|
254
264
|
}
|
|
255
265
|
}
|
|
256
266
|
}
|
|
@@ -261,43 +271,45 @@ let make = (~label: string="MCP"): t => {
|
|
|
261
271
|
|
|
262
272
|
let start = (~port: int=3001, ()) => {
|
|
263
273
|
let httpServer = McpSdk.createHttpServer((req, res) => {
|
|
264
|
-
let _ = (
|
|
265
|
-
|
|
266
|
-
|
|
274
|
+
let _ = (
|
|
275
|
+
async () => {
|
|
276
|
+
let reqMethod = req->McpSdk.method
|
|
277
|
+
let reqUrl = req->McpSdk.url
|
|
267
278
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
279
|
+
res->McpSdk.setHeader("Access-Control-Allow-Origin", "*")
|
|
280
|
+
res->McpSdk.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
|
|
281
|
+
res->McpSdk.setHeader("Access-Control-Allow-Headers", "Content-Type")
|
|
271
282
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
283
|
+
if reqUrl == "/mcp" || reqUrl->String.startsWith("/mcp?") {
|
|
284
|
+
switch reqMethod {
|
|
285
|
+
| "OPTIONS" =>
|
|
286
|
+
res->McpSdk.setStatusCode(204)
|
|
287
|
+
res->McpSdk.endResponseNoBody
|
|
288
|
+
| "POST" =>
|
|
289
|
+
let body = await McpSdk_Helpers.parseJsonBody(req)
|
|
290
|
+
let identity = extractIdentity(req)
|
|
291
|
+
let server = createServerInstance(identity)
|
|
292
|
+
let transport = McpSdk.newStreamableHTTPTransport({
|
|
293
|
+
enableJsonResponse: true,
|
|
294
|
+
})
|
|
295
|
+
let _ = await server->McpSdk.connect(transport)
|
|
296
|
+
let _ = await transport->McpSdk.handleRequest(req, res, body)
|
|
297
|
+
| "GET" =>
|
|
298
|
+
res->McpSdk.setHeader("Content-Type", "text/plain")
|
|
299
|
+
res->McpSdk.endResponse(`${label} server running`)
|
|
300
|
+
| "DELETE" =>
|
|
301
|
+
res->McpSdk.setStatusCode(200)
|
|
302
|
+
res->McpSdk.endResponseNoBody
|
|
303
|
+
| _ =>
|
|
304
|
+
res->McpSdk.setStatusCode(405)
|
|
305
|
+
res->McpSdk.endResponse("Method not allowed")
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
res->McpSdk.setStatusCode(404)
|
|
309
|
+
res->McpSdk.endResponse("Not found")
|
|
295
310
|
}
|
|
296
|
-
} else {
|
|
297
|
-
res->McpSdk.setStatusCode(404)
|
|
298
|
-
res->McpSdk.endResponse("Not found")
|
|
299
311
|
}
|
|
300
|
-
|
|
312
|
+
)()
|
|
301
313
|
})
|
|
302
314
|
|
|
303
315
|
httpServer->McpSdk.listen(port, () => {
|
|
@@ -83,11 +83,14 @@ let declaredStoreList = (): array<(string, string)> => declaredStores->Dict.toAr
|
|
|
83
83
|
// key becomes a path, and `uploads/../../etc/passwd` would otherwise escape the
|
|
84
84
|
// store.
|
|
85
85
|
let servedKey = (path: string): option<string> => {
|
|
86
|
-
let trimmed =
|
|
86
|
+
let trimmed =
|
|
87
|
+
path->String.startsWith("/") ? path->String.slice(~start=1, ~end=path->String.length) : path
|
|
87
88
|
let segments = trimmed->String.split("/")
|
|
88
89
|
let underAPrefix =
|
|
89
90
|
servedPrefixes.contents->Array.some(p =>
|
|
90
|
-
p->String.length > 0 &&
|
|
91
|
+
p->String.length > 0 &&
|
|
92
|
+
trimmed->String.startsWith(p ++ "/") &&
|
|
93
|
+
trimmed->String.length > p->String.length + 1
|
|
91
94
|
)
|
|
92
95
|
if underAPrefix && segments->Array.every(seg => seg != "" && seg != "." && seg != "..") {
|
|
93
96
|
Some(trimmed)
|
|
@@ -178,8 +178,7 @@ let topLevelPrefixes = (~root: string): array<string> =>
|
|
|
178
178
|
| _ => None
|
|
179
179
|
} {
|
|
180
180
|
| None => []
|
|
181
|
-
| Some(entries) =>
|
|
182
|
-
entries->Array.filter(NodeFs.isDirectory)->Array.map(NodeFs.direntName)
|
|
181
|
+
| Some(entries) => entries->Array.filter(NodeFs.isDirectory)->Array.map(NodeFs.direntName)
|
|
183
182
|
}
|
|
184
183
|
|
|
185
184
|
/** Deletes every object under `prefix`, returning how many were removed. Goes
|
|
@@ -27,16 +27,20 @@
|
|
|
27
27
|
// — without the wrapper, anonymous callers could trigger admin mutations
|
|
28
28
|
// (Plugin_Activate / _Deactivate, etc.) just by hitting the platform port.
|
|
29
29
|
|
|
30
|
-
let instance: ReventlessGraphqlServer.GraphQL_ServerInstance.t = ReventlessGraphqlServer.GraphQL_ServerInstance.make(
|
|
30
|
+
let instance: ReventlessGraphqlServer.GraphQL_ServerInstance.t = ReventlessGraphqlServer.GraphQL_ServerInstance.make(
|
|
31
|
+
~label="GraphQL:Platform",
|
|
32
|
+
)
|
|
31
33
|
|
|
32
34
|
// Wrap a resolver dict so every value enforces the Admin group. Keys are
|
|
33
35
|
// preserved; resolvers fall through to the underlying instance once the
|
|
34
36
|
// group check passes.
|
|
35
|
-
let wrapAdmin = (
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
let wrapAdmin = (resolvers: dict<ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn>): dict<
|
|
38
|
+
ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn,
|
|
39
|
+
> => {
|
|
38
40
|
let wrapped = Dict.make()
|
|
39
|
-
resolvers
|
|
41
|
+
resolvers
|
|
42
|
+
->Dict.toArray
|
|
43
|
+
->Array.forEach(((k, v)) =>
|
|
40
44
|
wrapped->Dict.set(k, Auth_GraphqlContext.requireGroup(~group="Admin", v))
|
|
41
45
|
)
|
|
42
46
|
wrapped
|