@reventlessdev/reventless-local 3.0.0-alpha.200 → 3.0.0-alpha.202
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 +10 -9
- package/rescript.json +2 -1
- package/src/Platform.res +70 -37
- package/src/Platform.res.mjs +48 -34
- package/src/adapter/BackendState.res +13 -0
- package/src/adapter/BackendState.res.mjs +17 -1
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +1 -1
- package/src/adapter/DomainGraphQL_Server.res +5 -5
- package/src/adapter/DomainGraphQL_Server.res.mjs +1 -1
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +1 -1
- package/src/adapter/LocalBus.res +6 -46
- package/src/adapter/LocalBus.res.mjs +0 -29
- package/src/adapter/LocalStateChangeDescriptor.res +93 -0
- package/src/adapter/LocalStateChangeDescriptor.res.mjs +61 -0
- package/src/adapter/LocalUploadResolvers.res +16 -3
- package/src/adapter/LocalUploadResolvers.res.mjs +6 -4
- package/src/adapter/ObjectStore/LocalObjectStore.res +157 -0
- package/src/adapter/ObjectStore/LocalObjectStore.res.mjs +129 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res +215 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_FileSystem.res.mjs +286 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res +33 -0
- package/src/adapter/ObjectStore/ObjectStoreStorage_InMemory.res.mjs +47 -0
- package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +1 -1
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +4 -2
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +3 -3
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +4 -2
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +3 -3
- package/src/reset/LocalSeedReset.res +552 -0
- package/src/reset/LocalSeedReset.res.mjs +533 -0
- package/tests/adapter/BackendParityTest.res +51 -0
- package/tests/adapter/BackendParityTest.res.mjs +44 -0
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res +6 -4
- package/tests/adapter/GraphQL_SubscriptionResolversTest.res.mjs +4 -3
- package/tests/adapter/ObjectStorePersistenceTest.res +243 -0
- package/tests/adapter/ObjectStorePersistenceTest.res.mjs +159 -0
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res +110 -0
- package/tests/components/eventlog/EventLogProvisioningSeamTest.res.mjs +141 -0
- package/tests/reset/LocalSeedResetTest.res +252 -0
- package/tests/reset/LocalSeedResetTest.res.mjs +280 -0
- package/src/adapter/LocalObjectStore.res +0 -70
- package/src/adapter/LocalObjectStore.res.mjs +0 -60
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// On-disk arm of the local object store: bytes under the same directory the
|
|
2
|
+
// SQLite database lives in, so uploads and offloaded payloads survive a restart
|
|
3
|
+
// exactly like the events that reference them.
|
|
4
|
+
//
|
|
5
|
+
// The two were previously out of step: with `REVENTLESS_LOCAL_BACKEND=sqlite` an
|
|
6
|
+
// event carrying an `Offloaded{store, key, …}` reference outlived the bytes it
|
|
7
|
+
// pointed at, so a restart replayed events whose payloads had evaporated.
|
|
8
|
+
//
|
|
9
|
+
// Layout, rooted at the SQLite file's directory (`./.reventless` by convention):
|
|
10
|
+
//
|
|
11
|
+
// objects/<key> served-object bytes; the key's slashes become real
|
|
12
|
+
// directories, so the tree mirrors the URL space the
|
|
13
|
+
// dev server serves at /{prefix}/*
|
|
14
|
+
// object-meta/<key>.json {"contentType": …} for that object
|
|
15
|
+
// offload/<key> content-addressed offload payload (JSON text)
|
|
16
|
+
//
|
|
17
|
+
// Content type needs a sidecar because a file holds bytes and nothing else.
|
|
18
|
+
// Keeping it in a PARALLEL tree rather than beside the object leaves `objects/`
|
|
19
|
+
// an exact mirror of what is served — no `.json` companions to skip when
|
|
20
|
+
// browsing, copying, or serving it directly.
|
|
21
|
+
//
|
|
22
|
+
// Every call is synchronous, matching the in-memory arm's signature: this is a
|
|
23
|
+
// dev-only store on a local disk, and going async here would push promises
|
|
24
|
+
// through the HTTP handlers and the offload hook for no benefit.
|
|
25
|
+
|
|
26
|
+
let objectsDir = (~root) => NodePath.join([root, "objects"])
|
|
27
|
+
let metaDir = (~root) => NodePath.join([root, "object-meta"])
|
|
28
|
+
let offloadDir = (~root) => NodePath.join([root, "offload"])
|
|
29
|
+
|
|
30
|
+
// A key becomes a path here, so it must not climb out of the root. Keys arrive
|
|
31
|
+
// from URL paths as well as from content addressing, and `..` segments that were
|
|
32
|
+
// inert as dict keys are not inert as path segments.
|
|
33
|
+
let isSafeKey = (key: string): bool =>
|
|
34
|
+
key != "" &&
|
|
35
|
+
!(key->String.startsWith("/")) &&
|
|
36
|
+
key->String.split("/")->Array.every(seg => seg != "" && seg != "." && seg != "..")
|
|
37
|
+
|
|
38
|
+
let writeUnder = (path: string, write: string => unit): unit => {
|
|
39
|
+
NodeFs.mkdirSync(NodePath.dirname(path), {recursive: true})
|
|
40
|
+
write(path)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Drop directories the deleted object left behind, walking up until one still
|
|
44
|
+
// holds something or `stopAt` is reached. S3 has no directories, so the empty
|
|
45
|
+
// `<uuid>/` a released upload leaves is an artifact of the filesystem mapping,
|
|
46
|
+
// not something the AWS path would have.
|
|
47
|
+
let rec pruneEmptyDirs = (~stopAt: string, ~from: string): unit =>
|
|
48
|
+
if from != stopAt && from->String.length > stopAt->String.length {
|
|
49
|
+
let entries = try NodeFs.readdirSync(from, {withFileTypes: true}) catch {
|
|
50
|
+
| _ => []
|
|
51
|
+
}
|
|
52
|
+
if entries->Array.length == 0 {
|
|
53
|
+
try NodeFs.rmSync(from, {recursive: true, force: true}) catch {
|
|
54
|
+
| _ => ()
|
|
55
|
+
}
|
|
56
|
+
pruneEmptyDirs(~stopAt, ~from=NodePath.dirname(from))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Served objects ──────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
let objectPath = (~root, ~key) => NodePath.join([objectsDir(~root), key])
|
|
63
|
+
let metaPath = (~root, ~key) => NodePath.join([metaDir(~root), key ++ ".json"])
|
|
64
|
+
|
|
65
|
+
let put = (~root: string, ~key: string, ~bytes: NodeBuffer.t, ~contentType: string): unit =>
|
|
66
|
+
if isSafeKey(key) {
|
|
67
|
+
writeUnder(objectPath(~root, ~key), path => NodeFs.writeFileSyncBuffer(path, bytes))
|
|
68
|
+
writeUnder(metaPath(~root, ~key), path =>
|
|
69
|
+
NodeFs.writeFileSync(
|
|
70
|
+
path,
|
|
71
|
+
Dict.fromArray([("contentType", JSON.Encode.string(contentType))])
|
|
72
|
+
->JSON.Encode.object
|
|
73
|
+
->JSON.stringify,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let defaultContentType = "application/octet-stream"
|
|
79
|
+
|
|
80
|
+
let readContentType = (~root, ~key): string =>
|
|
81
|
+
switch try Some(NodeFs.readFileSync(metaPath(~root, ~key))) catch {
|
|
82
|
+
| _ => None
|
|
83
|
+
} {
|
|
84
|
+
| Some(raw) =>
|
|
85
|
+
raw
|
|
86
|
+
->JSON.parseOrThrow
|
|
87
|
+
->JSON.Decode.object
|
|
88
|
+
->Option.flatMap(d => d->Dict.get("contentType"))
|
|
89
|
+
->Option.flatMap(JSON.Decode.string)
|
|
90
|
+
->Option.getOr(defaultContentType)
|
|
91
|
+
| None => defaultContentType
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The try covers the case where the key names a directory rather than a file
|
|
95
|
+
// (`/uploads/<uuid>` when only `/uploads/<uuid>/logo.svg` was stored): reading it
|
|
96
|
+
// throws EISDIR, and the answer the caller wants is the same 404 a missing key
|
|
97
|
+
// gets.
|
|
98
|
+
let get = (~root: string, ~key: string): option<(NodeBuffer.t, string)> =>
|
|
99
|
+
if !isSafeKey(key) {
|
|
100
|
+
None
|
|
101
|
+
} else {
|
|
102
|
+
switch try Some(NodeFs.readFileSyncBuffer(objectPath(~root, ~key))) catch {
|
|
103
|
+
| _ => None
|
|
104
|
+
} {
|
|
105
|
+
| Some(bytes) => Some((bytes, readContentType(~root, ~key)))
|
|
106
|
+
| None => None
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let removeFile = (path: string): unit =>
|
|
111
|
+
try NodeFs.unlinkSync(path) catch {
|
|
112
|
+
| _ => ()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let delete = (~root: string, ~key: string): unit =>
|
|
116
|
+
if isSafeKey(key) {
|
|
117
|
+
let object = objectPath(~root, ~key)
|
|
118
|
+
let meta = metaPath(~root, ~key)
|
|
119
|
+
removeFile(object)
|
|
120
|
+
removeFile(meta)
|
|
121
|
+
pruneEmptyDirs(~stopAt=objectsDir(~root), ~from=NodePath.dirname(object))
|
|
122
|
+
pruneEmptyDirs(~stopAt=metaDir(~root), ~from=NodePath.dirname(meta))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Offload objects ─────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
let offloadPath = (~root, ~key) => NodePath.join([offloadDir(~root), key])
|
|
128
|
+
|
|
129
|
+
let putOffload = (~root: string, ~key: string, ~bytes: string): unit =>
|
|
130
|
+
if isSafeKey(key) {
|
|
131
|
+
writeUnder(offloadPath(~root, ~key), path => NodeFs.writeFileSync(path, bytes))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let getOffload = (~root: string, ~key: string): option<string> =>
|
|
135
|
+
if !isSafeKey(key) {
|
|
136
|
+
None
|
|
137
|
+
} else {
|
|
138
|
+
try Some(NodeFs.readFileSync(offloadPath(~root, ~key))) catch {
|
|
139
|
+
| _ => None
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Prefix-scoped inspection and removal ────────────────────────────────────
|
|
144
|
+
//
|
|
145
|
+
// What a scoped reset works in: an object belongs to the store whose key prefix
|
|
146
|
+
// it sits under, so a wipe names a prefix and never a whole tree — the same
|
|
147
|
+
// prefix-scoped delete the deployed reset issues against S3.
|
|
148
|
+
|
|
149
|
+
let rec walkKeys = (~dir: string, ~prefix: string, ~into: array<string>): unit =>
|
|
150
|
+
switch try Some(NodeFs.readdirSync(dir, {withFileTypes: true})) catch {
|
|
151
|
+
| _ => None
|
|
152
|
+
} {
|
|
153
|
+
| None => ()
|
|
154
|
+
| Some(entries) =>
|
|
155
|
+
entries->Array.forEach(entry => {
|
|
156
|
+
let name = NodeFs.direntName(entry)
|
|
157
|
+
let key = prefix == "" ? name : `${prefix}/${name}`
|
|
158
|
+
if NodeFs.isDirectory(entry) {
|
|
159
|
+
walkKeys(~dir=NodePath.join([dir, name]), ~prefix=key, ~into)
|
|
160
|
+
} else {
|
|
161
|
+
into->Array.push(key)
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Every stored object key under `prefix` (the prefix itself included when it
|
|
167
|
+
names an object). An absent prefix is simply empty, not an error. */
|
|
168
|
+
let keysUnder = (~root: string, ~prefix: string): array<string> => {
|
|
169
|
+
let into = []
|
|
170
|
+
walkKeys(~dir=NodePath.join([objectsDir(~root), prefix]), ~prefix, ~into)
|
|
171
|
+
into
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Top-level key prefixes present in the store, one segment deep — what a plan
|
|
175
|
+
lists when it has to report objects no declared store claims. */
|
|
176
|
+
let topLevelPrefixes = (~root: string): array<string> =>
|
|
177
|
+
switch try Some(NodeFs.readdirSync(objectsDir(~root), {withFileTypes: true})) catch {
|
|
178
|
+
| _ => None
|
|
179
|
+
} {
|
|
180
|
+
| None => []
|
|
181
|
+
| Some(entries) =>
|
|
182
|
+
entries->Array.filter(NodeFs.isDirectory)->Array.map(NodeFs.direntName)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Deletes every object under `prefix`, returning how many were removed. Goes
|
|
186
|
+
through `delete` so the content-type sidecar and the emptied directories go
|
|
187
|
+
with each object. */
|
|
188
|
+
let deleteUnder = (~root: string, ~prefix: string): int => {
|
|
189
|
+
let keys = keysUnder(~root, ~prefix)
|
|
190
|
+
keys->Array.forEach(key => delete(~root, ~key))
|
|
191
|
+
keys->Array.length
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
let offloadKeys = (~root: string): array<string> => {
|
|
195
|
+
let into = []
|
|
196
|
+
walkKeys(~dir=offloadDir(~root), ~prefix="", ~into)
|
|
197
|
+
into
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let deleteOffloadAll = (~root: string): int => {
|
|
201
|
+
let keys = offloadKeys(~root)
|
|
202
|
+
try NodeFs.rmSync(offloadDir(~root), {recursive: true, force: true}) catch {
|
|
203
|
+
| _ => ()
|
|
204
|
+
}
|
|
205
|
+
keys->Array.length
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Removes the three trees, leaving the rest of the root (the database file, the
|
|
209
|
+
// dev `users.yaml`) untouched.
|
|
210
|
+
let reset = (~root: string): unit =>
|
|
211
|
+
[objectsDir(~root), metaDir(~root), offloadDir(~root)]->Array.forEach(dir =>
|
|
212
|
+
try NodeFs.rmSync(dir, {recursive: true, force: true}) catch {
|
|
213
|
+
| _ => ()
|
|
214
|
+
}
|
|
215
|
+
)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
|
|
8
|
+
function objectsDir(root) {
|
|
9
|
+
return Nodepath.join(root, "objects");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function metaDir(root) {
|
|
13
|
+
return Nodepath.join(root, "object-meta");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function offloadDir(root) {
|
|
17
|
+
return Nodepath.join(root, "offload");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isSafeKey(key) {
|
|
21
|
+
if (key !== "" && !key.startsWith("/")) {
|
|
22
|
+
return key.split("/").every(seg => {
|
|
23
|
+
if (seg !== "" && seg !== ".") {
|
|
24
|
+
return seg !== "..";
|
|
25
|
+
} else {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
} else {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeUnder(path, write) {
|
|
35
|
+
Nodefs.mkdirSync(Nodepath.dirname(path), {
|
|
36
|
+
recursive: true
|
|
37
|
+
});
|
|
38
|
+
write(path);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pruneEmptyDirs(stopAt, _from) {
|
|
42
|
+
while (true) {
|
|
43
|
+
let from = _from;
|
|
44
|
+
if (!(from !== stopAt && from.length > stopAt.length)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = Nodefs.readdirSync(from, {
|
|
50
|
+
withFileTypes: true
|
|
51
|
+
});
|
|
52
|
+
} catch (exn) {
|
|
53
|
+
entries = [];
|
|
54
|
+
}
|
|
55
|
+
if (entries.length !== 0) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
Nodefs.rmSync(from, {
|
|
60
|
+
recursive: true,
|
|
61
|
+
force: true
|
|
62
|
+
});
|
|
63
|
+
} catch (exn$1) {
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
_from = Nodepath.dirname(from);
|
|
67
|
+
continue;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function objectPath(root, key) {
|
|
72
|
+
return Nodepath.join(Nodepath.join(root, "objects"), key);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function metaPath(root, key) {
|
|
76
|
+
return Nodepath.join(Nodepath.join(root, "object-meta"), key + ".json");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function put(root, key, bytes, contentType) {
|
|
80
|
+
if (isSafeKey(key)) {
|
|
81
|
+
writeUnder(objectPath(root, key), path => {
|
|
82
|
+
Nodefs.writeFileSync(path, bytes);
|
|
83
|
+
});
|
|
84
|
+
return writeUnder(metaPath(root, key), path => {
|
|
85
|
+
Nodefs.writeFileSync(path, JSON.stringify(Object.fromEntries([[
|
|
86
|
+
"contentType",
|
|
87
|
+
contentType
|
|
88
|
+
]])), "utf8");
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let defaultContentType = "application/octet-stream";
|
|
94
|
+
|
|
95
|
+
function readContentType(root, key) {
|
|
96
|
+
let raw;
|
|
97
|
+
try {
|
|
98
|
+
raw = Nodefs.readFileSync(metaPath(root, key), "utf8");
|
|
99
|
+
} catch (exn) {
|
|
100
|
+
raw = undefined;
|
|
101
|
+
}
|
|
102
|
+
if (raw !== undefined) {
|
|
103
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(JSON.parse(raw)), d => d["contentType"]), Stdlib_JSON.Decode.string), defaultContentType);
|
|
104
|
+
} else {
|
|
105
|
+
return defaultContentType;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function get(root, key) {
|
|
110
|
+
if (!isSafeKey(key)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
let bytes;
|
|
114
|
+
try {
|
|
115
|
+
bytes = Nodefs.readFileSync(objectPath(root, key));
|
|
116
|
+
} catch (exn) {
|
|
117
|
+
bytes = undefined;
|
|
118
|
+
}
|
|
119
|
+
if (bytes !== undefined) {
|
|
120
|
+
return [
|
|
121
|
+
bytes,
|
|
122
|
+
readContentType(root, key)
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function removeFile(path) {
|
|
128
|
+
try {
|
|
129
|
+
Nodefs.unlinkSync(path);
|
|
130
|
+
return;
|
|
131
|
+
} catch (exn) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function $$delete(root, key) {
|
|
137
|
+
if (!isSafeKey(key)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let object = objectPath(root, key);
|
|
141
|
+
let meta = metaPath(root, key);
|
|
142
|
+
removeFile(object);
|
|
143
|
+
removeFile(meta);
|
|
144
|
+
pruneEmptyDirs(Nodepath.join(root, "objects"), Nodepath.dirname(object));
|
|
145
|
+
pruneEmptyDirs(Nodepath.join(root, "object-meta"), Nodepath.dirname(meta));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function offloadPath(root, key) {
|
|
149
|
+
return Nodepath.join(Nodepath.join(root, "offload"), key);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function putOffload(root, key, bytes) {
|
|
153
|
+
if (isSafeKey(key)) {
|
|
154
|
+
return writeUnder(offloadPath(root, key), path => {
|
|
155
|
+
Nodefs.writeFileSync(path, bytes, "utf8");
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getOffload(root, key) {
|
|
161
|
+
if (!isSafeKey(key)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
return Nodefs.readFileSync(offloadPath(root, key), "utf8");
|
|
166
|
+
} catch (exn) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function walkKeys(dir, prefix, into) {
|
|
172
|
+
let entries;
|
|
173
|
+
try {
|
|
174
|
+
entries = Nodefs.readdirSync(dir, {
|
|
175
|
+
withFileTypes: true
|
|
176
|
+
});
|
|
177
|
+
} catch (exn) {
|
|
178
|
+
entries = undefined;
|
|
179
|
+
}
|
|
180
|
+
if (entries !== undefined) {
|
|
181
|
+
entries.forEach(entry => {
|
|
182
|
+
let name = entry.name;
|
|
183
|
+
let key = prefix === "" ? name : prefix + `/` + name;
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
return walkKeys(Nodepath.join(dir, name), key, into);
|
|
186
|
+
} else {
|
|
187
|
+
into.push(key);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function keysUnder(root, prefix) {
|
|
196
|
+
let into = [];
|
|
197
|
+
walkKeys(Nodepath.join(Nodepath.join(root, "objects"), prefix), prefix, into);
|
|
198
|
+
return into;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function topLevelPrefixes(root) {
|
|
202
|
+
let entries;
|
|
203
|
+
try {
|
|
204
|
+
entries = Nodefs.readdirSync(Nodepath.join(root, "objects"), {
|
|
205
|
+
withFileTypes: true
|
|
206
|
+
});
|
|
207
|
+
} catch (exn) {
|
|
208
|
+
entries = undefined;
|
|
209
|
+
}
|
|
210
|
+
if (entries !== undefined) {
|
|
211
|
+
return entries.filter(prim => prim.isDirectory()).map(prim => prim.name);
|
|
212
|
+
} else {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function deleteUnder(root, prefix) {
|
|
218
|
+
let keys = keysUnder(root, prefix);
|
|
219
|
+
keys.forEach(key => $$delete(root, key));
|
|
220
|
+
return keys.length;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function offloadKeys(root) {
|
|
224
|
+
let into = [];
|
|
225
|
+
walkKeys(Nodepath.join(root, "offload"), "", into);
|
|
226
|
+
return into;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function deleteOffloadAll(root) {
|
|
230
|
+
let keys = offloadKeys(root);
|
|
231
|
+
try {
|
|
232
|
+
Nodefs.rmSync(Nodepath.join(root, "offload"), {
|
|
233
|
+
recursive: true,
|
|
234
|
+
force: true
|
|
235
|
+
});
|
|
236
|
+
} catch (exn) {
|
|
237
|
+
|
|
238
|
+
}
|
|
239
|
+
return keys.length;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function reset(root) {
|
|
243
|
+
[
|
|
244
|
+
Nodepath.join(root, "objects"),
|
|
245
|
+
Nodepath.join(root, "object-meta"),
|
|
246
|
+
Nodepath.join(root, "offload")
|
|
247
|
+
].forEach(dir => {
|
|
248
|
+
try {
|
|
249
|
+
Nodefs.rmSync(dir, {
|
|
250
|
+
recursive: true,
|
|
251
|
+
force: true
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
} catch (exn) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export {
|
|
261
|
+
objectsDir,
|
|
262
|
+
metaDir,
|
|
263
|
+
offloadDir,
|
|
264
|
+
isSafeKey,
|
|
265
|
+
writeUnder,
|
|
266
|
+
pruneEmptyDirs,
|
|
267
|
+
objectPath,
|
|
268
|
+
metaPath,
|
|
269
|
+
put,
|
|
270
|
+
defaultContentType,
|
|
271
|
+
readContentType,
|
|
272
|
+
get,
|
|
273
|
+
removeFile,
|
|
274
|
+
$$delete,
|
|
275
|
+
offloadPath,
|
|
276
|
+
putOffload,
|
|
277
|
+
getOffload,
|
|
278
|
+
walkKeys,
|
|
279
|
+
keysUnder,
|
|
280
|
+
topLevelPrefixes,
|
|
281
|
+
deleteUnder,
|
|
282
|
+
offloadKeys,
|
|
283
|
+
deleteOffloadAll,
|
|
284
|
+
reset,
|
|
285
|
+
}
|
|
286
|
+
/* node:fs Not a pure module */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// In-process arm of the local object store: two dicts, wiped on restart.
|
|
2
|
+
//
|
|
3
|
+
// Selected when nothing durable is on local disk (Memory backend, a `:memory:`
|
|
4
|
+
// SQLite, Postgres) — see BackendState.getObjectStoreRoot. Ephemeral by design:
|
|
5
|
+
// with the events themselves gone on restart, the bytes they reference have
|
|
6
|
+
// nothing to outlive.
|
|
7
|
+
//
|
|
8
|
+
// `get` returns a `(bytes, contentType)` pair rather than a record so that this
|
|
9
|
+
// arm and ObjectStoreStorage_FileSystem share one signature without either
|
|
10
|
+
// owning a type the other must import; LocalObjectStore names the pair.
|
|
11
|
+
|
|
12
|
+
type stored = (NodeBuffer.t, string)
|
|
13
|
+
|
|
14
|
+
let objects: dict<stored> = Dict.make()
|
|
15
|
+
|
|
16
|
+
let put = (~key: string, ~bytes: NodeBuffer.t, ~contentType: string): unit =>
|
|
17
|
+
objects->Dict.set(key, (bytes, contentType))
|
|
18
|
+
|
|
19
|
+
let get = (~key: string): option<stored> => objects->Dict.get(key)
|
|
20
|
+
|
|
21
|
+
let delete = (~key: string): unit => objects->Dict.delete(key)
|
|
22
|
+
|
|
23
|
+
// Offload payloads: a separate keyspace, for the reason LocalObjectStore gives.
|
|
24
|
+
let offloadObjects: dict<string> = Dict.make()
|
|
25
|
+
|
|
26
|
+
let putOffload = (~key: string, ~bytes: string): unit => offloadObjects->Dict.set(key, bytes)
|
|
27
|
+
|
|
28
|
+
let getOffload = (~key: string): option<string> => offloadObjects->Dict.get(key)
|
|
29
|
+
|
|
30
|
+
let reset = (): unit => {
|
|
31
|
+
objects->Dict.keysToArray->Array.forEach(k => objects->Dict.delete(k))
|
|
32
|
+
offloadObjects->Dict.keysToArray->Array.forEach(k => offloadObjects->Dict.delete(k))
|
|
33
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
|
+
|
|
5
|
+
let objects = {};
|
|
6
|
+
|
|
7
|
+
function put(key, bytes, contentType) {
|
|
8
|
+
objects[key] = [
|
|
9
|
+
bytes,
|
|
10
|
+
contentType
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function get(key) {
|
|
15
|
+
return objects[key];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function $$delete(key) {
|
|
19
|
+
Stdlib_Dict.$$delete(objects, key);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let offloadObjects = {};
|
|
23
|
+
|
|
24
|
+
function putOffload(key, bytes) {
|
|
25
|
+
offloadObjects[key] = bytes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getOffload(key) {
|
|
29
|
+
return offloadObjects[key];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function reset() {
|
|
33
|
+
Object.keys(objects).forEach(k => Stdlib_Dict.$$delete(objects, k));
|
|
34
|
+
Object.keys(offloadObjects).forEach(k => Stdlib_Dict.$$delete(offloadObjects, k));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
objects,
|
|
39
|
+
put,
|
|
40
|
+
get,
|
|
41
|
+
$$delete,
|
|
42
|
+
offloadObjects,
|
|
43
|
+
putOffload,
|
|
44
|
+
getOffload,
|
|
45
|
+
reset,
|
|
46
|
+
}
|
|
47
|
+
/* No side effect */
|
|
@@ -193,19 +193,21 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
193
193
|
|
|
194
194
|
let publishSaved = (~changeKind: string, id: string, state: JSON.t) => {
|
|
195
195
|
let subKey = getSubKey(state, subIdField)
|
|
196
|
-
let descriptor =
|
|
196
|
+
let descriptor = LocalStateChangeDescriptor.make(
|
|
197
197
|
~changeKind,
|
|
198
198
|
~id=entityKeyFor(id, subKey),
|
|
199
199
|
~state=Some(state),
|
|
200
|
+
~seq=LocalStateChangeDescriptor.nextSequence(),
|
|
200
201
|
)
|
|
201
202
|
Bus.publishStateChange(~name, ~descriptor)
|
|
202
203
|
}
|
|
203
204
|
|
|
204
205
|
let publishRemoved = (id: string, subKey: string) => {
|
|
205
|
-
let descriptor =
|
|
206
|
+
let descriptor = LocalStateChangeDescriptor.make(
|
|
206
207
|
~changeKind="Removed",
|
|
207
208
|
~id=entityKeyFor(id, subKey),
|
|
208
209
|
~state=None,
|
|
210
|
+
~seq=LocalStateChangeDescriptor.nextSequence(),
|
|
209
211
|
)
|
|
210
212
|
Bus.publishStateChange(~name, ~descriptor)
|
|
211
213
|
}
|
|
@@ -6,7 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Stream from "effect/Stream";
|
|
7
7
|
import * as Pulumi from "@pulumi/pulumi";
|
|
8
8
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
9
|
-
import * as
|
|
9
|
+
import * as LocalStateChangeDescriptor$ReventlessLocal from "../LocalStateChangeDescriptor.res.mjs";
|
|
10
10
|
|
|
11
11
|
function getSubKey(item, subIdField) {
|
|
12
12
|
if (subIdField === undefined) {
|
|
@@ -167,11 +167,11 @@ function Make(Bus) {
|
|
|
167
167
|
};
|
|
168
168
|
let publishSaved = (changeKind, id, state) => {
|
|
169
169
|
let subKey = getSubKey(state, subIdField);
|
|
170
|
-
let descriptor =
|
|
170
|
+
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make(changeKind, entityKeyFor(id, subKey), state, LocalStateChangeDescriptor$ReventlessLocal.nextSequence());
|
|
171
171
|
Bus.publishStateChange(name, descriptor);
|
|
172
172
|
};
|
|
173
173
|
let publishRemoved = (id, subKey) => {
|
|
174
|
-
let descriptor =
|
|
174
|
+
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make("Removed", entityKeyFor(id, subKey), undefined, LocalStateChangeDescriptor$ReventlessLocal.nextSequence());
|
|
175
175
|
Bus.publishStateChange(name, descriptor);
|
|
176
176
|
};
|
|
177
177
|
let save = async (id, state, _saveMode, ttl) => {
|
|
@@ -257,10 +257,11 @@ let makeStorage = (
|
|
|
257
257
|
|
|
258
258
|
let publishSaved = (~changeKind: string, id: string, state: JSON.t) => {
|
|
259
259
|
let subKey = computeSubKey(state, subIdField)
|
|
260
|
-
let descriptor =
|
|
260
|
+
let descriptor = LocalStateChangeDescriptor.make(
|
|
261
261
|
~changeKind,
|
|
262
262
|
~id=entityKeyFor(id, subKey),
|
|
263
263
|
~state=Some(state),
|
|
264
|
+
~seq=LocalStateChangeDescriptor.nextSequence(),
|
|
264
265
|
)
|
|
265
266
|
bus.publishStateChange(~name, ~descriptor)
|
|
266
267
|
}
|
|
@@ -278,10 +279,11 @@ let makeStorage = (
|
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
let publishRemoved = (id: string, subKey: string) => {
|
|
281
|
-
let descriptor =
|
|
282
|
+
let descriptor = LocalStateChangeDescriptor.make(
|
|
282
283
|
~changeKind="Removed",
|
|
283
284
|
~id=entityKeyFor(id, subKey),
|
|
284
285
|
~state=None,
|
|
286
|
+
~seq=LocalStateChangeDescriptor.nextSequence(),
|
|
285
287
|
)
|
|
286
288
|
bus.publishStateChange(~name, ~descriptor)
|
|
287
289
|
}
|
|
@@ -5,9 +5,9 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
5
5
|
import * as Stream from "effect/Stream";
|
|
6
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
7
7
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
8
|
-
import * as LocalBus$ReventlessLocal from "../LocalBus.res.mjs";
|
|
9
8
|
import * as SqliteDriver$ReventlessLocal from "../SqliteDriver.res.mjs";
|
|
10
9
|
import * as QueryDbListQuery$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/QueryDbListQuery.res.mjs";
|
|
10
|
+
import * as LocalStateChangeDescriptor$ReventlessLocal from "../LocalStateChangeDescriptor.res.mjs";
|
|
11
11
|
|
|
12
12
|
function tableName(name) {
|
|
13
13
|
return "qdb_" + name.replaceAll("-", "_");
|
|
@@ -194,7 +194,7 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
194
194
|
};
|
|
195
195
|
let publishSaved = (changeKind, id, state) => {
|
|
196
196
|
let subKey = computeSubKey(state, subIdField);
|
|
197
|
-
let descriptor =
|
|
197
|
+
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make(changeKind, entityKeyFor(id, subKey), state, LocalStateChangeDescriptor$ReventlessLocal.nextSequence());
|
|
198
198
|
bus.publishStateChange(name, descriptor);
|
|
199
199
|
};
|
|
200
200
|
let saveKind = (id, state) => {
|
|
@@ -209,7 +209,7 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
209
209
|
}
|
|
210
210
|
};
|
|
211
211
|
let publishRemoved = (id, subKey) => {
|
|
212
|
-
let descriptor =
|
|
212
|
+
let descriptor = LocalStateChangeDescriptor$ReventlessLocal.make("Removed", entityKeyFor(id, subKey), undefined, LocalStateChangeDescriptor$ReventlessLocal.nextSequence());
|
|
213
213
|
bus.publishStateChange(name, descriptor);
|
|
214
214
|
};
|
|
215
215
|
let rowKeysForPartition = id => SqliteDriver$ReventlessLocal.all(selectByPartitionStmt, [id]).map(row => {
|