@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.
- package/API.md +1469 -0
- package/LICENSE +21 -0
- package/NOTICE.md +9 -0
- package/README.md +21 -2
- package/objydb.config.json +48 -0
- package/package.json +42 -4
- package/public/index.html +4134 -0
- package/public/objy-db-logo.png +0 -0
- package/src/aggregate.js +438 -0
- package/src/api.js +15 -0
- package/src/auth.js +472 -0
- package/src/clock.js +18 -0
- package/src/cluster-secret.js +25 -0
- package/src/cluster-status.js +562 -0
- package/src/compression-stats.js +32 -0
- package/src/config.js +360 -0
- package/src/crdt.js +1076 -0
- package/src/derived-state-scheduler.js +128 -0
- package/src/files.js +324 -0
- package/src/hnsw.js +269 -0
- package/src/http-cors.js +31 -0
- package/src/http-router.js +162 -0
- package/src/index.js +137 -0
- package/src/interfaces/auth.js +123 -0
- package/src/interfaces/cluster.js +296 -0
- package/src/interfaces/public-object.js +2452 -0
- package/src/interfaces/ui.js +38 -0
- package/src/lib/crypto.cjs +54 -0
- package/src/lib/jwt.cjs +231 -0
- package/src/local-wal.js +122 -0
- package/src/memory-guard.js +152 -0
- package/src/ndjson.js +117 -0
- package/src/node-runtime.js +29 -0
- package/src/object-hooks.js +282 -0
- package/src/object-id.js +7 -0
- package/src/object-storage.js +263 -0
- package/src/object-store.js +288 -0
- package/src/patch.js +87 -0
- package/src/permissions.js +178 -0
- package/src/persistence.js +1918 -0
- package/src/plugin-loader.js +21 -0
- package/src/query-planner.js +266 -0
- package/src/query.js +416 -0
- package/src/references.js +349 -0
- package/src/replication-scheduler.js +303 -0
- package/src/restore-status.js +77 -0
- package/src/runtime.js +47 -0
- package/src/server.js +242 -0
- package/src/shape.js +286 -0
- package/src/shard-map-store.js +85 -0
- package/src/shard-map.js +333 -0
- package/src/shard-router.js +737 -0
- package/src/store.js +3410 -0
- package/src/system-secrets.js +107 -0
- package/src/vector.js +83 -0
package/src/hnsw.js
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
class Heap {
|
|
2
|
+
constructor(compare) {
|
|
3
|
+
this.items = [];
|
|
4
|
+
this.compare = compare;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
get size() {
|
|
8
|
+
return this.items.length;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
peek() {
|
|
12
|
+
return this.items[0];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
push(item) {
|
|
16
|
+
this.items.push(item);
|
|
17
|
+
this.up(this.items.length - 1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pop() {
|
|
21
|
+
const top = this.items[0];
|
|
22
|
+
const last = this.items.pop();
|
|
23
|
+
if (this.items.length > 0) {
|
|
24
|
+
this.items[0] = last;
|
|
25
|
+
this.down(0);
|
|
26
|
+
}
|
|
27
|
+
return top;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
up(index) {
|
|
31
|
+
while (index > 0) {
|
|
32
|
+
const parent = (index - 1) >> 1;
|
|
33
|
+
if (this.compare(this.items[index], this.items[parent]) >= 0) break;
|
|
34
|
+
[this.items[index], this.items[parent]] = [this.items[parent], this.items[index]];
|
|
35
|
+
index = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
down(index) {
|
|
40
|
+
for (;;) {
|
|
41
|
+
let best = index;
|
|
42
|
+
const left = index * 2 + 1;
|
|
43
|
+
const right = left + 1;
|
|
44
|
+
if (left < this.items.length && this.compare(this.items[left], this.items[best]) < 0) best = left;
|
|
45
|
+
if (right < this.items.length && this.compare(this.items[right], this.items[best]) < 0) best = right;
|
|
46
|
+
if (best === index) break;
|
|
47
|
+
[this.items[index], this.items[best]] = [this.items[best], this.items[index]];
|
|
48
|
+
index = best;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function minHeap() {
|
|
54
|
+
return new Heap((left, right) => left.dist - right.dist);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function maxHeap() {
|
|
58
|
+
return new Heap((left, right) => right.dist - left.dist);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function angularDistance(a, b) {
|
|
62
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return 1;
|
|
63
|
+
let dot = 0;
|
|
64
|
+
let na = 0;
|
|
65
|
+
let nb = 0;
|
|
66
|
+
for (let i = 0; i < a.length; i++) {
|
|
67
|
+
dot += a[i] * b[i];
|
|
68
|
+
na += a[i] * a[i];
|
|
69
|
+
nb += b[i] * b[i];
|
|
70
|
+
}
|
|
71
|
+
if (na === 0 || nb === 0) return 1;
|
|
72
|
+
return 1 - dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class HNSWIndex {
|
|
76
|
+
constructor({ M = 16, efConstruction = 200, getVec } = {}) {
|
|
77
|
+
if (typeof getVec !== "function") {
|
|
78
|
+
throw new TypeError("HNSWIndex requires getVec(id)");
|
|
79
|
+
}
|
|
80
|
+
this.M = positiveInt(M, 16);
|
|
81
|
+
this.M0 = this.M * 2;
|
|
82
|
+
this.ef = positiveInt(efConstruction, 200);
|
|
83
|
+
this.mL = 1 / Math.log(this.M);
|
|
84
|
+
this.getVec = getVec;
|
|
85
|
+
this.nodes = new Map();
|
|
86
|
+
this.entryPoint = null;
|
|
87
|
+
this.maxLayer = -1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get size() {
|
|
91
|
+
return this.nodes.size;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
insert(id, vec) {
|
|
95
|
+
const key = String(id);
|
|
96
|
+
if (!Array.isArray(vec)) return;
|
|
97
|
+
if (this.nodes.has(key)) this._unlinkExisting(key);
|
|
98
|
+
|
|
99
|
+
const level = Math.floor(-Math.log(Math.random()) * this.mL);
|
|
100
|
+
const node = { neighbors: Array.from({ length: level + 1 }, () => []) };
|
|
101
|
+
this.nodes.set(key, node);
|
|
102
|
+
|
|
103
|
+
if (this.entryPoint === null) {
|
|
104
|
+
this.entryPoint = key;
|
|
105
|
+
this.maxLayer = level;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const epVec = this.getVec(this.entryPoint);
|
|
110
|
+
let entry = epVec ? [{ id: this.entryPoint, dist: angularDistance(vec, epVec) }] : [];
|
|
111
|
+
|
|
112
|
+
for (let layer = this.maxLayer; layer > level; layer--) {
|
|
113
|
+
const found = this._searchLayer(entry, vec, 1, layer);
|
|
114
|
+
if (found.length) entry = found.slice(0, 1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (let layer = Math.min(level, this.maxLayer); layer >= 0; layer--) {
|
|
118
|
+
const maxNeighbors = layer === 0 ? this.M0 : this.M;
|
|
119
|
+
const candidates = this._searchLayer(entry, vec, this.ef, layer);
|
|
120
|
+
const neighbors = this._selectNeighbors(candidates, maxNeighbors);
|
|
121
|
+
node.neighbors[layer] = neighbors.map((neighbor) => neighbor.id);
|
|
122
|
+
|
|
123
|
+
for (const neighbor of neighbors) {
|
|
124
|
+
const other = this.nodes.get(neighbor.id);
|
|
125
|
+
if (!other) continue;
|
|
126
|
+
if (!other.neighbors[layer]) other.neighbors[layer] = [];
|
|
127
|
+
other.neighbors[layer].push(key);
|
|
128
|
+
if (other.neighbors[layer].length > maxNeighbors) {
|
|
129
|
+
const otherVec = this.getVec(neighbor.id);
|
|
130
|
+
if (!otherVec) continue;
|
|
131
|
+
const pruned = other.neighbors[layer]
|
|
132
|
+
.map((id) => {
|
|
133
|
+
const candidateVec = this.getVec(id);
|
|
134
|
+
return candidateVec ? { id, dist: angularDistance(otherVec, candidateVec) } : null;
|
|
135
|
+
})
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.sort((left, right) => left.dist - right.dist);
|
|
138
|
+
other.neighbors[layer] = this._selectNeighbors(pruned, maxNeighbors).map((item) => item.id);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
entry = candidates;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (level > this.maxLayer) {
|
|
146
|
+
this.maxLayer = level;
|
|
147
|
+
this.entryPoint = key;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
remove(id) {
|
|
152
|
+
const key = String(id);
|
|
153
|
+
if (!this.nodes.has(key)) return;
|
|
154
|
+
this._unlinkExisting(key);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
search(queryVec, k, ef = Math.max(k, 20)) {
|
|
158
|
+
if (!this.entryPoint) return [];
|
|
159
|
+
const epVec = this.getVec(this.entryPoint);
|
|
160
|
+
if (!epVec) return [];
|
|
161
|
+
|
|
162
|
+
let entry = [{ id: this.entryPoint, dist: angularDistance(queryVec, epVec) }];
|
|
163
|
+
for (let layer = this.maxLayer; layer > 0; layer--) {
|
|
164
|
+
const found = this._searchLayer(entry, queryVec, 1, layer);
|
|
165
|
+
if (found.length) entry = found.slice(0, 1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return this._searchLayer(entry, queryVec, Math.max(positiveInt(ef, 20), k), 0).slice(0, k);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
clear() {
|
|
172
|
+
this.nodes.clear();
|
|
173
|
+
this.entryPoint = null;
|
|
174
|
+
this.maxLayer = -1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_searchLayer(entryPoints, queryVec, ef, layer) {
|
|
178
|
+
const visited = new Set(entryPoints.map((entry) => entry.id));
|
|
179
|
+
const candidates = minHeap();
|
|
180
|
+
const result = maxHeap();
|
|
181
|
+
|
|
182
|
+
for (const entry of entryPoints) {
|
|
183
|
+
candidates.push(entry);
|
|
184
|
+
result.push(entry);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
while (candidates.size > 0) {
|
|
188
|
+
const current = candidates.pop();
|
|
189
|
+
if (result.size >= ef && current.dist > result.peek().dist) break;
|
|
190
|
+
|
|
191
|
+
const node = this.nodes.get(current.id);
|
|
192
|
+
if (!node) continue;
|
|
193
|
+
for (const neighborId of node.neighbors[layer] || []) {
|
|
194
|
+
if (visited.has(neighborId)) continue;
|
|
195
|
+
visited.add(neighborId);
|
|
196
|
+
const neighborVec = this.getVec(neighborId);
|
|
197
|
+
if (!neighborVec) continue;
|
|
198
|
+
const dist = angularDistance(queryVec, neighborVec);
|
|
199
|
+
if (result.size < ef || dist < result.peek().dist) {
|
|
200
|
+
candidates.push({ id: neighborId, dist });
|
|
201
|
+
result.push({ id: neighborId, dist });
|
|
202
|
+
if (result.size > ef) result.pop();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const out = [];
|
|
208
|
+
while (result.size > 0) out.push(result.pop());
|
|
209
|
+
return out.reverse();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_selectNeighbors(candidates, maxNeighbors) {
|
|
213
|
+
const selected = [];
|
|
214
|
+
const discarded = [];
|
|
215
|
+
for (const candidate of candidates) {
|
|
216
|
+
if (selected.length >= maxNeighbors) break;
|
|
217
|
+
const candidateVec = this.getVec(candidate.id);
|
|
218
|
+
if (!candidateVec) continue;
|
|
219
|
+
let good = true;
|
|
220
|
+
for (const current of selected) {
|
|
221
|
+
const currentVec = this.getVec(current.id);
|
|
222
|
+
if (currentVec && angularDistance(candidateVec, currentVec) < candidate.dist) {
|
|
223
|
+
good = false;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (good) selected.push(candidate);
|
|
228
|
+
else discarded.push(candidate);
|
|
229
|
+
}
|
|
230
|
+
for (const candidate of discarded) {
|
|
231
|
+
if (selected.length >= maxNeighbors) break;
|
|
232
|
+
selected.push(candidate);
|
|
233
|
+
}
|
|
234
|
+
return selected;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
_unlinkExisting(id) {
|
|
238
|
+
const old = this.nodes.get(id);
|
|
239
|
+
for (let layer = 0; layer < old.neighbors.length; layer++) {
|
|
240
|
+
for (const neighborId of old.neighbors[layer] || []) {
|
|
241
|
+
const neighbor = this.nodes.get(neighborId);
|
|
242
|
+
if (neighbor?.neighbors[layer]) {
|
|
243
|
+
neighbor.neighbors[layer] = neighbor.neighbors[layer].filter((entry) => entry !== id);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.nodes.delete(id);
|
|
248
|
+
if (this.entryPoint === id) this._resetEntryPoint();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_resetEntryPoint() {
|
|
252
|
+
let bestId = null;
|
|
253
|
+
let bestLayer = -1;
|
|
254
|
+
for (const [id, node] of this.nodes) {
|
|
255
|
+
const layer = node.neighbors.length - 1;
|
|
256
|
+
if (layer > bestLayer) {
|
|
257
|
+
bestId = id;
|
|
258
|
+
bestLayer = layer;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
this.entryPoint = bestId;
|
|
262
|
+
this.maxLayer = bestLayer;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function positiveInt(value, fallback) {
|
|
267
|
+
const n = Number(value);
|
|
268
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
269
|
+
}
|
package/src/http-cors.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Apply CORS before routing, including error responses and preflight requests. */
|
|
2
|
+
export function createCorsHandler(config = false) {
|
|
3
|
+
if (config === false || config == null) return () => false;
|
|
4
|
+
const origins = config.origins;
|
|
5
|
+
if (!Array.isArray(origins) || !origins.length || origins.some(origin => typeof origin !== 'string')) {
|
|
6
|
+
throw new TypeError('cors.origins must be a non-empty array of origins');
|
|
7
|
+
}
|
|
8
|
+
const allowed = new Set(origins);
|
|
9
|
+
const methods = 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS';
|
|
10
|
+
const headers = ['authorization', 'content-type', 'prefer', ...(config.headers || []).map(name => name.toLowerCase())];
|
|
11
|
+
return (req, res) => {
|
|
12
|
+
const origin = req.headers.origin;
|
|
13
|
+
if (!origin) return false;
|
|
14
|
+
res.setHeader('vary', 'Origin');
|
|
15
|
+
const accepted = allowed.has('*') || allowed.has(origin);
|
|
16
|
+
if (accepted) {
|
|
17
|
+
res.setHeader('access-control-allow-origin', allowed.has('*') ? '*' : origin);
|
|
18
|
+
res.setHeader('access-control-expose-headers', 'ETag, Link, Retry-After, Content-Disposition');
|
|
19
|
+
}
|
|
20
|
+
if (req.method !== 'OPTIONS') return false;
|
|
21
|
+
const requested = String(req.headers['access-control-request-headers'] || '').toLowerCase().split(',').map(name => name.trim()).filter(Boolean);
|
|
22
|
+
const method = req.headers['access-control-request-method'];
|
|
23
|
+
if (!accepted || !methods.split(', ').includes(method) || requested.some(name => !headers.includes(name))) {
|
|
24
|
+
res.writeHead(403); res.end(); return true;
|
|
25
|
+
}
|
|
26
|
+
res.setHeader('access-control-allow-methods', methods);
|
|
27
|
+
res.setHeader('access-control-allow-headers', headers.join(', '));
|
|
28
|
+
res.setHeader('access-control-max-age', '600');
|
|
29
|
+
res.writeHead(204); res.end(); return true;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { isNdjsonContentType, parseNdjsonStream } from "./ndjson.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_MAX_BODY_BYTES = 64 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
export class HttpRouter {
|
|
6
|
+
constructor({ maxBodyBytes = DEFAULT_MAX_BODY_BYTES } = {}) {
|
|
7
|
+
this.routes = [];
|
|
8
|
+
this.maxBodyBytes = maxBodyBytes;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
add(method, pattern, handler) {
|
|
12
|
+
this.routes.push({ method, pattern, handler, ...compilePattern(pattern) });
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get(pattern, handler) {
|
|
17
|
+
return this.add("GET", pattern, handler);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
post(pattern, handler) {
|
|
21
|
+
return this.add("POST", pattern, handler);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
put(pattern, handler) {
|
|
25
|
+
return this.add("PUT", pattern, handler);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
patch(pattern, handler) {
|
|
29
|
+
return this.add("PATCH", pattern, handler);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
delete(pattern, handler) {
|
|
33
|
+
return this.add("DELETE", pattern, handler);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async handle(req, res) {
|
|
37
|
+
const url = new URL(req.url, "http://localhost");
|
|
38
|
+
req.path = url.pathname;
|
|
39
|
+
req.query = url.searchParams;
|
|
40
|
+
req.maxBodyBytes = this.maxBodyBytes;
|
|
41
|
+
|
|
42
|
+
for (const route of this.routes) {
|
|
43
|
+
if (route.method !== req.method) continue;
|
|
44
|
+
const match = url.pathname.match(route.re);
|
|
45
|
+
if (!match) continue;
|
|
46
|
+
req.params = Object.fromEntries(route.names.map((name, i) => [name, decodeURIComponent(match[i + 1])]));
|
|
47
|
+
await route.handler(req, res);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function json(res, status, body, headers = {}) {
|
|
55
|
+
// A downstream auth rejection cannot undo work already begun by this node.
|
|
56
|
+
if (status >= 400 && res.operationStarted && body?.operationStarted === false) body = { ...body, operationStarted: true };
|
|
57
|
+
const payload = body === undefined ? "" : JSON.stringify(body);
|
|
58
|
+
res.writeHead(status, {
|
|
59
|
+
"content-type": "application/json; charset=utf-8",
|
|
60
|
+
"content-length": Buffer.byteLength(payload),
|
|
61
|
+
...headers,
|
|
62
|
+
});
|
|
63
|
+
res.end(payload);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function readJson(req, fallback = {}) {
|
|
67
|
+
const raw = await readBody(req);
|
|
68
|
+
if (!raw) return fallback;
|
|
69
|
+
return JSON.parse(raw);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function remoteHttpError(remote, fallback) {
|
|
73
|
+
const err = new Error(remote.body?.error || fallback);
|
|
74
|
+
err.status = remote.status;
|
|
75
|
+
err.body = remote.body;
|
|
76
|
+
return err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isNdjsonRequest(req) {
|
|
80
|
+
return isNdjsonContentType(req.headers?.["content-type"]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function* readNdjson(req, { maxObjectBytes = 0 } = {}) {
|
|
84
|
+
let totalBytes = 0;
|
|
85
|
+
const counting = (async function* () {
|
|
86
|
+
for await (const chunk of req) {
|
|
87
|
+
totalBytes += chunk.length;
|
|
88
|
+
req._bodyBytes = totalBytes;
|
|
89
|
+
yield chunk;
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
for await (const entry of parseNdjsonStream(counting, { maxObjectBytes, label: "NDJSON" })) {
|
|
94
|
+
req._bodyBytes = totalBytes;
|
|
95
|
+
yield entry;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function text(res, status, body, contentType = "text/plain; charset=utf-8") {
|
|
100
|
+
const payload = body || "";
|
|
101
|
+
res.writeHead(status, {
|
|
102
|
+
"content-type": contentType,
|
|
103
|
+
"content-length": Buffer.byteLength(payload),
|
|
104
|
+
});
|
|
105
|
+
res.end(payload);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readBody(req) {
|
|
109
|
+
if (req._bodyText !== undefined) return req._bodyText;
|
|
110
|
+
req._bodyText = await new Promise((resolve, reject) => {
|
|
111
|
+
const chunks = [];
|
|
112
|
+
let size = 0;
|
|
113
|
+
let settled = false;
|
|
114
|
+
const maxBodyBytes = req.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
115
|
+
req.on("data", (chunk) => {
|
|
116
|
+
size += chunk.length;
|
|
117
|
+
req._bodyBytes = size;
|
|
118
|
+
if (size > maxBodyBytes) {
|
|
119
|
+
if (settled) return;
|
|
120
|
+
settled = true;
|
|
121
|
+
const err = new Error("payload too large");
|
|
122
|
+
err.status = 413;
|
|
123
|
+
err.body = { error: err.message, maxBodyBytes };
|
|
124
|
+
reject(err);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!settled) chunks.push(chunk);
|
|
128
|
+
});
|
|
129
|
+
req.on("end", () => {
|
|
130
|
+
if (settled) return;
|
|
131
|
+
settled = true;
|
|
132
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
133
|
+
});
|
|
134
|
+
req.on("error", (err) => {
|
|
135
|
+
if (settled) return;
|
|
136
|
+
settled = true;
|
|
137
|
+
reject(err);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
return req._bodyText;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function compilePattern(pattern) {
|
|
144
|
+
if (pattern === "/") return { re: /^\/$/, names: [] };
|
|
145
|
+
const names = [];
|
|
146
|
+
const source = pattern
|
|
147
|
+
.split("/")
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.map((segment) => {
|
|
150
|
+
if (segment.startsWith(":")) {
|
|
151
|
+
names.push(segment.slice(1));
|
|
152
|
+
return "([^/]+)";
|
|
153
|
+
}
|
|
154
|
+
return escapeRegex(segment);
|
|
155
|
+
})
|
|
156
|
+
.join("/");
|
|
157
|
+
return { re: new RegExp(`^/${source}$`), names };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function escapeRegex(value) {
|
|
161
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
162
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { shardMapFromHttp, startRestoreStatusServer, startServer } from "./server.js";
|
|
3
|
+
import { CRDTNode } from "./crdt.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { createFileService } from "./files.js";
|
|
6
|
+
import { createPersistence } from "./persistence.js";
|
|
7
|
+
import { createReplicationScheduler } from "./replication-scheduler.js";
|
|
8
|
+
import { createRestoreStatus } from "./restore-status.js";
|
|
9
|
+
import { createShardMapStore } from "./shard-map-store.js";
|
|
10
|
+
import { createObjectHookRegistry } from "./object-hooks.js";
|
|
11
|
+
import { loadPlugins } from "./plugin-loader.js";
|
|
12
|
+
import { resolveSystemSecrets } from "./system-secrets.js";
|
|
13
|
+
import { isTopologyResharding } from "./shard-map.js";
|
|
14
|
+
import { nodeCompression, readHeapUsageRatio } from "./node-runtime.js";
|
|
15
|
+
|
|
16
|
+
const config = loadConfig();
|
|
17
|
+
|
|
18
|
+
const node = new CRDTNode({
|
|
19
|
+
id: config.node.id,
|
|
20
|
+
url: config.node.url,
|
|
21
|
+
retainOps: config.node.retainOps,
|
|
22
|
+
syncBatchSize: config.node.syncBatchSize,
|
|
23
|
+
storage: { ...config.storage, compression: nodeCompression, readHeapUsageRatio },
|
|
24
|
+
vector: config.vector,
|
|
25
|
+
});
|
|
26
|
+
const shardMapStore = createShardMapStore(config.cluster.shardMapStore, config.persistence);
|
|
27
|
+
const persistence = createPersistence(node, config.persistence);
|
|
28
|
+
const sharedSecrets = config.persistence.mode === "s3"
|
|
29
|
+
? { storage: persistence.storage, prefix: config.persistence.prefix }
|
|
30
|
+
: config.cluster.shardMapStore.mode === "s3"
|
|
31
|
+
? { storage: shardMapStore.storage, prefix: config.cluster.shardMapStore.prefix }
|
|
32
|
+
: {};
|
|
33
|
+
const secrets = await resolveSystemSecrets({
|
|
34
|
+
jwtSecret: config.auth.jwtSecret,
|
|
35
|
+
clusterSecret: config.cluster.secret,
|
|
36
|
+
...sharedSecrets,
|
|
37
|
+
localPath: resolve(config.persistence.localWal.dir, "..", "system-secrets.json"),
|
|
38
|
+
});
|
|
39
|
+
config.auth = { ...config.auth, jwtSecret: secrets.jwtSecret };
|
|
40
|
+
config.cluster.secret = secrets.clusterSecret;
|
|
41
|
+
const storedShardMap = await shardMapStore?.load();
|
|
42
|
+
const configuredShardMap = storedShardMap || config.cluster.shardMap;
|
|
43
|
+
if (configuredShardMap) {
|
|
44
|
+
node.replaceShardMap(shardMapFromHttp(configuredShardMap, node.id, {
|
|
45
|
+
secret: config.cluster.secret,
|
|
46
|
+
timeoutMs: config.cluster.replicationTimeoutMs,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
if (isTopologyResharding(node.shardMapSnapshot()) && (!persistence?.durable || !shardMapStore?.durable)) {
|
|
50
|
+
throw new Error("resharding requires S3 persistence and an S3 shard map store");
|
|
51
|
+
}
|
|
52
|
+
const replication = createReplicationScheduler({
|
|
53
|
+
node,
|
|
54
|
+
hintsEnabled: config.cluster.hints.enabled,
|
|
55
|
+
antiEntropyMs: config.cluster.syncMs,
|
|
56
|
+
debounceMs: config.cluster.hints.debounceMs,
|
|
57
|
+
maxScopesPerHint: config.cluster.hints.maxScopesPerHint,
|
|
58
|
+
});
|
|
59
|
+
const files = createFileService(config.files, config.persistence);
|
|
60
|
+
const restoreStatus = createRestoreStatus();
|
|
61
|
+
const restoreServer = await startRestoreStatusServer({
|
|
62
|
+
port: config.server.port,
|
|
63
|
+
node,
|
|
64
|
+
restoreStatus,
|
|
65
|
+
replication,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (persistence) {
|
|
69
|
+
node.setLogTruncatedHandler?.(({ scope }) => persistence.restoreScope(scope));
|
|
70
|
+
const restored = await persistence.restore({ progress: restoreStatus });
|
|
71
|
+
if (restored.reshardCheckpoint && shardMapStore?.durable && persistence.durable) {
|
|
72
|
+
const readiness = await persistence.reshardReadiness();
|
|
73
|
+
restored.reshardReadiness = readiness;
|
|
74
|
+
let activeMap;
|
|
75
|
+
if (readiness.ready) {
|
|
76
|
+
const finalized = await shardMapStore.finalize(readiness.topologyId);
|
|
77
|
+
activeMap = finalized.shardMap;
|
|
78
|
+
restored.reshardFinalized = true;
|
|
79
|
+
} else {
|
|
80
|
+
restoreStatus.waiting({ topologyId: readiness.topologyId });
|
|
81
|
+
activeMap = await shardMapStore.waitForActive(readiness.topologyId);
|
|
82
|
+
}
|
|
83
|
+
node.replaceShardMap(shardMapFromHttp(activeMap, node.id, {
|
|
84
|
+
secret: config.cluster.secret,
|
|
85
|
+
timeoutMs: config.cluster.replicationTimeoutMs,
|
|
86
|
+
}));
|
|
87
|
+
restoreStatus.done(restored);
|
|
88
|
+
}
|
|
89
|
+
persistence.start();
|
|
90
|
+
console.log(`persistence restored ${restored.snapshotObjects} snapshot objects and ${restored.journalOps} journal ops`);
|
|
91
|
+
} else {
|
|
92
|
+
restoreStatus.done({ disabled: true });
|
|
93
|
+
}
|
|
94
|
+
await new Promise((resolve) => restoreServer.server.close(resolve));
|
|
95
|
+
|
|
96
|
+
const objectHooks = createObjectHookRegistry();
|
|
97
|
+
await loadPlugins(config.plugins, { configPath: config.configPath, node, objectHooks });
|
|
98
|
+
|
|
99
|
+
const state = await startServer({
|
|
100
|
+
port: restoreServer.port,
|
|
101
|
+
maxJsonBytes: config.server.maxJsonBytes,
|
|
102
|
+
cors: config.server.cors,
|
|
103
|
+
node,
|
|
104
|
+
shardMapStore,
|
|
105
|
+
auth: config.auth,
|
|
106
|
+
persistence,
|
|
107
|
+
files,
|
|
108
|
+
restoreStatus,
|
|
109
|
+
memoryGuard: config.memoryGuard,
|
|
110
|
+
clusterSecret: config.cluster.secret,
|
|
111
|
+
forwardTimeoutMs: config.cluster.forwardTimeoutMs,
|
|
112
|
+
replicationTimeoutMs: config.cluster.replicationTimeoutMs,
|
|
113
|
+
replication,
|
|
114
|
+
imports: config.imports,
|
|
115
|
+
objectHooks,
|
|
116
|
+
});
|
|
117
|
+
replication.start();
|
|
118
|
+
console.log(`OBJY DB node ${state.node.id} listening on http://localhost:${state.port}`);
|
|
119
|
+
|
|
120
|
+
let shuttingDown = false;
|
|
121
|
+
|
|
122
|
+
async function shutdown() {
|
|
123
|
+
if (shuttingDown) return;
|
|
124
|
+
shuttingDown = true;
|
|
125
|
+
await replication.stop();
|
|
126
|
+
await new Promise((resolve) => state.server.close(resolve));
|
|
127
|
+
try {
|
|
128
|
+
await persistence?.stop();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
console.error(`persistence flush failed during shutdown: ${err.message}`);
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
}
|
|
133
|
+
process.exit(process.exitCode || 0);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
process.on("SIGINT", shutdown);
|
|
137
|
+
process.on("SIGTERM", shutdown);
|