@reventlessdev/reventless-core 3.0.0-alpha.177 → 3.0.0-alpha.179

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 CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.179 (2026-07-23)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** round-trip the DynamoDB Scan cursor in full-list connections ([0e21008](https://github.com/ReventlessDev/reventless-core/commit/0e2100860767a153a5c2f50a88fd6b501787c7f4))
11
+
12
+
13
+ # 3.0.0-alpha.178 (2026-07-22)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **core:** derive the disconnect schedule's rule name in one place ([6324a5b](https://github.com/ReventlessDev/reventless-core/commit/6324a5bc678a375688ec98571423f164b09375ae))
18
+
19
+
6
20
  # 3.0.0-alpha.177 (2026-07-22)
7
21
 
8
22
  * refactor!: retire the stale "Admin" vocabulary for platform-owned resources ([2770bd9](https://github.com/ReventlessDev/reventless-core/commit/2770bd96f309142bdfc65b9f36bd1a3ef60641bd))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.177",
3
+ "version": "3.0.0-alpha.179",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,16 +29,16 @@
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.30",
32
- "@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
32
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
33
33
  "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
34
+ "@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
34
35
  "@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
35
- "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
36
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
37
36
  "@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
38
- "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
39
- "@reventlessdev/reventless-spec": "3.0.0-alpha.80",
40
37
  "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
41
- "@reventlessdev/reventless-infra": "3.0.0-alpha.104"
38
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
39
+ "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
40
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.104",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.80"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
@@ -251,7 +251,7 @@ let validateScanSortAlignment = (
251
251
  None
252
252
  } else {
253
253
  Some(
254
- `Read model "${readModelName}": @scanSort field "${field}" is not the sort key of any table or GSI. Sort requests for this field will be evaluated as a JS-runtime per-page sort over a full Scan — expensive in production. Add it as the sort key of an index, or accept the per-page-sort caveat.`,
254
+ `Read model "${readModelName}": @scanSort field "${field}" is not the sort key of any table or GSI. Sort requests Scan the whole table and sort per-page, so results past the first page are NOT globally ordered — and the Scan is expensive in production. Promote the field to an index sort key for correct, cheap ordering, or accept per-page ordering.`,
255
255
  )
256
256
  }
257
257
  )
@@ -221,7 +221,7 @@ function validateScanSortAlignment(schema, readModelName, knownSortFields) {
221
221
  if (knownSortFields.includes(field)) {
222
222
  return;
223
223
  } else {
224
- return `Read model "` + readModelName + `": @scanSort field "` + field + `" is not the sort key of any table or GSI. Sort requests for this field will be evaluated as a JS-runtime per-page sort over a full Scan — expensive in production. Add it as the sort key of an index, or accept the per-page-sort caveat.`;
224
+ return `Read model "` + readModelName + `": @scanSort field "` + field + `" is not the sort key of any table or GSI. Sort requests Scan the whole table and sort per-page, so results past the first page are NOT globally ordered — and the Scan is expensive in production. Promote the field to an index sort key for correct, cheap ordering, or accept per-page ordering.`;
225
225
  }
226
226
  });
227
227
  } else {
@@ -29,6 +29,13 @@ module type Spec = {
29
29
  }
30
30
 
31
31
  module Make = (Spec: Spec) => {
32
+ // Single owner of the disconnect schedule's rule name. Create and delete must
33
+ // derive it identically — they previously did not (create prefixed the stack,
34
+ // delete did not), so every delete targeted a name that was never created and
35
+ // the rule survived. The stack prefix itself is load-bearing: several stacks
36
+ // share one EventBridge bus, and an unprefixed plugin id would collide.
37
+ let disconnectScheduleName = id => Spec.environment ++ ("-" ++ id)
38
+
32
39
  let forwardCommand = async (
33
40
  _id,
34
41
  command,
@@ -104,7 +111,7 @@ module Make = (Spec: Spec) => {
104
111
  switch directive {
105
112
  | PluginExtensionPointSpec.CreateDisconnectSchedule(id, timeout) =>
106
113
  await createSchedule({
107
- name: Spec.environment ++ ("-" ++ id),
114
+ name: id->disconnectScheduleName,
108
115
  rate: timeout->ScheduleOps.minutesFromNow,
109
116
  payload: {
110
117
  Message.id,
@@ -114,7 +121,7 @@ module Make = (Spec: Spec) => {
114
121
  ->Message.encodeCommand'(S.string, PluginExtensionPointSpec.commandSchema)
115
122
  ->JSON.stringify,
116
123
  })
117
- | DeleteDisconnectSchedule(id) => await deleteSchedule(id)
124
+ | DeleteDisconnectSchedule(id) => await deleteSchedule(id->disconnectScheduleName)
118
125
  | ForwardCommand({id, command, extensionPointName}) =>
119
126
  await forwardCommand(id, command, extensionPointName, queryEngine)
120
127
  | DoConnectPlugin(pluginDef) =>
@@ -23,6 +23,7 @@ function disconnectGrace(interval) {
23
23
  }
24
24
 
25
25
  function Make(Spec) {
26
+ let disconnectScheduleName = id => Spec.environment + ("-" + id);
26
27
  let forwardCommand = async (_id, command, extensionPointName, queryEngine) => {
27
28
  let jsons = await queryEngine.scan(PluginSpec$ReventlessCore.name, [
28
29
  [
@@ -81,7 +82,7 @@ function Make(Spec) {
81
82
  case "CreateDisconnectSchedule" :
82
83
  let id = directive._0;
83
84
  return await createSchedule({
84
- name: Spec.environment + ("-" + id),
85
+ name: disconnectScheduleName(id),
85
86
  rate: ScheduleOps$ReventlessCore.minutesFromNow(directive._1),
86
87
  payload: JSON.stringify(Message$ReventlessCore.encodeCommand$p({
87
88
  id: id,
@@ -90,7 +91,7 @@ function Make(Spec) {
90
91
  }, S.string, PluginExtensionPointSpec$ReventlessInfra.commandSchema))
91
92
  });
92
93
  case "DeleteDisconnectSchedule" :
93
- return await deleteSchedule(directive._0);
94
+ return await deleteSchedule(disconnectScheduleName(directive._0));
94
95
  case "DoConnectPlugin" :
95
96
  let fn = Spec.manageSubscriptions;
96
97
  if (fn !== undefined) {
@@ -355,6 +356,7 @@ function Make(Spec) {
355
356
  mapOutgoingEvent: mapOutgoingEvent
356
357
  });
357
358
  return {
359
+ disconnectScheduleName: disconnectScheduleName,
358
360
  forwardCommand: forwardCommand,
359
361
  directiveHandler: directiveHandler,
360
362
  PluginMapping: PluginMapping,
@@ -0,0 +1,66 @@
1
+ // Guards the disconnect schedule's rule name. Create and delete must derive it
2
+ // identically: they previously did not — create prefixed `Spec.environment`,
3
+ // delete did not — so every delete targeted a rule that had never been created
4
+ // and the real rule survived. The leak is invisible at runtime, because the
5
+ // delete reports success against a name AWS simply doesn't have.
6
+
7
+ open JestGlobals
8
+
9
+ let created: ref<array<string>> = ref([])
10
+ let deleted: ref<array<string>> = ref([])
11
+
12
+ module DisconnectSpec = {
13
+ let runtimeOps: PluginRuntimeOperations.operations = {
14
+ messagePublish: {
15
+ sendMessageToChannel: async (~channelId as _, ~messageBody as _) => (),
16
+ },
17
+ }
18
+ let environment = "teststack"
19
+ let updateApiSchema = None
20
+ let manageSubscriptions = None
21
+ }
22
+
23
+ module EP = PluginExtensionPoint_Plugin.Make(DisconnectSpec)
24
+
25
+ let createSchedule: Reventless.Schedule.create = async schedule => {
26
+ created := created.contents->Array.concat([schedule.name])
27
+ }
28
+
29
+ let deleteSchedule: Reventless.Schedule.delete = async name => {
30
+ deleted := deleted.contents->Array.concat([name])
31
+ }
32
+
33
+ let queryEngine: Reventless.QueryEngine.operations = {
34
+ scan: async (~readModelName as _, ~filterConfigs as _, ~limit as _) => [],
35
+ query: async (
36
+ ~readModelName as _,
37
+ ~key as _=?,
38
+ ~id as _,
39
+ ~subIdConfig as _=?,
40
+ ~filterConfigs as _=?,
41
+ ~ascending as _=?,
42
+ ~limit as _=?,
43
+ ) => [],
44
+ }
45
+
46
+ let pluginId = "Catalog_1.0.0-alpha.169"
47
+
48
+ let handle = directive => EP.directiveHandler(createSchedule, deleteSchedule, queryEngine, directive)
49
+
50
+ describe("plugin disconnect schedule", () => {
51
+ test("create and delete address the same rule name", async () => {
52
+ created := []
53
+ deleted := []
54
+ await handle(ReventlessInfra.PluginExtensionPointSpec.CreateDisconnectSchedule(pluginId, 5))
55
+ await handle(DeleteDisconnectSchedule(pluginId))
56
+ expect(deleted.contents)->toEqual(created.contents)
57
+ })
58
+
59
+ test("the rule name is namespaced by stack", async () => {
60
+ created := []
61
+ await handle(ReventlessInfra.PluginExtensionPointSpec.CreateDisconnectSchedule(pluginId, 5))
62
+ // Several stacks share one EventBridge bus, so a bare plugin id would
63
+ // collide across them.
64
+ expect(created.contents)->toEqual(["teststack-" ++ pluginId])
65
+ })
66
+ })