@lossless.org/objectstorage 11.0.0 → 11.2.0

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.
Files changed (51) hide show
  1. package/.smartconfig.json +19 -1
  2. package/dist_rust/ruststorage_linux_amd64 +0 -0
  3. package/dist_rust/ruststorage_linux_amd64.tsrust-build.json +4 -4
  4. package/dist_rust/ruststorage_linux_amd64_musl +0 -0
  5. package/dist_rust/ruststorage_linux_amd64_musl.tsrust-build.json +4 -4
  6. package/dist_rust/ruststorage_linux_arm64 +0 -0
  7. package/dist_rust/ruststorage_linux_arm64.tsrust-build.json +4 -4
  8. package/dist_rust/ruststorage_linux_arm64_musl +0 -0
  9. package/dist_rust/ruststorage_linux_arm64_musl.tsrust-build.json +4 -4
  10. package/dist_rust/ruststorage_macos_amd64 +0 -0
  11. package/dist_rust/ruststorage_macos_amd64.tsrust-build.json +4 -4
  12. package/dist_rust/ruststorage_macos_arm64 +0 -0
  13. package/dist_rust/ruststorage_macos_arm64.tsrust-build.json +4 -4
  14. package/dist_ts/00_commitinfo_data.js +1 -1
  15. package/dist_ts/classes.dashboard.d.ts +49 -0
  16. package/dist_ts/classes.dashboard.js +234 -0
  17. package/dist_ts/dashboard.bundled.d.ts +4 -0
  18. package/dist_ts/dashboard.bundled.js +20 -0
  19. package/dist_ts/dashboard.interfaces.d.ts +132 -0
  20. package/dist_ts/dashboard.interfaces.js +2 -0
  21. package/dist_ts/dashboard.plugins.d.ts +5 -0
  22. package/dist_ts/dashboard.plugins.js +10 -0
  23. package/dist_ts/index.d.ts +116 -1
  24. package/dist_ts/index.js +90 -2
  25. package/package.json +11 -7
  26. package/readme.md +400 -22
  27. package/third-party-notices.md +20 -4
  28. package/third_party_licenses/borsh-1.8.1-license.txt +23 -0
  29. package/third_party_licenses/borsh-derive-1.8.1-apache-2.0.txt +201 -0
  30. package/third_party_licenses/cargo-dependencies.html +420 -0
  31. package/third_party_licenses/dyn-clone-1.0.20-license.txt +23 -0
  32. package/third_party_licenses/equivalent-1.0.3-license.txt +25 -0
  33. package/third_party_licenses/hashbrown-0.17.1-license.txt +25 -0
  34. package/third_party_licenses/indexmap-2.14.2-license.txt +25 -0
  35. package/third_party_licenses/inventory.json +310 -8
  36. package/third_party_licenses/proc-macro-crate-3.5.0-license.txt +23 -0
  37. package/third_party_licenses/ref-cast-1.0.27-license.txt +23 -0
  38. package/third_party_licenses/ref-cast-impl-1.0.27-license.txt +23 -0
  39. package/third_party_licenses/schemars-1.2.3-license.txt +21 -0
  40. package/third_party_licenses/syn-3.0.9-license.txt +23 -0
  41. package/third_party_licenses/toml_datetime-1.1.2-license.txt +19 -0
  42. package/third_party_licenses/toml_edit-0.25.17-license.txt +19 -0
  43. package/third_party_licenses/toml_parser-1.1.5-license.txt +19 -0
  44. package/third_party_licenses/winnow-1.0.4-license.txt +18 -0
  45. package/ts/00_commitinfo_data.ts +1 -1
  46. package/ts/classes.dashboard.ts +280 -0
  47. package/ts/dashboard.bundled.ts.third-party-notices.json +1741 -0
  48. package/ts/dashboard.bundled.ts.third-party-notices.txt +5015 -0
  49. package/ts/dashboard.interfaces.ts +148 -0
  50. package/ts/dashboard.plugins.ts +13 -0
  51. package/ts/index.ts +217 -2
@@ -0,0 +1,19 @@
1
+ Copyright (c) Individual contributors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining
2
+ a copy of this software and associated documentation files (the
3
+ "Software"), to deal in the Software without restriction, including
4
+ without limitation the rights to use, copy, modify, merge, publish,
5
+ distribute, sublicense, and/or sell copies of the Software, and to
6
+ permit persons to whom the Software is furnished to do so, subject to
7
+ the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be
10
+ included in all copies or substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
13
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
16
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
17
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
18
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@lossless.org/objectstorage',
6
- version: '11.0.0',
6
+ version: '11.2.0',
7
7
  description: 'A Node.js TypeScript package to create a local S3-compatible storage server using mapped local directories for development and testing purposes.'
8
8
  }
@@ -0,0 +1,280 @@
1
+ import * as plugins from './dashboard.plugins.js';
2
+ import { files as bundledFiles } from './dashboard.bundled.js';
3
+ import type {
4
+ IEvent_ObjectStorageDashboardSample,
5
+ IObjectStorageClusterSnapshot,
6
+ IObjectStorageDashboardSample,
7
+ IReq_ObjectStorageDashboardGetSample,
8
+ } from './dashboard.interfaces.js';
9
+
10
+ /** How often the shared sampler asks the engine for a snapshot. */
11
+ export const DASHBOARD_SAMPLE_INTERVAL_MS = 2_000;
12
+ /** Dashboard sockets admitted at once; the next one is refused. */
13
+ export const DASHBOARD_MAXIMUM_CONNECTIONS = 16;
14
+ /** Largest encoded sample pushed; TypedSocket refuses events above 256 KiB. */
15
+ const MAXIMUM_SAMPLE_BYTES = 200 * 1024;
16
+ const DASHBOARD_TAG = 'objectstorage-dashboard';
17
+ const SAMPLE_EVENT: IEvent_ObjectStorageDashboardSample['name'] = 'objectStorageDashboardSample';
18
+
19
+ const contentTypes: Record<string, string> = {
20
+ '.html': 'text/html; charset=utf-8',
21
+ '.js': 'text/javascript; charset=utf-8',
22
+ '.css': 'text/css; charset=utf-8',
23
+ '.json': 'application/json; charset=utf-8',
24
+ '.txt': 'text/plain; charset=utf-8',
25
+ '.svg': 'image/svg+xml',
26
+ '.woff2': 'font/woff2',
27
+ };
28
+
29
+ export interface IObjectStorageDashboardOptions {
30
+ address: string;
31
+ port: number;
32
+ /** Reads one engine snapshot; called by the shared sampler only. */
33
+ readSnapshot: () => Promise<IObjectStorageClusterSnapshot>;
34
+ }
35
+
36
+ /**
37
+ * The read-only dashboard: the bundled page plus one TypedSocket on its own
38
+ * port. It reads the engine only through `readSnapshot` (one IPC command),
39
+ * and only while at least one dashboard is connected: one shared sampler
40
+ * every two seconds, one sample in flight, missed ticks skipped. Its router
41
+ * holds one read handler; nothing it serves can change the engine.
42
+ */
43
+ export class ObjectStorageDashboard {
44
+ private readonly options: IObjectStorageDashboardOptions;
45
+ private readonly router = new plugins.typedrequest.TypedRouter();
46
+ private readonly typedSocket: plugins.typedsocket.TypedSocket;
47
+ private readonly smartServe: plugins.smartserve.SmartServe;
48
+ private boundPort = 0;
49
+ private timer: ReturnType<typeof setInterval> | undefined;
50
+ private inFlight: Promise<IObjectStorageDashboardSample> | undefined;
51
+ private latestSample: IObjectStorageDashboardSample | undefined;
52
+ private publishFailureLogged = false;
53
+ private stopped = false;
54
+
55
+ constructor(optionsArg: IObjectStorageDashboardOptions) {
56
+ this.options = optionsArg;
57
+ this.router.addTypedHandler(
58
+ new plugins.typedrequest.TypedHandler<IReq_ObjectStorageDashboardGetSample>(
59
+ 'objectStorageDashboardGetSample',
60
+ async () => this.latestSample ?? (await this.takeSample()),
61
+ ),
62
+ );
63
+ this.typedSocket = plugins.typedsocket.TypedSocket.createServer(this.router, {
64
+ onServerConnectionReady: (connection) => {
65
+ this.typedSocket.setServerTag(connection, DASHBOARD_TAG);
66
+ this.startSampler();
67
+ return undefined;
68
+ },
69
+ });
70
+ this.smartServe = new plugins.smartserve.SmartServe({
71
+ port: optionsArg.port,
72
+ hostname: optionsArg.address,
73
+ websocket: {
74
+ typedRouter: this.typedSocket.getServerRoutingSurface(this.router),
75
+ transportOwner: this.typedSocket.webSocketTransportOwner,
76
+ admit: () => {
77
+ if (this.connectionCount() >= DASHBOARD_MAXIMUM_CONNECTIONS) {
78
+ return new Response(
79
+ `The dashboard admits ${DASHBOARD_MAXIMUM_CONNECTIONS} connections at once.`,
80
+ { status: 503, headers: { 'Retry-After': '10' } },
81
+ );
82
+ }
83
+ return true;
84
+ },
85
+ onConnectionOpen: (peer) => {
86
+ // Admission and registration are not one step: upgrades admitted
87
+ // together can pass the check before any of them registers.
88
+ if (this.connectionCount() > DASHBOARD_MAXIMUM_CONNECTIONS) {
89
+ peer.close(1013, 'Dashboard connection limit reached');
90
+ }
91
+ },
92
+ },
93
+ });
94
+ this.typedSocket.attachSmartServe(this.smartServe);
95
+ this.addStaticRoutes();
96
+ }
97
+
98
+ /** The bound TCP port while running. */
99
+ public get port(): number {
100
+ return this.boundPort;
101
+ }
102
+
103
+ public async start(): Promise<void> {
104
+ const instance = await this.smartServe.start();
105
+ this.boundPort = instance.port;
106
+ }
107
+
108
+ /** Stops the sampler, waits for a sample in flight, then closes every socket. */
109
+ public async stop(): Promise<void> {
110
+ this.stopped = true;
111
+ this.stopSampler();
112
+ if (this.inFlight) {
113
+ await this.inFlight;
114
+ }
115
+ await this.typedSocket.stop();
116
+ await this.smartServe.stop();
117
+ this.boundPort = 0;
118
+ }
119
+
120
+ private connectionCount(): number {
121
+ return this.smartServe.getWebSocketConnections().length;
122
+ }
123
+
124
+ private addStaticRoutes(): void {
125
+ const index = bundledFiles.find((file) => file.path === 'index.html');
126
+ if (!index) {
127
+ throw new Error('The dashboard bundle has no index.html; run `tsrepo bundle`.');
128
+ }
129
+ const serve = (file: { path: string; contentBase64: string }) => {
130
+ const body = Buffer.from(file.contentBase64, 'base64');
131
+ const extension = file.path.slice(file.path.lastIndexOf('.'));
132
+ const headers = {
133
+ 'Content-Type': contentTypes[extension] ?? 'application/octet-stream',
134
+ 'Cache-Control': 'no-cache',
135
+ 'X-Content-Type-Options': 'nosniff',
136
+ 'Referrer-Policy': 'no-referrer',
137
+ };
138
+ return () => new Response(body, { status: 200, headers });
139
+ };
140
+ this.smartServe.addRoute('/', 'GET', serve(index));
141
+ for (const file of bundledFiles) {
142
+ this.smartServe.addRoute(`/${file.path}`, 'GET', serve(file));
143
+ }
144
+ }
145
+
146
+ private startSampler(): void {
147
+ if (this.timer || this.stopped) {
148
+ return;
149
+ }
150
+ this.timer = setInterval(() => this.tick(), DASHBOARD_SAMPLE_INTERVAL_MS);
151
+ this.tick();
152
+ }
153
+
154
+ private stopSampler(): void {
155
+ if (this.timer) {
156
+ clearInterval(this.timer);
157
+ this.timer = undefined;
158
+ }
159
+ }
160
+
161
+ private tick(): void {
162
+ if (this.stopped) {
163
+ return;
164
+ }
165
+ if (this.connectionCount() === 0) {
166
+ this.stopSampler();
167
+ this.latestSample = undefined;
168
+ return;
169
+ }
170
+ if (this.inFlight) {
171
+ return;
172
+ }
173
+ void this.takeSample().then((sample) => this.publish(sample));
174
+ }
175
+
176
+ /**
177
+ * Pushes one sample to every dashboard. Never throws: it ends the sampler's
178
+ * promise chain, so a throw here would be an unhandled rejection.
179
+ */
180
+ private publish(sampleArg: IObjectStorageDashboardSample): void {
181
+ if (this.stopped) {
182
+ return;
183
+ }
184
+ let sampleFailure: unknown;
185
+ try {
186
+ this.typedSocket.publishToTag<IEvent_ObjectStorageDashboardSample>(
187
+ DASHBOARD_TAG,
188
+ SAMPLE_EVENT,
189
+ sampleArg,
190
+ );
191
+ this.publishFailureLogged = false;
192
+ return;
193
+ } catch (error) {
194
+ sampleFailure = error;
195
+ }
196
+ // A sample TypedSocket refuses (too large even without peer events)
197
+ // reaches the dashboards as an error instead of silence.
198
+ try {
199
+ this.typedSocket.publishToTag<IEvent_ObjectStorageDashboardSample>(
200
+ DASHBOARD_TAG,
201
+ SAMPLE_EVENT,
202
+ {
203
+ sampledAt: sampleArg.sampledAt,
204
+ snapshot: null,
205
+ error: `The sample could not be pushed: ${describeError(sampleFailure)}`,
206
+ eventsTruncated: false,
207
+ },
208
+ );
209
+ this.publishFailureLogged = false;
210
+ } catch (errorPushFailure) {
211
+ // Neither reached the dashboards, so the process log is the only
212
+ // channel left: once per run of failures, not every tick.
213
+ if (!this.publishFailureLogged) {
214
+ this.publishFailureLogged = true;
215
+ console.error(
216
+ `objectstorage dashboard: a sample could not be pushed (${describeError(sampleFailure)}),`
217
+ + ` nor its error (${describeError(errorPushFailure)}).`,
218
+ );
219
+ }
220
+ }
221
+ }
222
+
223
+ /** Takes one sample, or joins the one in flight. Never rejects. */
224
+ private takeSample(): Promise<IObjectStorageDashboardSample> {
225
+ if (this.inFlight) {
226
+ return this.inFlight;
227
+ }
228
+ const sample = this.readSample().finally(() => {
229
+ this.inFlight = undefined;
230
+ });
231
+ this.inFlight = sample;
232
+ return sample;
233
+ }
234
+
235
+ private async readSample(): Promise<IObjectStorageDashboardSample> {
236
+ const sampledAt = Date.now();
237
+ let sample: IObjectStorageDashboardSample;
238
+ try {
239
+ const snapshot = await this.options.readSnapshot();
240
+ sample = fitSample({ sampledAt, snapshot, error: null, eventsTruncated: false });
241
+ } catch (error) {
242
+ sample = {
243
+ sampledAt,
244
+ snapshot: null,
245
+ error: describeError(error),
246
+ eventsTruncated: false,
247
+ };
248
+ }
249
+ this.latestSample = sample;
250
+ return sample;
251
+ }
252
+ }
253
+
254
+ const describeError = (errorArg: unknown): string =>
255
+ errorArg instanceof Error ? errorArg.message : String(errorArg);
256
+
257
+ /**
258
+ * Keeps a sample within the push limit by halving the events of the peer
259
+ * snapshots until it fits; the local node keeps all of its events.
260
+ */
261
+ const fitSample = (sampleArg: IObjectStorageDashboardSample): IObjectStorageDashboardSample => {
262
+ const encodedBytes = (value: IObjectStorageDashboardSample) =>
263
+ Buffer.byteLength(JSON.stringify(value));
264
+ const peers = sampleArg.snapshot?.cluster?.peers ?? [];
265
+ let sample = sampleArg;
266
+ while (encodedBytes(sample) > MAXIMUM_SAMPLE_BYTES) {
267
+ const longest = Math.max(0, ...peers.map((peer) => peer.snapshot?.events.length ?? 0));
268
+ if (longest === 0) {
269
+ break;
270
+ }
271
+ const keep = Math.floor(longest / 2);
272
+ for (const peer of peers) {
273
+ if (peer.snapshot && peer.snapshot.events.length > keep) {
274
+ peer.snapshot.events = peer.snapshot.events.slice(peer.snapshot.events.length - keep);
275
+ }
276
+ }
277
+ sample = { ...sample, eventsTruncated: true };
278
+ }
279
+ return sample;
280
+ };