@reventlessdev/reventless-spec 3.0.0-alpha.93 → 3.0.0-alpha.95
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 +17 -0
- package/package.json +2 -2
- package/src/components/Plugin.res +10 -6
- package/src/components/Plugin.res.mjs +7 -6
- package/src/semantic/Offload.res +247 -0
- package/src/semantic/Offload.res.mjs +157 -0
- package/src/semantic/Semantic.res +5 -0
- package/src/semantic/Semantic.res.mjs +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
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.95 (2026-08-02)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-spec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.94 (2026-08-02)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **spec:** add [@offload](https://github.com/offload) inline-or-reference field primitive ([a54c39c](https://github.com/ReventlessDev/reventless-core/commit/a54c39ceb45b2c0b63caffd8ad039e732a5eb95d))
|
|
19
|
+
* **spec:** add Offload prepare/resolve/cachedFetch client helpers ([2855990](https://github.com/ReventlessDev/reventless-core/commit/2855990ba5cc56bd691460d3614ac666382c7fac))
|
|
20
|
+
* **spec:** make plugin structure/apiSchemaFragment offloadable payloads ([a0391bf](https://github.com/ReventlessDev/reventless-core/commit/a0391bf26f91035460cbf90f52d105a1003414c3))
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
# 3.0.0-alpha.93 (2026-08-02)
|
|
7
24
|
|
|
8
25
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-spec",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.95",
|
|
4
4
|
"description": "Specifications for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"sury": "11.0.0-alpha.4",
|
|
22
22
|
"sury-ppx": "11.0.0-alpha.2",
|
|
23
23
|
"yaml": "^2.8.3",
|
|
24
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.
|
|
24
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"rescript": "12.3.0",
|
|
@@ -106,7 +106,7 @@ type apiTarget = Domain | Platform
|
|
|
106
106
|
// inside union variant payloads. js_nullable creates T | null (no undefined) which is
|
|
107
107
|
// JSON-safe and passes jsonableValidation in all contexts.
|
|
108
108
|
@module("sury/src/Sury.res.mjs") external _jsNullable: (S.t<'a>, unit) => S.t<option<'a>> = "js_nullable"
|
|
109
|
-
let
|
|
109
|
+
let apiSchemaFragmentOffloadSchema = Offload.optionSchema(~store="pluginApiFragments", apiSchemaFragmentSchema)
|
|
110
110
|
let dcbEventLogOptionSchema = _jsNullable(dcbEventLogDefinitionSchema, ())
|
|
111
111
|
// js_nullable creates T | null which passes sury's jsonableValidation inside union variant payloads.
|
|
112
112
|
let stringOptionSchema = _jsNullable(S.string, ())
|
|
@@ -488,7 +488,7 @@ type pluginStructure = {
|
|
|
488
488
|
option<array<requiredStoreDeclaration>>,
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
let
|
|
491
|
+
let pluginStructureOffloadSchema = Offload.optionSchema(~store="pluginStructures", pluginStructureSchema)
|
|
492
492
|
|
|
493
493
|
/**
|
|
494
494
|
The self-description of a deployed plugin, persisted in the plugin's event store.
|
|
@@ -509,9 +509,11 @@ type pluginDefinition = {
|
|
|
509
509
|
// Use [] when the plugin does not need version negotiation.
|
|
510
510
|
extensionProtocols: array<extensionProtocol>,
|
|
511
511
|
// GraphQL schema fragment contributed by this plugin (optional, set at build time).
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
|
|
512
|
+
// Offloadable: a large SDL fragment is content-addressed to the pluginApiFragments
|
|
513
|
+
// store by the client and carried by reference; a small one stays Inline. optionSchema
|
|
514
|
+
// wraps the untagged codec in js_nullable (T | null, not T | undefined | null) so it
|
|
515
|
+
// passes jsonableValidation inside the lifecycle Message union, and marks the store.
|
|
516
|
+
apiSchemaFragment: @s.matches(apiSchemaFragmentOffloadSchema) option<Offload.payload<apiSchemaFragment>>,
|
|
515
517
|
// API target for schema routing in split-API mode.
|
|
516
518
|
// None/"Domain" → fragment goes to the DomainApi (default).
|
|
517
519
|
// Some("Platform") → fragment goes to the PlatformApi; excluded from DomainApi runtime schema.
|
|
@@ -519,7 +521,9 @@ type pluginDefinition = {
|
|
|
519
521
|
// which passes sury's jsonableValidation inside union variant payloads.
|
|
520
522
|
apiTarget: @s.matches(stringOptionSchema) option<string>,
|
|
521
523
|
// Component graph metadata — populated by makePluginDefinition; absent for older protocol versions.
|
|
522
|
-
|
|
524
|
+
// Offloadable: the large structure is content-addressed to the pluginStructures store by
|
|
525
|
+
// the client and carried by reference; a small one stays Inline (see apiSchemaFragment).
|
|
526
|
+
structure: @s.matches(pluginStructureOffloadSchema) option<Offload.payload<pluginStructure>>,
|
|
523
527
|
// DCB EventLog definition for plugins that bundle a DcbEventLog component.
|
|
524
528
|
// Carries the EventTopic ARN so the admin can provision cross-plugin SNS
|
|
525
529
|
// subscriptions from this plugin's DCB topic → peer EventCollectors.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Offload$Reventless from "../semantic/Offload.res.mjs";
|
|
4
5
|
import * as SuryResMjs from "sury/src/Sury.res.mjs";
|
|
5
6
|
|
|
6
7
|
let pluginKindSchema = S.union([
|
|
@@ -43,7 +44,7 @@ let apiTargetSchema = S.union([
|
|
|
43
44
|
S.literal("Platform")
|
|
44
45
|
]);
|
|
45
46
|
|
|
46
|
-
let
|
|
47
|
+
let apiSchemaFragmentOffloadSchema = Offload$Reventless.optionSchema(undefined, "pluginApiFragments", apiSchemaFragmentSchema);
|
|
47
48
|
|
|
48
49
|
let dcbEventLogOptionSchema = SuryResMjs.js_nullable(dcbEventLogDefinitionSchema);
|
|
49
50
|
|
|
@@ -201,7 +202,7 @@ let pluginStructureSchema = S.schema(s => ({
|
|
|
201
202
|
requiredStoreDeclarations: s.m(requiredStoreDeclarationArrayOptionSchema)
|
|
202
203
|
}));
|
|
203
204
|
|
|
204
|
-
let
|
|
205
|
+
let pluginStructureOffloadSchema = Offload$Reventless.optionSchema(undefined, "pluginStructures", pluginStructureSchema);
|
|
205
206
|
|
|
206
207
|
let pluginDefinitionSchema = S.schema(s => ({
|
|
207
208
|
id: s.m(S.string),
|
|
@@ -211,9 +212,9 @@ let pluginDefinitionSchema = S.schema(s => ({
|
|
|
211
212
|
extensions: s.m(S.array(extensionDefinitionSchema)),
|
|
212
213
|
eventCollector: s.m(S.string),
|
|
213
214
|
extensionProtocols: s.m(S.array(extensionProtocolSchema)),
|
|
214
|
-
apiSchemaFragment: s.m(
|
|
215
|
+
apiSchemaFragment: s.m(apiSchemaFragmentOffloadSchema),
|
|
215
216
|
apiTarget: s.m(stringOptionSchema),
|
|
216
|
-
structure: s.m(
|
|
217
|
+
structure: s.m(pluginStructureOffloadSchema),
|
|
217
218
|
dcbEventLog: s.m(dcbEventLogOptionSchema),
|
|
218
219
|
kind: s.m(pluginKindSchema)
|
|
219
220
|
}));
|
|
@@ -232,7 +233,7 @@ export {
|
|
|
232
233
|
extensionProtocolSchema,
|
|
233
234
|
apiSchemaFragmentSchema,
|
|
234
235
|
apiTargetSchema,
|
|
235
|
-
|
|
236
|
+
apiSchemaFragmentOffloadSchema,
|
|
236
237
|
dcbEventLogOptionSchema,
|
|
237
238
|
stringOptionSchema,
|
|
238
239
|
stringArrayOptionSchema,
|
|
@@ -257,7 +258,7 @@ export {
|
|
|
257
258
|
requiredStoreDeclarationSchema,
|
|
258
259
|
requiredStoreDeclarationArrayOptionSchema,
|
|
259
260
|
pluginStructureSchema,
|
|
260
|
-
|
|
261
|
+
pluginStructureOffloadSchema,
|
|
261
262
|
pluginDefinitionSchema,
|
|
262
263
|
}
|
|
263
264
|
/* pluginKindSchema Not a pure module */
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A field whose large value lives in a content-addressed object store, carried by
|
|
3
|
+
reference — or inline when it is small enough not to be worth a round trip.
|
|
4
|
+
|
|
5
|
+
## Sibling of `StorageRef`, not the same thing
|
|
6
|
+
|
|
7
|
+
`StorageRef` and `Offload` are two members of one family: both say "this field's
|
|
8
|
+
value lives in an object store", both declare that store through the shared
|
|
9
|
+
`Semantic.StoredIn` marker, and both are produced by a **client** that uploads
|
|
10
|
+
the bytes before the command is issued — never by the framework inside `decide`.
|
|
11
|
+
They differ in what the field carries:
|
|
12
|
+
|
|
13
|
+
- `@storageRef` is always a reference (an origin-relative path a store minted),
|
|
14
|
+
and the reference *is* the value the reader sees (a URL it renders).
|
|
15
|
+
- `@offload` is an **inline-or-reference** value. Below a size threshold the
|
|
16
|
+
value stays embedded; above it the client stores the bytes under a
|
|
17
|
+
content-addressed key and the field carries `Offloaded{store, key, hash, bytes}`.
|
|
18
|
+
A reader resolves either arm back to the value.
|
|
19
|
+
|
|
20
|
+
Content addressing (the key is the SHA-256 of the bytes) makes the store write
|
|
21
|
+
idempotent and deduplicating: the same value stored twice lands on the same key,
|
|
22
|
+
so identical payloads across versions or tenants hold one object, not many.
|
|
23
|
+
|
|
24
|
+
## The backward-compatible wire form
|
|
25
|
+
|
|
26
|
+
Every event already in history stored the value **inline and unwrapped** — a
|
|
27
|
+
plain record, with no variant tag. So the codec here must decode those bytes
|
|
28
|
+
unchanged, which rules out sury's default tagged-union encoding (`{TAG, _0}`).
|
|
29
|
+
|
|
30
|
+
Instead the codec is *untagged* and sniffs a reserved sentinel key:
|
|
31
|
+
|
|
32
|
+
- an `Offloaded` value encodes as `{"$offload": {store, key, hash, bytes}}`;
|
|
33
|
+
- an `Inline` value encodes as the raw value, exactly as before.
|
|
34
|
+
|
|
35
|
+
On decode, a JSON object carrying the `$offload` key is an `Offloaded`; anything
|
|
36
|
+
else is decoded as the inner value into `Inline`. A record field name can never
|
|
37
|
+
be `$offload` (identifiers cannot start with `$`), so a legacy inline payload can
|
|
38
|
+
never be mistaken for a reference, and vice versa — no migration, and a
|
|
39
|
+
pre-change fixture decodes as `Inline` untouched.
|
|
40
|
+
|
|
41
|
+
@example
|
|
42
|
+
```rescript
|
|
43
|
+
// an event/command field, optional and offloadable to the "pluginStructures" store
|
|
44
|
+
structure: @s.matches(Offload.optionSchema(~store="pluginStructures", pluginStructureSchema))
|
|
45
|
+
option<Offload.payload<pluginStructure>>
|
|
46
|
+
```
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/** The reference an offloaded value carries: which store holds it, the
|
|
50
|
+
content-addressed key, the content hash (== the key's basis), and the byte
|
|
51
|
+
length. `hash` is redundant with `key` today (`key` is `sha256/<hash>`) but
|
|
52
|
+
named so a reader can verify integrity without parsing the key. */
|
|
53
|
+
@schema
|
|
54
|
+
type offloadedRef = {
|
|
55
|
+
store: string,
|
|
56
|
+
key: string,
|
|
57
|
+
hash: string,
|
|
58
|
+
bytes: int,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A field's value: embedded, or a reference to bytes the client stored. */
|
|
62
|
+
type payload<'a> =
|
|
63
|
+
| Inline('a)
|
|
64
|
+
| Offloaded(offloadedRef)
|
|
65
|
+
|
|
66
|
+
/** `nullableAsOption` emits `T | undefined | null`, which fails
|
|
67
|
+
`jsonableValidation` inside an event union; `js_nullable` emits `T | null`,
|
|
68
|
+
which is JSON-safe there. Same reason `Plugin.res` reaches for it. */
|
|
69
|
+
@module("sury/src/Sury.res.mjs")
|
|
70
|
+
external _jsNullable: (S.t<'a>, unit) => S.t<option<'a>> = "js_nullable"
|
|
71
|
+
|
|
72
|
+
/** The codec builds on `S.json`, which sury 11 gates behind an explicit enable.
|
|
73
|
+
Doing it here (at module load, before any `schema` call) makes the primitive
|
|
74
|
+
self-contained: importing `Offload` is enough, no consumer has to remember. */
|
|
75
|
+
S.enableJson()
|
|
76
|
+
|
|
77
|
+
/** The object key under which an `Offloaded` value hides. Reserved: no ReScript
|
|
78
|
+
record field encodes to a key starting with `$`, so it cannot collide with an
|
|
79
|
+
inline payload's own fields. */
|
|
80
|
+
let sentinelKey = "$offload"
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
The untagged inline-or-reference codec for a field of inner type `'a`.
|
|
84
|
+
|
|
85
|
+
Parameterised by the inner value's schema because the `Inline` arm round-trips
|
|
86
|
+
through it. The `Offloaded` arm round-trips through `offloadedRefSchema` under the
|
|
87
|
+
sentinel key. See the module doc for why this is untagged.
|
|
88
|
+
*/
|
|
89
|
+
let schema = (inner: S.t<'a>): S.t<payload<'a>> => {
|
|
90
|
+
// Offloaded arm: recognised by the reserved sentinel key, strict, and tried
|
|
91
|
+
// first so an offloaded value is never mistaken for an inline one. The ref is
|
|
92
|
+
// a fixed new shape that needs no healing, so a direct json-transform is fine.
|
|
93
|
+
let offloadedArm = S.json->S.transform(s => {
|
|
94
|
+
parser: json =>
|
|
95
|
+
switch json->JSON.Decode.object->Option.flatMap(dict => dict->Dict.get(sentinelKey)) {
|
|
96
|
+
| Some(refJson) => Offloaded(refJson->S.parseJsonOrThrow(offloadedRefSchema))
|
|
97
|
+
| None => s.fail("not an offloaded reference")
|
|
98
|
+
},
|
|
99
|
+
serializer: payload =>
|
|
100
|
+
switch payload {
|
|
101
|
+
| Offloaded(ref) =>
|
|
102
|
+
Dict.fromArray([(sentinelKey, ref->S.reverseConvertToJsonOrThrow(offloadedRefSchema))])
|
|
103
|
+
->JSON.Encode.object
|
|
104
|
+
| Inline(_) => s.fail("not an offloaded reference")
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
// Inline arm: the inner schema applied through sury's own pipeline, so it
|
|
108
|
+
// inherits whatever tolerance the surrounding decode uses — the lifecycle
|
|
109
|
+
// Message decoder heals older payloads with missing fields. This is why the
|
|
110
|
+
// codec is a union rather than one json-transform with a nested
|
|
111
|
+
// parseJsonOrThrow: a nested parse runs strict and breaks the frozen corpus.
|
|
112
|
+
let inlineArm = inner->S.transform(s => {
|
|
113
|
+
parser: value => Inline(value),
|
|
114
|
+
serializer: payload =>
|
|
115
|
+
switch payload {
|
|
116
|
+
| Inline(value) => value
|
|
117
|
+
| Offloaded(_) => s.fail("not an inline value")
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
S.union([offloadedArm, inlineArm])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
The codec plus the `StoredIn` marker declaring which store the field's references
|
|
125
|
+
live in, for a **non-optional** field. `plugin` is absent for the declaring
|
|
126
|
+
plugin's own store; qualify as `"<plugin>.<store>"` to point at another's.
|
|
127
|
+
|
|
128
|
+
Prefer the `@offload("<store>")` ppx shorthand over calling this by hand.
|
|
129
|
+
*/
|
|
130
|
+
let forStore = (~plugin: option<string>=?, ~store: string, inner: S.t<'a>): S.t<payload<'a>> =>
|
|
131
|
+
schema(inner)->Semantic.mark(~id=Semantic.Id.offload, ~payload=StoredIn({plugin, store}))
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
The codec wrapped for an **optional** field (`js_nullable`), plus the `StoredIn`
|
|
135
|
+
marker. This is the common case: offloadable fields are usually optional (absent
|
|
136
|
+
for older protocol versions, say). The marker sits on the outer schema, where
|
|
137
|
+
`Semantic.get` reads it first.
|
|
138
|
+
*/
|
|
139
|
+
let optionSchema = (
|
|
140
|
+
~plugin: option<string>=?,
|
|
141
|
+
~store: string,
|
|
142
|
+
inner: S.t<'a>,
|
|
143
|
+
): S.t<option<payload<'a>>> =>
|
|
144
|
+
_jsNullable(schema(inner), ())->Semantic.mark(
|
|
145
|
+
~id=Semantic.Id.offload,
|
|
146
|
+
~payload=StoredIn({plugin, store}),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
/** The inline value, if this payload is `Inline`. `None` for `Offloaded` — a
|
|
150
|
+
caller that must handle both arms uses `resolve` (async, fetches the ref); a
|
|
151
|
+
caller that only ever sees inline values (a test, or a path where offloading
|
|
152
|
+
is not yet wired) uses this. */
|
|
153
|
+
let getInline = (payload: payload<'a>): option<'a> =>
|
|
154
|
+
switch payload {
|
|
155
|
+
| Inline(value) => Some(value)
|
|
156
|
+
| Offloaded(_) => None
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── Client helpers ─────────────────────────────────────────────────────────
|
|
160
|
+
//
|
|
161
|
+
// The producer/reader surface that makes offloading easy for a client to drive.
|
|
162
|
+
// Both take their store I/O as injected transports so this module stays pure
|
|
163
|
+
// and provider-agnostic: a browser passes a presigned PUT / fetch, the Node seed
|
|
164
|
+
// passes a direct SDK call, a test passes an in-memory map. The plugin's own
|
|
165
|
+
// deploy-time producer is the exception — it offloads Pulumi-natively (declares a
|
|
166
|
+
// content-addressed object resource) rather than through `prepare`, because its
|
|
167
|
+
// value is a `Pulumi.Output` and a resource cannot be created inside `.apply`.
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
Decide inline-or-offloaded for one value and, when it is large, upload it.
|
|
171
|
+
|
|
172
|
+
Serializes `value` with `schema`; if the JSON is below `threshold` it stays
|
|
173
|
+
`Inline` (no round trip). Otherwise it is hashed, uploaded under the
|
|
174
|
+
content-addressed key `sha256/<hash>`, and returned as `Offloaded`. Because the
|
|
175
|
+
key is the content hash, re-uploading identical bytes writes the same object —
|
|
176
|
+
idempotent, and the source of cross-version/cross-client dedupe.
|
|
177
|
+
|
|
178
|
+
`~hash` and `~upload` are injected so this stays provider-agnostic; `~hash` must
|
|
179
|
+
be a stable content hash (the same bytes must always hash the same). Size is the
|
|
180
|
+
JSON string's length in characters — a close proxy for byte length on the
|
|
181
|
+
mostly-ASCII JSON these payloads are, and only ever used for the threshold cut.
|
|
182
|
+
*/
|
|
183
|
+
let prepare = (
|
|
184
|
+
value: 'a,
|
|
185
|
+
~schema: S.t<'a>,
|
|
186
|
+
~store: string,
|
|
187
|
+
~threshold: int,
|
|
188
|
+
~hash: string => string,
|
|
189
|
+
~upload: (~key: string, ~bytes: string) => promise<unit>,
|
|
190
|
+
): promise<payload<'a>> => {
|
|
191
|
+
let bytes = value->S.reverseConvertToJsonStringOrThrow(schema)
|
|
192
|
+
if bytes->String.length < threshold {
|
|
193
|
+
Promise.resolve(Inline(value))
|
|
194
|
+
} else {
|
|
195
|
+
let digest = hash(bytes)
|
|
196
|
+
let key = "sha256/" ++ digest
|
|
197
|
+
upload(~key, ~bytes)->Promise.then(() =>
|
|
198
|
+
Promise.resolve(Offloaded({store, key, hash: digest, bytes: bytes->String.length}))
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
Resolve a payload back to its value: `Inline` directly, `Offloaded` by fetching
|
|
205
|
+
the object's bytes (via the injected `~fetch`) and decoding with `schema`.
|
|
206
|
+
|
|
207
|
+
Offloaded objects are written by current code, so their bytes decode strictly —
|
|
208
|
+
unlike the inline arm of the wire codec, which heals older event payloads.
|
|
209
|
+
*/
|
|
210
|
+
let resolve = (payload: payload<'a>, ~schema: S.t<'a>, ~fetch: string => promise<string>): promise<
|
|
211
|
+
'a,
|
|
212
|
+
> =>
|
|
213
|
+
switch payload {
|
|
214
|
+
| Inline(value) => Promise.resolve(value)
|
|
215
|
+
| Offloaded({key}) =>
|
|
216
|
+
fetch(key)->Promise.then(bytes => Promise.resolve(bytes->S.parseJsonStringOrThrow(schema)))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
Wrap a `~fetch` so each content-addressed key is fetched at most once per process.
|
|
221
|
+
|
|
222
|
+
Keys are immutable (they are content hashes), so a fetched object can be cached
|
|
223
|
+
forever — every replay/projection that references the same hash reuses it, and
|
|
224
|
+
concurrent resolves of the same key share the one in-flight promise.
|
|
225
|
+
*/
|
|
226
|
+
let cachedFetch = (fetch: string => promise<string>): (string => promise<string>) => {
|
|
227
|
+
let cache: dict<promise<string>> = Dict.make()
|
|
228
|
+
key =>
|
|
229
|
+
switch cache->Dict.get(key) {
|
|
230
|
+
| Some(inflight) => inflight
|
|
231
|
+
| None =>
|
|
232
|
+
let inflight = fetch(key)
|
|
233
|
+
cache->Dict.set(key, inflight)
|
|
234
|
+
inflight
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The store an `@offload` field declares, if any — the read side of the marker,
|
|
239
|
+
used by provisioning to know the field requires this store to exist. Distinct
|
|
240
|
+
from `StorageRef.getStore` by the semantic id, so the two families stay
|
|
241
|
+
separable (offload objects are content-addressed and durable up front; they
|
|
242
|
+
must not be swept by the pending-upload claimer). */
|
|
243
|
+
let getStore = (schema: S.t<'a>): option<Semantic.storeTarget> =>
|
|
244
|
+
switch Semantic.get(schema) {
|
|
245
|
+
| Some({id, payload: StoredIn(target)}) if id == Semantic.Id.offload => Some(target)
|
|
246
|
+
| _ => None
|
|
247
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
7
|
+
import * as Semantic$Reventless from "./Semantic.res.mjs";
|
|
8
|
+
import * as SuryResMjs from "sury/src/Sury.res.mjs";
|
|
9
|
+
|
|
10
|
+
let offloadedRefSchema = S.schema(s => ({
|
|
11
|
+
store: s.m(S.string),
|
|
12
|
+
key: s.m(S.string),
|
|
13
|
+
hash: s.m(S.string),
|
|
14
|
+
bytes: s.m(S.int)
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
S.enableJson();
|
|
18
|
+
|
|
19
|
+
let sentinelKey = "$offload";
|
|
20
|
+
|
|
21
|
+
function schema(inner) {
|
|
22
|
+
let offloadedArm = S.transform(S.json, s => ({
|
|
23
|
+
p: json => {
|
|
24
|
+
let refJson = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), dict => dict[sentinelKey]);
|
|
25
|
+
if (refJson !== undefined) {
|
|
26
|
+
return {
|
|
27
|
+
TAG: "Offloaded",
|
|
28
|
+
_0: S.parseJsonOrThrow(refJson, offloadedRefSchema)
|
|
29
|
+
};
|
|
30
|
+
} else {
|
|
31
|
+
return s.fail("not an offloaded reference", undefined);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
s: payload => {
|
|
35
|
+
if (payload.TAG === "Inline") {
|
|
36
|
+
return s.fail("not an offloaded reference", undefined);
|
|
37
|
+
} else {
|
|
38
|
+
return Object.fromEntries([[
|
|
39
|
+
sentinelKey,
|
|
40
|
+
S.reverseConvertToJsonOrThrow(payload._0, offloadedRefSchema)
|
|
41
|
+
]]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
let inlineArm = S.transform(inner, s => ({
|
|
46
|
+
p: value => ({
|
|
47
|
+
TAG: "Inline",
|
|
48
|
+
_0: value
|
|
49
|
+
}),
|
|
50
|
+
s: payload => {
|
|
51
|
+
if (payload.TAG === "Inline") {
|
|
52
|
+
return payload._0;
|
|
53
|
+
} else {
|
|
54
|
+
return s.fail("not an inline value", undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
return S.union([
|
|
59
|
+
offloadedArm,
|
|
60
|
+
inlineArm
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function forStore(plugin, store, inner) {
|
|
65
|
+
return Semantic$Reventless.mark(schema(inner), Semantic$Reventless.Id.offload, {
|
|
66
|
+
TAG: "StoredIn",
|
|
67
|
+
_0: {
|
|
68
|
+
plugin: plugin,
|
|
69
|
+
store: store
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function optionSchema(plugin, store, inner) {
|
|
75
|
+
return Semantic$Reventless.mark(SuryResMjs.js_nullable(schema(inner)), Semantic$Reventless.Id.offload, {
|
|
76
|
+
TAG: "StoredIn",
|
|
77
|
+
_0: {
|
|
78
|
+
plugin: plugin,
|
|
79
|
+
store: store
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getInline(payload) {
|
|
85
|
+
if (payload.TAG === "Inline") {
|
|
86
|
+
return Primitive_option.some(payload._0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function prepare(value, schema, store, threshold, hash, upload) {
|
|
91
|
+
let bytes = S.reverseConvertToJsonStringOrThrow(value, schema, undefined);
|
|
92
|
+
if (bytes.length < threshold) {
|
|
93
|
+
return Promise.resolve({
|
|
94
|
+
TAG: "Inline",
|
|
95
|
+
_0: value
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
let digest = hash(bytes);
|
|
99
|
+
let key = "sha256/" + digest;
|
|
100
|
+
return upload(key, bytes).then(() => Promise.resolve({
|
|
101
|
+
TAG: "Offloaded",
|
|
102
|
+
_0: {
|
|
103
|
+
store: store,
|
|
104
|
+
key: key,
|
|
105
|
+
hash: digest,
|
|
106
|
+
bytes: bytes.length
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolve(payload, schema, fetch) {
|
|
112
|
+
if (payload.TAG === "Inline") {
|
|
113
|
+
return Promise.resolve(payload._0);
|
|
114
|
+
} else {
|
|
115
|
+
return fetch(payload._0.key).then(bytes => Promise.resolve(S.parseJsonStringOrThrow(bytes, schema)));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function cachedFetch(fetch) {
|
|
120
|
+
let cache = {};
|
|
121
|
+
return key => {
|
|
122
|
+
let inflight = cache[key];
|
|
123
|
+
if (inflight !== undefined) {
|
|
124
|
+
return inflight;
|
|
125
|
+
}
|
|
126
|
+
let inflight$1 = fetch(key);
|
|
127
|
+
cache[key] = inflight$1;
|
|
128
|
+
return inflight$1;
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getStore(schema) {
|
|
133
|
+
let match = Semantic$Reventless.get(schema);
|
|
134
|
+
if (match === undefined) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
let target = match.payload;
|
|
138
|
+
if (typeof target !== "object" || target.TAG === "ReferenceTo" || match.id !== Semantic$Reventless.Id.offload) {
|
|
139
|
+
return;
|
|
140
|
+
} else {
|
|
141
|
+
return target._0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export {
|
|
146
|
+
offloadedRefSchema,
|
|
147
|
+
sentinelKey,
|
|
148
|
+
schema,
|
|
149
|
+
forStore,
|
|
150
|
+
optionSchema,
|
|
151
|
+
getInline,
|
|
152
|
+
prepare,
|
|
153
|
+
resolve,
|
|
154
|
+
cachedFetch,
|
|
155
|
+
getStore,
|
|
156
|
+
}
|
|
157
|
+
/* offloadedRefSchema Not a pure module */
|
|
@@ -48,6 +48,11 @@ module Id = {
|
|
|
48
48
|
let dateTime = "dateTime"
|
|
49
49
|
let reference = "reference"
|
|
50
50
|
let storageRef = "storageRef"
|
|
51
|
+
// A field whose large value the client stored in a content-addressed object
|
|
52
|
+
// store and carries by reference (inline below a size threshold). Sibling of
|
|
53
|
+
// `storageRef`: same `StoredIn` store declaration, but an inline-or-reference
|
|
54
|
+
// value rather than an always-a-ref path string.
|
|
55
|
+
let offload = "offload"
|
|
51
56
|
|
|
52
57
|
// The branded scalars. Each refines a `string` or a number without changing
|
|
53
58
|
// its shape, so a field gains one of these without anything stored changing.
|