@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 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.93",
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.0"
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 apiSchemaFragmentOptionSchema = _jsNullable(apiSchemaFragmentSchema, ())
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 pluginStructureOptionSchema = _jsNullable(pluginStructureSchema, ())
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
- // Uses @s.matches(apiSchemaFragmentOptionSchema) — js_nullable creates T | null
513
- // (not T | undefined | null), which passes sury's jsonableValidation inside union variants.
514
- apiSchemaFragment: @s.matches(apiSchemaFragmentOptionSchema) option<apiSchemaFragment>,
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
- structure: @s.matches(pluginStructureOptionSchema) option<pluginStructure>,
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 apiSchemaFragmentOptionSchema = SuryResMjs.js_nullable(apiSchemaFragmentSchema);
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 pluginStructureOptionSchema = SuryResMjs.js_nullable(pluginStructureSchema);
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(apiSchemaFragmentOptionSchema),
215
+ apiSchemaFragment: s.m(apiSchemaFragmentOffloadSchema),
215
216
  apiTarget: s.m(stringOptionSchema),
216
- structure: s.m(pluginStructureOptionSchema),
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
- apiSchemaFragmentOptionSchema,
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
- pluginStructureOptionSchema,
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.
@@ -6,6 +6,7 @@ let Id = {
6
6
  dateTime: "dateTime",
7
7
  reference: "reference",
8
8
  storageRef: "storageRef",
9
+ offload: "offload",
9
10
  email: "email",
10
11
  phone: "phone",
11
12
  url: "url",