@objy/server 0.0.0-stage → 0.1.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 (55) hide show
  1. package/API.md +1469 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +9 -0
  4. package/README.md +21 -2
  5. package/objydb.config.json +48 -0
  6. package/package.json +42 -4
  7. package/public/index.html +4134 -0
  8. package/public/objy-db-logo.png +0 -0
  9. package/src/aggregate.js +438 -0
  10. package/src/api.js +15 -0
  11. package/src/auth.js +472 -0
  12. package/src/clock.js +18 -0
  13. package/src/cluster-secret.js +25 -0
  14. package/src/cluster-status.js +562 -0
  15. package/src/compression-stats.js +32 -0
  16. package/src/config.js +360 -0
  17. package/src/crdt.js +1076 -0
  18. package/src/derived-state-scheduler.js +128 -0
  19. package/src/files.js +324 -0
  20. package/src/hnsw.js +269 -0
  21. package/src/http-cors.js +31 -0
  22. package/src/http-router.js +162 -0
  23. package/src/index.js +137 -0
  24. package/src/interfaces/auth.js +123 -0
  25. package/src/interfaces/cluster.js +296 -0
  26. package/src/interfaces/public-object.js +2452 -0
  27. package/src/interfaces/ui.js +38 -0
  28. package/src/lib/crypto.cjs +54 -0
  29. package/src/lib/jwt.cjs +231 -0
  30. package/src/local-wal.js +122 -0
  31. package/src/memory-guard.js +152 -0
  32. package/src/ndjson.js +117 -0
  33. package/src/node-runtime.js +29 -0
  34. package/src/object-hooks.js +282 -0
  35. package/src/object-id.js +7 -0
  36. package/src/object-storage.js +263 -0
  37. package/src/object-store.js +288 -0
  38. package/src/patch.js +87 -0
  39. package/src/permissions.js +178 -0
  40. package/src/persistence.js +1918 -0
  41. package/src/plugin-loader.js +21 -0
  42. package/src/query-planner.js +266 -0
  43. package/src/query.js +416 -0
  44. package/src/references.js +349 -0
  45. package/src/replication-scheduler.js +303 -0
  46. package/src/restore-status.js +77 -0
  47. package/src/runtime.js +47 -0
  48. package/src/server.js +242 -0
  49. package/src/shape.js +286 -0
  50. package/src/shard-map-store.js +85 -0
  51. package/src/shard-map.js +333 -0
  52. package/src/shard-router.js +737 -0
  53. package/src/store.js +3410 -0
  54. package/src/system-secrets.js +107 -0
  55. package/src/vector.js +83 -0
package/src/config.js ADDED
@@ -0,0 +1,360 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { normalizeAuthConfig } from "./auth.js";
4
+ import { normalizeVectorConfig } from "./vector.js";
5
+
6
+ export const DEFAULT_CONFIG_PATH = "objydb.config.json";
7
+ export const DEFAULT_DATA_PREFIX = "objydb";
8
+ export const DEFAULT_FILE_PREFIX = `${DEFAULT_DATA_PREFIX}/files`;
9
+ export const DEFAULT_LOCAL_WAL_DIR = ".objydb/wal";
10
+ export const DEFAULT_S3_BUCKET = "objydb";
11
+ export const DEFAULT_MAX_JSON_BYTES = 64 * 1024 * 1024;
12
+ export const DEFAULT_IMPORT_CONFIG = {
13
+ maxBulkObjects: 10_000,
14
+ maxBulkBytes: DEFAULT_MAX_JSON_BYTES,
15
+ maxObjectBytes: 8 * 1024 * 1024,
16
+ maxPendingPersistenceBytes: 256 * 1024 * 1024,
17
+ retryAfterMs: 1000,
18
+ };
19
+
20
+ export function loadConfig(argv = process.argv.slice(2), cwd = process.cwd(), options = {}) {
21
+ const configPath = resolve(cwd, configPathFromArgs(argv) || DEFAULT_CONFIG_PATH);
22
+ const environment = options.environment || process.env;
23
+ const document = existsSync(configPath) ? readJsonFile(configPath) : {};
24
+ const fileConfig = resolveEnvironmentReferences(document, environment);
25
+ return normalizeConfig(fileConfig, configPath, cliOverridesFromArgs(argv), options);
26
+ }
27
+
28
+ export function normalizeConfig(config = {}, configPath = null, overrides = {}, options = {}) {
29
+ const requireNode = options.requireNode !== false;
30
+ const nodeConfig = config.node || {};
31
+ const serverConfig = config.server || {};
32
+ const nodeId = overrides.nodeId || nodeConfig.id;
33
+ const port = positiveInt(overrides.port ?? serverConfig.port ?? nodeConfig.port, 8080);
34
+
35
+ if (requireNode && !nodeId) {
36
+ throw new Error("node id is required; pass --node <id>");
37
+ }
38
+
39
+ return {
40
+ configPath,
41
+ node: {
42
+ id: nodeId,
43
+ url: overrides.url || nodeConfig.url || `http://localhost:${port}`,
44
+ retainOps: positiveInt(nodeConfig.retainOps, 256),
45
+ syncBatchSize: positiveInt(nodeConfig.syncBatchSize, 1024),
46
+ },
47
+ server: {
48
+ port,
49
+ maxJsonBytes: positiveInt(serverConfig.maxJsonBytes, DEFAULT_MAX_JSON_BYTES),
50
+ cors: serverConfig.cors ?? false,
51
+ },
52
+ imports: normalizeImportConfig(config.imports),
53
+ plugins: config.plugins ?? [],
54
+ storage: normalizeStorageConfig(config.storage),
55
+ files: normalizeFilesConfig(config.files, config.persistence),
56
+ vector: normalizeVectorConfig(config.vector),
57
+ memoryGuard: normalizeMemoryGuardConfig(config.memoryGuard),
58
+ cluster: {
59
+ syncMs: positiveInt(config.cluster?.syncMs, 2500),
60
+ forwardTimeoutMs: positiveInt(config.cluster?.forwardTimeoutMs, 60_000),
61
+ replicationTimeoutMs: positiveInt(config.cluster?.replicationTimeoutMs, 60_000),
62
+ hints: normalizeReplicationHintsConfig(config.cluster?.hints),
63
+ secret: clusterSecret(config.cluster),
64
+ shardMap: config.cluster?.shardMap,
65
+ shardMapStore: normalizeShardMapStoreConfig(config.cluster?.shardMapStore, config.persistence),
66
+ },
67
+ auth: normalizeAuthConfig(config.auth),
68
+ persistence: normalizePersistenceConfig(config.persistence),
69
+ };
70
+ }
71
+
72
+ function normalizeFilesConfig(config = {}, persistenceConfig = {}) {
73
+ const mode = config.mode || "off";
74
+ if (!["off", "memory", "s3"].includes(mode)) {
75
+ throw new Error(`unsupported files mode: ${mode}`);
76
+ }
77
+ const s3Config = mode === "s3"
78
+ ? {
79
+ ...(persistenceConfig?.s3 || {}),
80
+ ...(config.s3 || {}),
81
+ }
82
+ : (config.s3 || {});
83
+ return {
84
+ mode,
85
+ prefix: config.prefix || DEFAULT_FILE_PREFIX,
86
+ maxUploadBytes: positiveInt(config.maxUploadBytes, 256 * 1024 * 1024),
87
+ s3: normalizeS3Config(s3Config, "files.s3"),
88
+ };
89
+ }
90
+
91
+ function normalizeStorageConfig(config = {}) {
92
+ const engine = config.engine || "object";
93
+ if (!["shape", "object"].includes(engine)) {
94
+ throw new Error("storage.engine must be one of: shape, object");
95
+ }
96
+ return {
97
+ engine,
98
+ object: normalizeObjectStorageConfig(config.object),
99
+ };
100
+ }
101
+
102
+ function normalizeObjectStorageConfig(config = {}) {
103
+ return {
104
+ valueDirectory: normalizeValueDirectoryConfig(config.valueDirectory),
105
+ };
106
+ }
107
+
108
+ function normalizeValueDirectoryConfig(config = {}) {
109
+ return {
110
+ enabled: config.enabled !== false,
111
+ minRefs: positiveInt(config.minRefs, 2),
112
+ maxEntries: positiveInt(config.maxEntries, 100_000),
113
+ maxCandidates: positiveInt(config.maxCandidates, 4_096),
114
+ maxCandidatesPerPath: positiveInt(config.maxCandidatesPerPath, 256),
115
+ maxPaths: positiveInt(config.maxPaths, 1_024),
116
+ maxStringLength: positiveInt(config.maxStringLength, 256),
117
+ maxKeyBytes: positiveInt(config.maxKeyBytes, 512),
118
+ poolStructuredValues: config.poolStructuredValues === true,
119
+ maxNestedContainerEntries: positiveInt(config.maxNestedContainerEntries, 64),
120
+ };
121
+ }
122
+
123
+ function normalizeMemoryGuardConfig(config = {}) {
124
+ const enabled = config.enabled === true;
125
+ const rejectWritesPercent = percent(config.rejectWritesPercent ?? config.maxRssPercent, 85);
126
+ const resumeWritesPercent = percent(config.resumeWritesPercent, Math.min(78, rejectWritesPercent));
127
+ if (resumeWritesPercent > rejectWritesPercent) {
128
+ throw new Error("memoryGuard.resumeWritesPercent must be <= memoryGuard.rejectWritesPercent");
129
+ }
130
+ return {
131
+ enabled,
132
+ limitMb: positiveNumber(config.limitMb, null),
133
+ rejectWritesPercent,
134
+ resumeWritesPercent,
135
+ checkMs: positiveInt(config.checkMs, 1000),
136
+ };
137
+ }
138
+
139
+ function normalizeReplicationHintsConfig(config = {}) {
140
+ return {
141
+ enabled: config.enabled !== false,
142
+ debounceMs: positiveInt(config.debounceMs, 25),
143
+ maxScopesPerHint: positiveInt(config.maxScopesPerHint, 128),
144
+ };
145
+ }
146
+
147
+ function normalizeShardMapStoreConfig(config = {}, persistenceConfig = {}) {
148
+ const mode = config.mode || "off";
149
+ if (!["off", "memory", "s3"].includes(mode)) {
150
+ throw new Error(`unsupported shard map store mode: ${mode}`);
151
+ }
152
+ return {
153
+ mode,
154
+ prefix: config.prefix || persistenceConfig?.prefix || DEFAULT_DATA_PREFIX,
155
+ key: config.key || "cluster/shard-map.json",
156
+ s3: {
157
+ ...(persistenceConfig?.s3 || {}),
158
+ ...(config.s3 || {}),
159
+ },
160
+ };
161
+ }
162
+
163
+ function normalizePersistenceConfig(config = {}) {
164
+ const mode = config.mode || "off";
165
+ if (!["off", "memory", "s3"].includes(mode)) {
166
+ throw new Error(`unsupported persistence mode: ${mode}`);
167
+ }
168
+ const ack = ackMode(config.ack);
169
+
170
+ return {
171
+ mode,
172
+ prefix: config.prefix || DEFAULT_DATA_PREFIX,
173
+ flushMs: positiveInt(config.flushMs, 5000),
174
+ opBatchSize: positiveInt(config.opBatchSize, 1000),
175
+ journalMaxBytes: positiveInt(config.journalMaxBytes, 4 * 1024 * 1024),
176
+ snapshotMs: positiveInt(config.snapshotMs, 60_000),
177
+ segmentBytes: positiveInt(config.segmentBytes, 16 * 1024 * 1024),
178
+ compactJournals: config.compactJournals !== false,
179
+ snapshotRetention: normalizeRetentionConfig(config.snapshotRetention),
180
+ journalRetention: normalizeRetentionConfig(config.journalRetention),
181
+ snapshotDirtyOnly: config.snapshotDirtyOnly !== false,
182
+ snapshotRole: snapshotRole(config.snapshotRole),
183
+ restoreSnapshotId: optionalString(config.restoreSnapshotId),
184
+ restoreScopes: Array.isArray(config.restoreScopes) ? config.restoreScopes : undefined,
185
+ ack,
186
+ localWal: normalizeLocalWalConfig(config.localWal, ack),
187
+ s3: normalizeS3Config(config.s3),
188
+ };
189
+ }
190
+
191
+ function normalizeS3Config(config = {}, label = "persistence.s3") {
192
+ const endpoint = config.endpoint;
193
+ const endpointLocation = typeof endpoint === "string" ? hetznerLocationFromEndpoint(endpoint) : null;
194
+ const configuredRegion = typeof config.region === "string" && config.region.trim()
195
+ ? config.region.trim()
196
+ : null;
197
+ const region = normalizeS3Region(configuredRegion, endpointLocation);
198
+ if (endpointLocation && region !== endpointLocation) {
199
+ throw new Error(`${label}.region must be ${endpointLocation} for endpoint ${endpoint}`);
200
+ }
201
+ return {
202
+ bucket: config.bucket || DEFAULT_S3_BUCKET,
203
+ endpoint,
204
+ region,
205
+ accessKeyId: config.accessKeyId,
206
+ secretAccessKey: config.secretAccessKey,
207
+ forcePathStyle: config.forcePathStyle,
208
+ };
209
+ }
210
+
211
+ function normalizeS3Region(region, endpointLocation) {
212
+ if (!endpointLocation) return region || "us-east-1";
213
+ if (!region || region === "us-east-1" || region === "US") return endpointLocation;
214
+ return region;
215
+ }
216
+
217
+ function hetznerLocationFromEndpoint(endpoint) {
218
+ const host = endpoint.replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
219
+ const match = host.match(/^(fsn1|nbg1|hel1)\.your-objectstorage\.com$/);
220
+ return match?.[1] || null;
221
+ }
222
+
223
+ function configPathFromArgs(argv) {
224
+ const positional = [];
225
+ for (let i = 0; i < argv.length; i++) {
226
+ const arg = argv[i];
227
+ if (arg === "--config" || arg === "-c") return argv[i + 1];
228
+ if (arg.startsWith("--config=")) return arg.slice("--config=".length);
229
+ if (arg === "--node" || arg === "-n" || arg === "--id" || arg === "--url" || arg === "--port" || arg === "-p") {
230
+ i++;
231
+ continue;
232
+ }
233
+ if (
234
+ arg.startsWith("--node=") ||
235
+ arg.startsWith("--id=") ||
236
+ arg.startsWith("--url=") ||
237
+ arg.startsWith("--port=") ||
238
+ arg.startsWith("-p=")
239
+ ) continue;
240
+ if (!arg.startsWith("-")) positional.push(arg);
241
+ }
242
+ return positional[0];
243
+ }
244
+
245
+ function cliOverridesFromArgs(argv) {
246
+ return {
247
+ nodeId: valueFromArgs(argv, ["--node", "-n", "--id"]),
248
+ url: valueFromArgs(argv, ["--url"]),
249
+ port: valueFromArgs(argv, ["--port", "-p"]),
250
+ };
251
+ }
252
+
253
+ function valueFromArgs(argv, names) {
254
+ for (let i = 0; i < argv.length; i++) {
255
+ const arg = argv[i];
256
+ if (names.includes(arg)) return argv[i + 1];
257
+ for (const name of names) {
258
+ if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1);
259
+ }
260
+ }
261
+ return undefined;
262
+ }
263
+
264
+ function readJsonFile(path) {
265
+ try {
266
+ return JSON.parse(readFileSync(path, "utf8"));
267
+ } catch (err) {
268
+ throw new Error(`failed to read config ${path}: ${err.message}`);
269
+ }
270
+ }
271
+
272
+ function resolveEnvironmentReferences(value, environment) {
273
+ if (Array.isArray(value)) {
274
+ return value.map((item) => resolveEnvironmentReferences(item, environment));
275
+ }
276
+ if (!value || typeof value !== "object") return value;
277
+ const keys = Object.keys(value);
278
+ if (keys.length === 1 && keys[0] === "fromEnvironment") {
279
+ const name = value.fromEnvironment;
280
+ const resolved = environment[name];
281
+ if (typeof resolved !== "string" || resolved.length === 0) {
282
+ throw new Error(`required environment variable is missing: ${name}`);
283
+ }
284
+ return resolved;
285
+ }
286
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
287
+ key,
288
+ resolveEnvironmentReferences(item, environment),
289
+ ]));
290
+ }
291
+
292
+ function positiveInt(value, fallback) {
293
+ const n = Number(value);
294
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : fallback;
295
+ }
296
+
297
+ function positiveNumber(value, fallback) {
298
+ const n = Number(value);
299
+ return Number.isFinite(n) && n > 0 ? n : fallback;
300
+ }
301
+
302
+ function percent(value, fallback) {
303
+ const n = Number(value);
304
+ return Number.isFinite(n) && n >= 0 && n <= 100 ? n : fallback;
305
+ }
306
+
307
+ function snapshotRole(value) {
308
+ const role = value || "primary";
309
+ if (role === "primary" || role === "all" || role === "none") return role;
310
+ throw new Error("persistence.snapshotRole must be one of: primary, all, none");
311
+ }
312
+
313
+ function ackMode(value) {
314
+ const mode = value || "memory";
315
+ if (mode === "memory") return "memory";
316
+ if (mode === "localWal" || mode === "local-wal" || mode === "wal") return "localWal";
317
+ throw new Error("persistence.ack must be one of: memory, localWal");
318
+ }
319
+
320
+ function normalizeLocalWalConfig(config = {}, ack = "memory") {
321
+ const rawMode = config.mode ?? (ack === "localWal" ? "on" : "off");
322
+ const mode = rawMode === true ? "on" : rawMode === false ? "off" : rawMode;
323
+ if (mode !== "off" && mode !== "on") throw new Error("persistence.localWal.mode must be one of: off, on");
324
+ if (ack === "localWal" && mode === "off") {
325
+ throw new Error("persistence.ack localWal requires persistence.localWal.mode on");
326
+ }
327
+ return {
328
+ mode,
329
+ dir: config.dir || DEFAULT_LOCAL_WAL_DIR,
330
+ fsync: config.fsync !== false,
331
+ flushMs: positiveInt(config.flushMs, 0),
332
+ maxRecordBytes: positiveInt(config.maxRecordBytes, 4 * 1024 * 1024),
333
+ maxOpsPerRecord: positiveInt(config.maxOpsPerRecord, 1000),
334
+ };
335
+ }
336
+
337
+ function normalizeImportConfig(config = {}) {
338
+ return {
339
+ maxBulkObjects: positiveInt(config.maxBulkObjects, DEFAULT_IMPORT_CONFIG.maxBulkObjects),
340
+ maxBulkBytes: positiveInt(config.maxBulkBytes, DEFAULT_IMPORT_CONFIG.maxBulkBytes),
341
+ maxObjectBytes: positiveInt(config.maxObjectBytes, DEFAULT_IMPORT_CONFIG.maxObjectBytes),
342
+ maxPendingPersistenceBytes: positiveInt(config.maxPendingPersistenceBytes, DEFAULT_IMPORT_CONFIG.maxPendingPersistenceBytes),
343
+ retryAfterMs: positiveInt(config.retryAfterMs, DEFAULT_IMPORT_CONFIG.retryAfterMs),
344
+ };
345
+ }
346
+
347
+ function normalizeRetentionConfig(config = {}) {
348
+ return {
349
+ keepLatest: positiveInt(config.keepLatest, 0),
350
+ keepForMs: positiveInt(config.keepForMs, 0),
351
+ };
352
+ }
353
+
354
+ function optionalString(value) {
355
+ return typeof value === "string" && value.length > 0 ? value : null;
356
+ }
357
+
358
+ function clusterSecret(cluster = {}) {
359
+ return optionalString(cluster?.secret) || optionalString(cluster?.clusterSecret);
360
+ }