@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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function createRestoreStatus() {
|
|
2
|
+
const state = {
|
|
3
|
+
status: "idle",
|
|
4
|
+
phase: "idle",
|
|
5
|
+
startedAt: null,
|
|
6
|
+
finishedAt: null,
|
|
7
|
+
error: null,
|
|
8
|
+
currentScope: null,
|
|
9
|
+
scopesTotal: 0,
|
|
10
|
+
scopesDone: 0,
|
|
11
|
+
snapshotObjects: 0,
|
|
12
|
+
journalOps: 0,
|
|
13
|
+
skippedJournals: 0,
|
|
14
|
+
skippedJournalOps: 0,
|
|
15
|
+
localWalOps: 0,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
start(meta = {}) {
|
|
20
|
+
Object.assign(state, {
|
|
21
|
+
status: "restoring",
|
|
22
|
+
phase: "starting",
|
|
23
|
+
startedAt: new Date().toISOString(),
|
|
24
|
+
finishedAt: null,
|
|
25
|
+
error: null,
|
|
26
|
+
...meta,
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
update(patch = {}) {
|
|
30
|
+
Object.assign(state, patch);
|
|
31
|
+
},
|
|
32
|
+
add(field, count = 1) {
|
|
33
|
+
state[field] = (Number(state[field]) || 0) + (Number(count) || 0);
|
|
34
|
+
},
|
|
35
|
+
scope(scope, index, total) {
|
|
36
|
+
state.phase = "scope";
|
|
37
|
+
state.currentScope = scope ? { ...scope } : null;
|
|
38
|
+
state.scopesDone = Math.max(0, Number(index) || 0);
|
|
39
|
+
state.scopesTotal = Math.max(state.scopesTotal, Number(total) || 0);
|
|
40
|
+
},
|
|
41
|
+
done(stats = {}) {
|
|
42
|
+
Object.assign(state, {
|
|
43
|
+
status: "ready",
|
|
44
|
+
phase: "ready",
|
|
45
|
+
finishedAt: new Date().toISOString(),
|
|
46
|
+
currentScope: null,
|
|
47
|
+
stats: { ...stats },
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
fail(error) {
|
|
51
|
+
Object.assign(state, {
|
|
52
|
+
status: "error",
|
|
53
|
+
phase: "error",
|
|
54
|
+
finishedAt: new Date().toISOString(),
|
|
55
|
+
error: error?.message || String(error),
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
waiting(patch = {}) {
|
|
59
|
+
Object.assign(state, {
|
|
60
|
+
status: "restoring",
|
|
61
|
+
phase: "waiting-topology",
|
|
62
|
+
finishedAt: null,
|
|
63
|
+
...patch,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
snapshot() {
|
|
67
|
+
return {
|
|
68
|
+
...state,
|
|
69
|
+
currentScope: state.currentScope ? { ...state.currentScope } : null,
|
|
70
|
+
stats: state.stats ? { ...state.stats } : undefined,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
get restoring() {
|
|
74
|
+
return state.status === "restoring";
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/runtime.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
2
|
+
|
|
3
|
+
export function randomUUID() {
|
|
4
|
+
const crypto = webCrypto();
|
|
5
|
+
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
6
|
+
|
|
7
|
+
const bytes = randomBytes(16);
|
|
8
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
9
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
10
|
+
const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
11
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function randomBase64Url(size) {
|
|
15
|
+
const bytes = randomBytes(size);
|
|
16
|
+
let out = "";
|
|
17
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
18
|
+
const left = bytes[index];
|
|
19
|
+
const middle = bytes[index + 1];
|
|
20
|
+
const right = bytes[index + 2];
|
|
21
|
+
out += BASE64URL[left >> 2];
|
|
22
|
+
out += BASE64URL[((left & 3) << 4) | ((middle ?? 0) >> 4)];
|
|
23
|
+
if (middle !== undefined) out += BASE64URL[((middle & 15) << 2) | ((right ?? 0) >> 6)];
|
|
24
|
+
if (right !== undefined) out += BASE64URL[right & 63];
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function yieldToEventLoop() {
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
if (typeof globalThis.setImmediate === "function") globalThis.setImmediate(resolve);
|
|
32
|
+
else globalThis.setTimeout(resolve, 0);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function randomBytes(size) {
|
|
37
|
+
const length = Number(size);
|
|
38
|
+
if (!Number.isInteger(length) || length <= 0) throw new TypeError("random byte count must be a positive integer");
|
|
39
|
+
const bytes = new Uint8Array(length);
|
|
40
|
+
webCrypto().getRandomValues(bytes);
|
|
41
|
+
return bytes;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function webCrypto() {
|
|
45
|
+
if (globalThis.crypto?.getRandomValues) return globalThis.crypto;
|
|
46
|
+
throw new Error("Web Crypto is required");
|
|
47
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { createAuthService } from "./auth.js";
|
|
3
|
+
import { localClusterStatus } from "./cluster-status.js";
|
|
4
|
+
import { CRDTNode } from "./crdt.js";
|
|
5
|
+
import { HttpRouter, json } from "./http-router.js";
|
|
6
|
+
import { createAuthInterface } from "./interfaces/auth.js";
|
|
7
|
+
import { createClusterInterface, httpPeer, shardMapFromHttp } from "./interfaces/cluster.js";
|
|
8
|
+
import { createPublicObjectInterface } from "./interfaces/public-object.js";
|
|
9
|
+
import { createUiInterface } from "./interfaces/ui.js";
|
|
10
|
+
import { createCorsHandler } from "./http-cors.js";
|
|
11
|
+
import { normalizeClusterSecret } from "./cluster-secret.js";
|
|
12
|
+
import { createMemoryGuard } from "./memory-guard.js";
|
|
13
|
+
import { createObjectHookRegistry, createObjectHookRunner } from "./object-hooks.js";
|
|
14
|
+
import { createReplicationScheduler } from "./replication-scheduler.js";
|
|
15
|
+
import { ShardRequestRouter } from "./shard-router.js";
|
|
16
|
+
import { randomSecret } from "./system-secrets.js";
|
|
17
|
+
import { nodeCompression, readHeapUsageRatio } from "./node-runtime.js";
|
|
18
|
+
|
|
19
|
+
export { createObjectHookRegistry, httpPeer, shardMapFromHttp };
|
|
20
|
+
|
|
21
|
+
export async function startRestoreStatusServer({
|
|
22
|
+
port = 0,
|
|
23
|
+
node,
|
|
24
|
+
memoryGuard,
|
|
25
|
+
replication,
|
|
26
|
+
restoreStatus,
|
|
27
|
+
} = {}) {
|
|
28
|
+
const n = node || new CRDTNode({ storage: { compression: nodeCompression, readHeapUsageRatio } });
|
|
29
|
+
const server = createServer((req, res) => {
|
|
30
|
+
const url = new URL(req.url || "/", "http://localhost");
|
|
31
|
+
if (url.pathname === "/health") {
|
|
32
|
+
return json(res, 200, {
|
|
33
|
+
ok: true,
|
|
34
|
+
node: n.id,
|
|
35
|
+
restoring: restoreStatus?.restoring === true,
|
|
36
|
+
restore: restoreStatus?.snapshot?.() || null,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (url.pathname === "/api/config" || url.pathname === "/cluster/status") {
|
|
40
|
+
return json(res, 200, {
|
|
41
|
+
...localClusterStatus(n, memoryGuard, replication, restoreStatus, {
|
|
42
|
+
details: url.searchParams.get("details") === "1",
|
|
43
|
+
}),
|
|
44
|
+
local: true,
|
|
45
|
+
reachable: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return json(res, 503, {
|
|
49
|
+
error: "database is restoring",
|
|
50
|
+
restore: restoreStatus?.snapshot?.() || null,
|
|
51
|
+
}, { "retry-after": "5" });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await new Promise((resolve) => server.listen(port, resolve));
|
|
55
|
+
return { server, port: server.address().port };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function startServer({
|
|
59
|
+
port = 0,
|
|
60
|
+
peers = [],
|
|
61
|
+
shardMap,
|
|
62
|
+
node,
|
|
63
|
+
shardMapStore,
|
|
64
|
+
auth,
|
|
65
|
+
persistence,
|
|
66
|
+
durability,
|
|
67
|
+
files,
|
|
68
|
+
memoryGuard,
|
|
69
|
+
clusterSecret,
|
|
70
|
+
forwardTimeoutMs,
|
|
71
|
+
replicationTimeoutMs,
|
|
72
|
+
replication,
|
|
73
|
+
restoreStatus,
|
|
74
|
+
replicationHints,
|
|
75
|
+
maxJsonBytes,
|
|
76
|
+
imports,
|
|
77
|
+
objectHooks,
|
|
78
|
+
environment = process.env,
|
|
79
|
+
bootstrapLogger = console.log,
|
|
80
|
+
interfaces = [],
|
|
81
|
+
useDefaultInterfaces = true,
|
|
82
|
+
cors = false,
|
|
83
|
+
} = {}) {
|
|
84
|
+
const handleCors = createCorsHandler(cors);
|
|
85
|
+
const n = node || new CRDTNode({ storage: { compression: nodeCompression, readHeapUsageRatio } });
|
|
86
|
+
const secret = normalizeClusterSecret(clusterSecret ?? auth?.clusterSecret ?? auth?.jwtSecret) || randomSecret();
|
|
87
|
+
const guard = createMemoryGuard(memoryGuard);
|
|
88
|
+
const hookRunner = createObjectHookRunner(n, objectHooks || createObjectHookRegistry());
|
|
89
|
+
const scheduler = replication || createReplicationScheduler({ node: n, ...normalizeReplicationHintOptions(replicationHints) });
|
|
90
|
+
if (persistence?.restoreScope) {
|
|
91
|
+
n.setLogTruncatedHandler?.(({ scope }) => persistence.restoreScope(scope));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (shardMap) n.replaceShardMap(shardMapFromHttp(shardMap, n.id, { secret, timeoutMs: replicationTimeoutMs }));
|
|
95
|
+
for (const peer of peers) n.addPeer(httpPeer(peer, n.id, { secret, timeoutMs: replicationTimeoutMs }));
|
|
96
|
+
const authService = createAuthService(n, auth, { environment });
|
|
97
|
+
const state = { node: n, auth: authService, server: null, port, router: null, memoryGuard: guard, replication: scheduler, restoreStatus, objectHooks: hookRunner };
|
|
98
|
+
|
|
99
|
+
const router = new HttpRouter({ maxBodyBytes: maxJsonBytes });
|
|
100
|
+
const context = createInterfaceContext({
|
|
101
|
+
node: n,
|
|
102
|
+
shardMapStore,
|
|
103
|
+
auth: authService,
|
|
104
|
+
persistence,
|
|
105
|
+
durability,
|
|
106
|
+
files,
|
|
107
|
+
memoryGuard: guard,
|
|
108
|
+
restoreStatus,
|
|
109
|
+
clusterSecret: secret,
|
|
110
|
+
forwardTimeoutMs,
|
|
111
|
+
replicationScheduler: scheduler,
|
|
112
|
+
imports,
|
|
113
|
+
objectHooks: hookRunner,
|
|
114
|
+
});
|
|
115
|
+
const activeInterfaces = [
|
|
116
|
+
...(useDefaultInterfaces ? defaultInterfaces() : []),
|
|
117
|
+
...interfaces,
|
|
118
|
+
];
|
|
119
|
+
for (const iface of activeInterfaces) await registerInterface(iface, router, context);
|
|
120
|
+
|
|
121
|
+
const server = createServer(async (req, res) => {
|
|
122
|
+
try {
|
|
123
|
+
if (handleCors(req, res)) return;
|
|
124
|
+
if (await router.handle(req, res)) return;
|
|
125
|
+
json(res, 404, { error: "not found" });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (!res.writableEnded) json(res, err.status || 500, err.body || { error: err.message }, err.headers || {});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
server.once("close", () => hookRunner.dispose());
|
|
131
|
+
|
|
132
|
+
await new Promise((resolve) => server.listen(port, resolve));
|
|
133
|
+
state.server = server;
|
|
134
|
+
state.port = server.address().port;
|
|
135
|
+
state.router = router;
|
|
136
|
+
const bootstrap = authService.consumeBootstrapNotice?.();
|
|
137
|
+
if (bootstrap) {
|
|
138
|
+
await persistence?.afterWrite?.();
|
|
139
|
+
await persistence?.flush?.();
|
|
140
|
+
scheduler.afterLocalWrite?.();
|
|
141
|
+
bootstrapLogger?.(bootstrapMessage(bootstrap));
|
|
142
|
+
}
|
|
143
|
+
n.startIndexWarmup?.();
|
|
144
|
+
n.startVectorIndexWarmup().catch((err) => {
|
|
145
|
+
console.error(`vector index warmup failed: ${err.message}`);
|
|
146
|
+
});
|
|
147
|
+
return state;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function bootstrapMessage({ username, password, supplied }) {
|
|
151
|
+
if (supplied) return `OBJY DB initialized global administrator ${username} from OBJYDB_ADMIN_PASSWORD`;
|
|
152
|
+
return [
|
|
153
|
+
"OBJY DB initialized global administrator",
|
|
154
|
+
`username: ${username}`,
|
|
155
|
+
`password: ${password}`,
|
|
156
|
+
"This password will not be displayed again.",
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function defaultInterfaces() {
|
|
161
|
+
return [
|
|
162
|
+
createClusterInterface(),
|
|
163
|
+
createAuthInterface(),
|
|
164
|
+
createPublicObjectInterface(),
|
|
165
|
+
createUiInterface(),
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function createInterfaceContext({
|
|
170
|
+
node,
|
|
171
|
+
shardMapStore,
|
|
172
|
+
auth,
|
|
173
|
+
persistence,
|
|
174
|
+
files,
|
|
175
|
+
durability,
|
|
176
|
+
memoryGuard,
|
|
177
|
+
restoreStatus,
|
|
178
|
+
clusterSecret,
|
|
179
|
+
forwardTimeoutMs,
|
|
180
|
+
replicationScheduler,
|
|
181
|
+
imports,
|
|
182
|
+
objectHooks,
|
|
183
|
+
}) {
|
|
184
|
+
const durableWrite = durability || {
|
|
185
|
+
afterWrite: () => persistence?.afterWrite?.() || Promise.resolve({ records: 0, ops: 0 }),
|
|
186
|
+
getStats: () => persistence?.getStats?.() || null,
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
node,
|
|
190
|
+
shardMapStore,
|
|
191
|
+
persistence,
|
|
192
|
+
auth,
|
|
193
|
+
files,
|
|
194
|
+
imports,
|
|
195
|
+
clusterSecret,
|
|
196
|
+
memoryGuard,
|
|
197
|
+
restoreStatus,
|
|
198
|
+
routing: new ShardRequestRouter({ node, forwardTimeoutMs, clusterSecret }),
|
|
199
|
+
objectHooks,
|
|
200
|
+
replicationScheduler,
|
|
201
|
+
durability: {
|
|
202
|
+
...durableWrite,
|
|
203
|
+
afterWrite: async () => {
|
|
204
|
+
const result = await durableWrite.afterWrite();
|
|
205
|
+
replicationScheduler?.afterLocalWrite?.();
|
|
206
|
+
return result;
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
data: {
|
|
210
|
+
node,
|
|
211
|
+
store: node.db,
|
|
212
|
+
},
|
|
213
|
+
replication: {
|
|
214
|
+
syncAll: () => node.syncAll(),
|
|
215
|
+
httpPeer,
|
|
216
|
+
},
|
|
217
|
+
cluster: {
|
|
218
|
+
shardMapStore,
|
|
219
|
+
shardMapFromHttp,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeReplicationHintOptions(options = {}) {
|
|
225
|
+
const { enabled, ...rest } = options || {};
|
|
226
|
+
return {
|
|
227
|
+
...rest,
|
|
228
|
+
...(enabled !== undefined ? { hintsEnabled: enabled } : {}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function registerInterface(iface, router, context) {
|
|
233
|
+
if (typeof iface === "function") {
|
|
234
|
+
await iface(router, context);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (iface && typeof iface.register === "function") {
|
|
238
|
+
await iface.register(router, context);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
throw new TypeError("interface must be a function or an object with register(router, context)");
|
|
242
|
+
}
|
package/src/shape.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
export function valueKind(value) {
|
|
2
|
+
if (value === null) return "null";
|
|
3
|
+
if (Array.isArray(value)) return "array";
|
|
4
|
+
return typeof value;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function typeOf(value) {
|
|
8
|
+
return valueKind(value);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function shapeId(fields) {
|
|
12
|
+
let h = 0x811c9dc5;
|
|
13
|
+
const str = JSON.stringify(fields);
|
|
14
|
+
for (let i = 0; i < str.length; i++) {
|
|
15
|
+
h ^= str.charCodeAt(i);
|
|
16
|
+
h = Math.imul(h, 0x01000193);
|
|
17
|
+
}
|
|
18
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function layoutOf(entries) {
|
|
22
|
+
return [...new Set(entries.map(([name]) => name))]
|
|
23
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
24
|
+
.map((name) => ({ name }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stableKey(value) {
|
|
28
|
+
return `${valueKind(value)}:${JSON.stringify(sortForKey(value))}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sortForKey(value) {
|
|
32
|
+
if (Array.isArray(value)) return value.map(sortForKey);
|
|
33
|
+
if (value && typeof value === "object") {
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
Object.entries(value)
|
|
36
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
37
|
+
.map(([key, val]) => [key, sortForKey(val)]),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cloneValue(value) {
|
|
44
|
+
if (value === null || typeof value !== "object") return value;
|
|
45
|
+
return structuredClone(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function estBytes(value) {
|
|
49
|
+
if (value === null || value === undefined) return 0;
|
|
50
|
+
if (typeof value === "number") return 8;
|
|
51
|
+
if (typeof value === "boolean") return 1;
|
|
52
|
+
if (typeof value === "string") return value.length * 2;
|
|
53
|
+
return JSON.stringify(value).length * 2;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class ValueColumn {
|
|
57
|
+
constructor(capacity = 8) {
|
|
58
|
+
this.codes = new Array(capacity).fill(0);
|
|
59
|
+
this.keyToCode = new Map();
|
|
60
|
+
this.values = [undefined];
|
|
61
|
+
this.refs = [0];
|
|
62
|
+
this.freeCodes = [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ensure(row) {
|
|
66
|
+
if (row < this.codes.length) return;
|
|
67
|
+
let cap = this.codes.length || 8;
|
|
68
|
+
while (row >= cap) cap *= 2;
|
|
69
|
+
this.codes.length = cap;
|
|
70
|
+
this.codes.fill(0, row + 1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
set(row, value) {
|
|
74
|
+
this.ensure(row);
|
|
75
|
+
const oldCode = this.codes[row];
|
|
76
|
+
if (oldCode) this._release(oldCode);
|
|
77
|
+
const nextCode = this._codeFor(value);
|
|
78
|
+
this.refs[nextCode]++;
|
|
79
|
+
this.codes[row] = nextCode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get(row) {
|
|
83
|
+
return cloneValue(this.values[this.codes[row]]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getForQuery(row) {
|
|
87
|
+
return this.values[this.codes[row]];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
clear(row) {
|
|
91
|
+
const code = this.codes[row];
|
|
92
|
+
if (!code) return;
|
|
93
|
+
this._release(code);
|
|
94
|
+
this.codes[row] = 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
bytes() {
|
|
98
|
+
let total = this.codes.length * 8;
|
|
99
|
+
for (const value of this.values) {
|
|
100
|
+
if (value !== undefined) total += estBytes(value);
|
|
101
|
+
}
|
|
102
|
+
return total;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
stats() {
|
|
106
|
+
return {
|
|
107
|
+
kind: "value",
|
|
108
|
+
capacity: this.codes.length,
|
|
109
|
+
values: this.keyToCode.size,
|
|
110
|
+
bytes: this.bytes(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_codeFor(value) {
|
|
115
|
+
const key = stableKey(value);
|
|
116
|
+
const existing = this.keyToCode.get(key);
|
|
117
|
+
if (existing) return existing;
|
|
118
|
+
|
|
119
|
+
const code = this.freeCodes.pop() || this.values.length;
|
|
120
|
+
this.keyToCode.set(key, code);
|
|
121
|
+
this.values[code] = cloneValue(value);
|
|
122
|
+
this.refs[code] = 0;
|
|
123
|
+
return code;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_release(code) {
|
|
127
|
+
this.refs[code]--;
|
|
128
|
+
if (this.refs[code] > 0) return;
|
|
129
|
+
|
|
130
|
+
const key = stableKey(this.values[code]);
|
|
131
|
+
this.keyToCode.delete(key);
|
|
132
|
+
this.values[code] = undefined;
|
|
133
|
+
this.refs[code] = 0;
|
|
134
|
+
this.freeCodes.push(code);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class Shape {
|
|
139
|
+
constructor(fields) {
|
|
140
|
+
this.id = shapeId(fields);
|
|
141
|
+
this.fields = fields;
|
|
142
|
+
this.byName = new Set(fields.map((f) => f.name));
|
|
143
|
+
this.columns = {};
|
|
144
|
+
for (const f of fields) this.columns[f.name] = new ValueColumn();
|
|
145
|
+
this.rows = new Map();
|
|
146
|
+
this.free = [];
|
|
147
|
+
this.length = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
hasField(name) {
|
|
151
|
+
return this.byName.has(name);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_ensure(row) {
|
|
155
|
+
for (const f of this.fields) this.columns[f.name].ensure(row);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
push(id, values) {
|
|
159
|
+
const row = this.free.length ? this.free.pop() : this.length++;
|
|
160
|
+
this._ensure(row);
|
|
161
|
+
this.rows.set(id, row);
|
|
162
|
+
for (const f of this.fields) this.columns[f.name].set(row, values[f.name]);
|
|
163
|
+
return row;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
set(id, name, value) {
|
|
167
|
+
const row = this.rows.get(id);
|
|
168
|
+
if (row === undefined || !this.hasField(name)) return false;
|
|
169
|
+
this.columns[name].set(row, value);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
remove(id) {
|
|
174
|
+
const row = this.rows.get(id);
|
|
175
|
+
if (row === undefined) return;
|
|
176
|
+
for (const f of this.fields) this.columns[f.name].clear(row);
|
|
177
|
+
this.rows.delete(id);
|
|
178
|
+
this.free.push(row);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
read(id) {
|
|
182
|
+
const row = this.rows.get(id);
|
|
183
|
+
if (row === undefined) return undefined;
|
|
184
|
+
const out = {};
|
|
185
|
+
for (const f of this.fields) out[f.name] = this.columns[f.name].get(row);
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
readForQuery(id) {
|
|
190
|
+
const row = this.rows.get(id);
|
|
191
|
+
if (row === undefined) return undefined;
|
|
192
|
+
const out = {};
|
|
193
|
+
for (const f of this.fields) out[f.name] = this.columns[f.name].getForQuery(row);
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
bytes() {
|
|
198
|
+
let total = 0;
|
|
199
|
+
for (const f of this.fields) total += this.columns[f.name].bytes();
|
|
200
|
+
return total;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
columnStats() {
|
|
204
|
+
return Object.fromEntries(this.fields.map((f) => [f.name, this.columns[f.name].stats()]));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export class ShapeStore {
|
|
209
|
+
constructor() {
|
|
210
|
+
this.shapes = new Map();
|
|
211
|
+
this.loc = new Map();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
ensureShape(layout) {
|
|
215
|
+
const id = shapeId(layout);
|
|
216
|
+
let shape = this.shapes.get(id);
|
|
217
|
+
if (!shape) {
|
|
218
|
+
shape = new Shape(layout);
|
|
219
|
+
this.shapes.set(id, shape);
|
|
220
|
+
}
|
|
221
|
+
return shape;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
rebuild(id, live) {
|
|
225
|
+
const old = this.loc.get(id);
|
|
226
|
+
if (old) {
|
|
227
|
+
const shape = this.shapes.get(old.shapeId);
|
|
228
|
+
shape.remove(id);
|
|
229
|
+
if (shape.rows.size === 0) this.shapes.delete(old.shapeId);
|
|
230
|
+
this.loc.delete(id);
|
|
231
|
+
}
|
|
232
|
+
if (!live.length) return;
|
|
233
|
+
const layout = layoutOf(live);
|
|
234
|
+
const shape = this.ensureShape(layout);
|
|
235
|
+
shape.push(id, Object.fromEntries(live));
|
|
236
|
+
this.loc.set(id, { shapeId: shape.id });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
writeField(id, field, value, live) {
|
|
240
|
+
const old = this.loc.get(id);
|
|
241
|
+
if (old) {
|
|
242
|
+
const shape = this.shapes.get(old.shapeId);
|
|
243
|
+
if (shape.hasField(field)) {
|
|
244
|
+
shape.set(id, field, value);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
this.rebuild(id, live);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
removeObject(id) {
|
|
252
|
+
const old = this.loc.get(id);
|
|
253
|
+
if (!old) return;
|
|
254
|
+
const shape = this.shapes.get(old.shapeId);
|
|
255
|
+
shape.remove(id);
|
|
256
|
+
if (shape.rows.size === 0) this.shapes.delete(old.shapeId);
|
|
257
|
+
this.loc.delete(id);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
clear() {
|
|
261
|
+
this.shapes.clear();
|
|
262
|
+
this.loc.clear();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
read(id) {
|
|
266
|
+
const old = this.loc.get(id);
|
|
267
|
+
if (!old) return undefined;
|
|
268
|
+
return this.shapes.get(old.shapeId).read(id);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
readForQuery(id) {
|
|
272
|
+
const old = this.loc.get(id);
|
|
273
|
+
if (!old) return undefined;
|
|
274
|
+
return this.shapes.get(old.shapeId).readForQuery(id);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
shapeStats() {
|
|
278
|
+
return [...this.shapes.values()].map((s) => ({
|
|
279
|
+
shapeId: s.id,
|
|
280
|
+
fields: s.fields,
|
|
281
|
+
rows: s.rows.size,
|
|
282
|
+
bytes: s.bytes(),
|
|
283
|
+
columns: s.columnStats(),
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
}
|