@reventlessdev/reventless-local 3.0.0-alpha.199 → 3.0.0-alpha.201
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 +137 -86
- package/src/Platform.res.mjs +57 -44
- 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/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,129 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as StoreLayout$ReventlessCore from "@reventlessdev/reventless-core/src/util/StoreLayout.res.mjs";
|
|
6
|
+
import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
|
|
7
|
+
import * as ObjectStoreStorage_InMemory$ReventlessLocal from "./ObjectStoreStorage_InMemory.res.mjs";
|
|
8
|
+
import * as ObjectStoreStorage_FileSystem$ReventlessLocal from "./ObjectStoreStorage_FileSystem.res.mjs";
|
|
9
|
+
|
|
10
|
+
let defaultUploadPrefix = "uploads";
|
|
11
|
+
|
|
12
|
+
let servedPrefixes = {
|
|
13
|
+
contents: [defaultUploadPrefix]
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function registerServedPrefix(prefix) {
|
|
17
|
+
if (!servedPrefixes.contents.includes(prefix)) {
|
|
18
|
+
servedPrefixes.contents = servedPrefixes.contents.concat([prefix]);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let declaredStores = {};
|
|
24
|
+
|
|
25
|
+
function localPrefixFor(qualified) {
|
|
26
|
+
return defaultUploadPrefix + `/` + StoreLayout$ReventlessCore.prefixOfQualified(qualified);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function registerStore(qualified, prefix) {
|
|
30
|
+
declaredStores[qualified] = prefix;
|
|
31
|
+
registerServedPrefix(prefix);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function storePrefix(qualified) {
|
|
35
|
+
return declaredStores[qualified];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function declaredStoreList() {
|
|
39
|
+
return Object.entries(declaredStores);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function servedKey(path) {
|
|
43
|
+
let trimmed = path.startsWith("/") ? path.slice(1, path.length) : path;
|
|
44
|
+
let segments = trimmed.split("/");
|
|
45
|
+
let underAPrefix = servedPrefixes.contents.some(p => {
|
|
46
|
+
if (p.length > 0 && trimmed.startsWith(p + "/")) {
|
|
47
|
+
return trimmed.length > (p.length + 1 | 0);
|
|
48
|
+
} else {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (underAPrefix && segments.every(seg => seg !== "" && seg !== "." ? seg !== ".." : false)) {
|
|
53
|
+
return trimmed;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function put(key, bytes, contentType) {
|
|
58
|
+
let root = BackendState$ReventlessLocal.getObjectStoreRoot();
|
|
59
|
+
if (root !== undefined) {
|
|
60
|
+
return ObjectStoreStorage_FileSystem$ReventlessLocal.put(root, key, bytes, contentType);
|
|
61
|
+
} else {
|
|
62
|
+
return ObjectStoreStorage_InMemory$ReventlessLocal.put(key, bytes, contentType);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function get(key) {
|
|
67
|
+
let root = BackendState$ReventlessLocal.getObjectStoreRoot();
|
|
68
|
+
let stored = root !== undefined ? ObjectStoreStorage_FileSystem$ReventlessLocal.get(root, key) : ObjectStoreStorage_InMemory$ReventlessLocal.get(key);
|
|
69
|
+
return Stdlib_Option.map(stored, param => ({
|
|
70
|
+
bytes: param[0],
|
|
71
|
+
contentType: param[1]
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function $$delete(key) {
|
|
76
|
+
let root = BackendState$ReventlessLocal.getObjectStoreRoot();
|
|
77
|
+
if (root !== undefined) {
|
|
78
|
+
return ObjectStoreStorage_FileSystem$ReventlessLocal.$$delete(root, key);
|
|
79
|
+
} else {
|
|
80
|
+
return ObjectStoreStorage_InMemory$ReventlessLocal.$$delete(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function putOffload(key, bytes) {
|
|
85
|
+
let root = BackendState$ReventlessLocal.getObjectStoreRoot();
|
|
86
|
+
if (root !== undefined) {
|
|
87
|
+
return ObjectStoreStorage_FileSystem$ReventlessLocal.putOffload(root, key, bytes);
|
|
88
|
+
} else {
|
|
89
|
+
return ObjectStoreStorage_InMemory$ReventlessLocal.putOffload(key, bytes);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getOffload(key) {
|
|
94
|
+
let root = BackendState$ReventlessLocal.getObjectStoreRoot();
|
|
95
|
+
if (root !== undefined) {
|
|
96
|
+
return ObjectStoreStorage_FileSystem$ReventlessLocal.getOffload(root, key);
|
|
97
|
+
} else {
|
|
98
|
+
return ObjectStoreStorage_InMemory$ReventlessLocal.getOffload(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function reset() {
|
|
103
|
+
ObjectStoreStorage_InMemory$ReventlessLocal.reset();
|
|
104
|
+
let root = BackendState$ReventlessLocal.getObjectStoreRoot();
|
|
105
|
+
if (root !== undefined) {
|
|
106
|
+
ObjectStoreStorage_FileSystem$ReventlessLocal.reset(root);
|
|
107
|
+
}
|
|
108
|
+
servedPrefixes.contents = [defaultUploadPrefix];
|
|
109
|
+
Object.keys(declaredStores).forEach(k => Stdlib_Dict.$$delete(declaredStores, k));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
defaultUploadPrefix,
|
|
114
|
+
servedPrefixes,
|
|
115
|
+
registerServedPrefix,
|
|
116
|
+
declaredStores,
|
|
117
|
+
localPrefixFor,
|
|
118
|
+
registerStore,
|
|
119
|
+
storePrefix,
|
|
120
|
+
declaredStoreList,
|
|
121
|
+
servedKey,
|
|
122
|
+
put,
|
|
123
|
+
get,
|
|
124
|
+
$$delete,
|
|
125
|
+
putOffload,
|
|
126
|
+
getOffload,
|
|
127
|
+
reset,
|
|
128
|
+
}
|
|
129
|
+
/* BackendState-ReventlessLocal Not a pure module */
|
|
@@ -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 */
|