@reventlessdev/reventless-spec 3.0.0-alpha.95 → 3.0.0-alpha.97
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 +14 -0
- package/package.json +1 -1
- package/src/components/Plugin.res.mjs +2 -2
- package/src/semantic/Offload.res +50 -3
- package/src/semantic/Offload.res.mjs +38 -4
- package/src/semantic/Semantic.res +6 -3
- package/src/semantic/StorageRef.res +1 -1
- package/src/semantic/StorageRef.res.mjs +2 -1
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.97 (2026-08-02)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **ppx:** add [@offload](https://github.com/offload) field shorthand with per-field threshold ([3d5e3b5](https://github.com/ReventlessDev/reventless-core/commit/3d5e3b5da5010547ce0eaf7d94d660daec67feed))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.96 (2026-08-02)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **spec:** add Offload.toJson to serialize a payload as untagged wire JSON ([6a4c60f](https://github.com/ReventlessDev/reventless-core/commit/6a4c60f606dd6657082981acf75a90070099bf29))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.95 (2026-08-02)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @reventlessdev/reventless-spec
|
package/package.json
CHANGED
|
@@ -44,7 +44,7 @@ let apiTargetSchema = S.union([
|
|
|
44
44
|
S.literal("Platform")
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
|
-
let apiSchemaFragmentOffloadSchema = Offload$Reventless.optionSchema(undefined, "pluginApiFragments", apiSchemaFragmentSchema);
|
|
47
|
+
let apiSchemaFragmentOffloadSchema = Offload$Reventless.optionSchema(undefined, "pluginApiFragments", undefined, apiSchemaFragmentSchema);
|
|
48
48
|
|
|
49
49
|
let dcbEventLogOptionSchema = SuryResMjs.js_nullable(dcbEventLogDefinitionSchema);
|
|
50
50
|
|
|
@@ -202,7 +202,7 @@ let pluginStructureSchema = S.schema(s => ({
|
|
|
202
202
|
requiredStoreDeclarations: s.m(requiredStoreDeclarationArrayOptionSchema)
|
|
203
203
|
}));
|
|
204
204
|
|
|
205
|
-
let pluginStructureOffloadSchema = Offload$Reventless.optionSchema(undefined, "pluginStructures", pluginStructureSchema);
|
|
205
|
+
let pluginStructureOffloadSchema = Offload$Reventless.optionSchema(undefined, "pluginStructures", undefined, pluginStructureSchema);
|
|
206
206
|
|
|
207
207
|
let pluginDefinitionSchema = S.schema(s => ({
|
|
208
208
|
id: s.m(S.string),
|
package/src/semantic/Offload.res
CHANGED
|
@@ -127,8 +127,13 @@ plugin's own store; qualify as `"<plugin>.<store>"` to point at another's.
|
|
|
127
127
|
|
|
128
128
|
Prefer the `@offload("<store>")` ppx shorthand over calling this by hand.
|
|
129
129
|
*/
|
|
130
|
-
let forStore = (
|
|
131
|
-
|
|
130
|
+
let forStore = (
|
|
131
|
+
~plugin: option<string>=?,
|
|
132
|
+
~store: string,
|
|
133
|
+
~threshold: option<int>=?,
|
|
134
|
+
inner: S.t<'a>,
|
|
135
|
+
): S.t<payload<'a>> =>
|
|
136
|
+
schema(inner)->Semantic.mark(~id=Semantic.Id.offload, ~payload=StoredIn({plugin, store, threshold}))
|
|
132
137
|
|
|
133
138
|
/**
|
|
134
139
|
The codec wrapped for an **optional** field (`js_nullable`), plus the `StoredIn`
|
|
@@ -139,13 +144,22 @@ for older protocol versions, say). The marker sits on the outer schema, where
|
|
|
139
144
|
let optionSchema = (
|
|
140
145
|
~plugin: option<string>=?,
|
|
141
146
|
~store: string,
|
|
147
|
+
~threshold: option<int>=?,
|
|
142
148
|
inner: S.t<'a>,
|
|
143
149
|
): S.t<option<payload<'a>>> =>
|
|
144
150
|
_jsNullable(schema(inner), ())->Semantic.mark(
|
|
145
151
|
~id=Semantic.Id.offload,
|
|
146
|
-
~payload=StoredIn({plugin, store}),
|
|
152
|
+
~payload=StoredIn({plugin, store, threshold}),
|
|
147
153
|
)
|
|
148
154
|
|
|
155
|
+
/** Serialize a payload to its **untagged** wire JSON: `Inline` becomes the bare
|
|
156
|
+
inner value's JSON, `Offloaded` becomes `{"$offload": {...}}`. Used where a
|
|
157
|
+
payload must be stored as a plain JSON blob rather than the ReScript variant —
|
|
158
|
+
e.g. the plugin read model, whose DynamoDB write path marshals the raw value
|
|
159
|
+
and would otherwise persist the variant's runtime `{TAG, _0}` shape. */
|
|
160
|
+
let toJson = (inner: S.t<'a>, payload: payload<'a>): JSON.t =>
|
|
161
|
+
payload->S.reverseConvertToJsonOrThrow(schema(inner))
|
|
162
|
+
|
|
149
163
|
/** The inline value, if this payload is `Inline`. `None` for `Offloaded` — a
|
|
150
164
|
caller that must handle both arms uses `resolve` (async, fetches the ref); a
|
|
151
165
|
caller that only ever sees inline values (a test, or a path where offloading
|
|
@@ -245,3 +259,36 @@ let getStore = (schema: S.t<'a>): option<Semantic.storeTarget> =>
|
|
|
245
259
|
| Some({id, payload: StoredIn(target)}) if id == Semantic.Id.offload => Some(target)
|
|
246
260
|
| _ => None
|
|
247
261
|
}
|
|
262
|
+
|
|
263
|
+
/** The framework-default inline-vs-offloaded byte cut, used when neither the field
|
|
264
|
+
marker nor the platform declares one. Retuning any level is safe: both arms of
|
|
265
|
+
the codec read back to identical bytes, so the threshold only decides how
|
|
266
|
+
*future* values are split — no wire change, no re-encoding, existing events
|
|
267
|
+
stay valid. */
|
|
268
|
+
let defaultThreshold = 8192
|
|
269
|
+
|
|
270
|
+
/** The per-field threshold an `@offload` field declares, if any — the top of the
|
|
271
|
+
precedence chain (`@offload({..., threshold})`). `None` when the field left it
|
|
272
|
+
unset, which defers to the platform default and then {!defaultThreshold}. */
|
|
273
|
+
let getThreshold = (schema: S.t<'a>): option<int> =>
|
|
274
|
+
switch Semantic.get(schema) {
|
|
275
|
+
| Some({id, payload: StoredIn({threshold})}) if id == Semantic.Id.offload => threshold
|
|
276
|
+
| _ => None
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
The effective threshold for a field, resolving the precedence chain most-specific
|
|
281
|
+
first: the per-field `@offload({threshold})` (read from the field's schema), then
|
|
282
|
+
the platform config default (`~platformDefault`, e.g. `MakeWithConfig`'s
|
|
283
|
+
`offloadThreshold`), then {!defaultThreshold}.
|
|
284
|
+
|
|
285
|
+
A client drives an offloadable field by reading its field schema, calling this to
|
|
286
|
+
get the cut, and passing the result as {!prepare}'s `~threshold`. `prepare` stays
|
|
287
|
+
threshold-explicit (it holds the *value* schema, not the field schema that carries
|
|
288
|
+
the marker), so this is the seam that turns the declaration into a number.
|
|
289
|
+
*/
|
|
290
|
+
let effectiveThreshold = (schema: S.t<'a>, ~platformDefault: option<int>=?, ()): int =>
|
|
291
|
+
switch getThreshold(schema) {
|
|
292
|
+
| Some(t) => t
|
|
293
|
+
| None => platformDefault->Option.getOr(defaultThreshold)
|
|
294
|
+
}
|
|
@@ -61,26 +61,32 @@ function schema(inner) {
|
|
|
61
61
|
]);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
function forStore(plugin, store, inner) {
|
|
64
|
+
function forStore(plugin, store, threshold, inner) {
|
|
65
65
|
return Semantic$Reventless.mark(schema(inner), Semantic$Reventless.Id.offload, {
|
|
66
66
|
TAG: "StoredIn",
|
|
67
67
|
_0: {
|
|
68
68
|
plugin: plugin,
|
|
69
|
-
store: store
|
|
69
|
+
store: store,
|
|
70
|
+
threshold: threshold
|
|
70
71
|
}
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
function optionSchema(plugin, store, inner) {
|
|
75
|
+
function optionSchema(plugin, store, threshold, inner) {
|
|
75
76
|
return Semantic$Reventless.mark(SuryResMjs.js_nullable(schema(inner)), Semantic$Reventless.Id.offload, {
|
|
76
77
|
TAG: "StoredIn",
|
|
77
78
|
_0: {
|
|
78
79
|
plugin: plugin,
|
|
79
|
-
store: store
|
|
80
|
+
store: store,
|
|
81
|
+
threshold: threshold
|
|
80
82
|
}
|
|
81
83
|
});
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
function toJson(inner, payload) {
|
|
87
|
+
return S.reverseConvertToJsonOrThrow(payload, schema(inner));
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
function getInline(payload) {
|
|
85
91
|
if (payload.TAG === "Inline") {
|
|
86
92
|
return Primitive_option.some(payload._0);
|
|
@@ -142,16 +148,44 @@ function getStore(schema) {
|
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
|
|
151
|
+
function getThreshold(schema) {
|
|
152
|
+
let match = Semantic$Reventless.get(schema);
|
|
153
|
+
if (match === undefined) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let match$1 = match.payload;
|
|
157
|
+
if (typeof match$1 !== "object" || match$1.TAG === "ReferenceTo" || match.id !== Semantic$Reventless.Id.offload) {
|
|
158
|
+
return;
|
|
159
|
+
} else {
|
|
160
|
+
return match$1._0.threshold;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function effectiveThreshold(schema, platformDefault, param) {
|
|
165
|
+
let t = getThreshold(schema);
|
|
166
|
+
if (t !== undefined) {
|
|
167
|
+
return t;
|
|
168
|
+
} else {
|
|
169
|
+
return Stdlib_Option.getOr(platformDefault, 8192);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let defaultThreshold = 8192;
|
|
174
|
+
|
|
145
175
|
export {
|
|
146
176
|
offloadedRefSchema,
|
|
147
177
|
sentinelKey,
|
|
148
178
|
schema,
|
|
149
179
|
forStore,
|
|
150
180
|
optionSchema,
|
|
181
|
+
toJson,
|
|
151
182
|
getInline,
|
|
152
183
|
prepare,
|
|
153
184
|
resolve,
|
|
154
185
|
cachedFetch,
|
|
155
186
|
getStore,
|
|
187
|
+
defaultThreshold,
|
|
188
|
+
getThreshold,
|
|
189
|
+
effectiveThreshold,
|
|
156
190
|
}
|
|
157
191
|
/* offloadedRefSchema Not a pure module */
|
|
@@ -24,9 +24,12 @@ that can fail at runtime.
|
|
|
24
24
|
/** Which entity a reference field points to. */
|
|
25
25
|
type referenceTarget = {entity: string, plugin: option<string>}
|
|
26
26
|
|
|
27
|
-
/** Which object store a storage-ref field's value lives in. `plugin` is
|
|
28
|
-
when the store belongs to the declaring plugin, which is the common
|
|
29
|
-
|
|
27
|
+
/** Which object store a storage-ref / offload field's value lives in. `plugin` is
|
|
28
|
+
absent when the store belongs to the declaring plugin, which is the common
|
|
29
|
+
case. `threshold` is the per-field inline-vs-offloaded byte cut an `@offload`
|
|
30
|
+
field may declare (`None` for `@storageRef`, which is always a ref, and for
|
|
31
|
+
`@offload` fields that leave it to the platform default). */
|
|
32
|
+
type storeTarget = {plugin: option<string>, store: string, threshold: option<int>}
|
|
30
33
|
|
|
31
34
|
/** Per-semantic detail, for the semantics that carry any. */
|
|
32
35
|
type payload =
|
|
@@ -114,7 +114,7 @@ let forStore = (~plugin: option<string>=?, ~store: string): S.t<t> =>
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
)
|
|
117
|
-
->Semantic.mark(~id=Semantic.Id.storageRef, ~payload=StoredIn({plugin, store}))
|
|
117
|
+
->Semantic.mark(~id=Semantic.Id.storageRef, ~payload=StoredIn({plugin, store, threshold: None}))
|
|
118
118
|
|
|
119
119
|
/** The store a field's schema declares its refs live in, if any. */
|
|
120
120
|
let getStore = (schema: S.t<'a>): option<Semantic.storeTarget> =>
|