@delali/narsil 0.2.2 → 0.2.3-next.33
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/README.md +12 -9
- package/dist/adapters/filesystem.d.ts +1 -1
- package/dist/adapters/filesystem.mjs +1 -1
- package/dist/adapters/indexeddb.d.ts +1 -1
- package/dist/adapters/indexeddb.mjs +1 -1
- package/dist/adapters/memory.d.ts +1 -1
- package/dist/{adapters-CVqI5NBt.d.ts → adapters-79OIa4xm.d.ts} +1 -1
- package/dist/brute-force-DbHYCTc3.d.ts +8 -0
- package/dist/chunk-BFIH7OV5.mjs +185 -0
- package/dist/chunk-C6VVLMRX.mjs +63 -0
- package/dist/{chunk-UNJZ7ZDB.mjs → chunk-EMI63EXP.mjs} +33 -2
- package/dist/chunk-MFFAJ2NR.mjs +135 -0
- package/dist/chunk-OBXS7OZM.mjs +376 -0
- package/dist/{chunk-ASI6AOW2.mjs → chunk-RO4EFWE6.mjs} +7833 -3524
- package/dist/{chunk-WLR5BFL3.mjs → chunk-TEAJO7CU.mjs} +3067 -2087
- package/dist/{chunk-S7RJJMHI.mjs → chunk-WDL632CN.mjs} +699 -222
- package/dist/{chunk-BKAI2WXI.mjs → chunk-WTONLDU3.mjs} +5 -0
- package/dist/client-DQHB_swQ.d.ts +793 -0
- package/dist/client.d.ts +6 -0
- package/dist/client.mjs +843 -0
- package/dist/{config-DXypXAfN.d.ts → config-ChZ7lfqc.d.ts} +3 -3
- package/dist/direct-executor-YARCDMJF.mjs +8 -0
- package/dist/distribution/coordinator/etcd.d.ts +3 -3
- package/dist/distribution/coordinator/etcd.mjs +471 -2
- package/dist/distribution/coordinator/in-memory.d.ts +3 -3
- package/dist/distribution/coordinator/in-memory.mjs +286 -1
- package/dist/distribution/transport/grpc.d.ts +45 -0
- package/dist/distribution/transport/grpc.mjs +586 -0
- package/dist/distribution/transport/in-memory.d.ts +118 -0
- package/dist/distribution/transport/in-memory.mjs +214 -0
- package/dist/distribution/transport/tcp.d.ts +27 -0
- package/dist/distribution/transport/tcp.mjs +595 -0
- package/dist/distribution.d.ts +241 -192
- package/dist/distribution.mjs +6620 -4795
- package/dist/embeddings/openai.d.ts +1 -1
- package/dist/embeddings/openai.mjs +1 -1
- package/dist/{engine-CO9Wnfie.d.ts → engine-DScGlT2Z.d.ts} +30 -3
- package/dist/index.browser.mjs +10035 -3914
- package/dist/index.d.ts +8 -8
- package/dist/index.mjs +8 -5
- package/dist/invalidation/broadcast-channel.d.ts +1 -1
- package/dist/invalidation/filesystem.d.ts +1 -1
- package/dist/invalidation/filesystem.mjs +3 -1
- package/dist/invalidation/noop.d.ts +1 -1
- package/dist/options-BORJg5XB.d.ts +234 -0
- package/dist/persistence/durability/checkpoint-worker.d.ts +4 -2
- package/dist/persistence/durability/checkpoint-worker.mjs +8 -5
- package/dist/react.d.ts +340 -0
- package/dist/react.mjs +687 -0
- package/dist/{schema-CE6S_aAr.d.ts → schema-emTQfEiX.d.ts} +21 -7
- package/dist/{search-DLBtiPdQ.d.ts → search-CQ-9bKxo.d.ts} +226 -14
- package/dist/server.d.ts +42 -356
- package/dist/server.mjs +493 -106
- package/dist/tasks-CnMLpfk-.d.ts +274 -0
- package/dist/types-BQDlIvfS.d.ts +34 -0
- package/dist/{types-BV3nAKTL.d.ts → types-Bfhhnh3G.d.ts} +23 -1
- package/dist/types-D5OwhtsQ.d.ts +97 -0
- package/dist/vector/hnsw-build-worker.d.ts +2 -7
- package/dist/vector/hnsw-build-worker.mjs +2 -1
- package/dist/vector/search-worker.d.ts +373 -0
- package/dist/vector/search-worker.mjs +279 -0
- package/dist/workers/entry.mjs +43 -25
- package/package.json +42 -2
- package/dist/chunk-FKK76OR7.mjs +0 -456
- package/dist/chunk-RQLXC2YS.mjs +0 -269
- package/dist/direct-executor-3W4QRA4E.mjs +0 -5
package/dist/workers/entry.mjs
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
1
|
-
import { isValidWorkerAction } from '../chunk-
|
|
1
|
+
import { isValidWorkerAction } from '../chunk-WTONLDU3.mjs';
|
|
2
|
+
import { NarsilError, ErrorCodes } from '../chunk-EMI63EXP.mjs';
|
|
3
|
+
|
|
4
|
+
// src/workers/action-handler.ts
|
|
5
|
+
function buildErrorResponse(requestId, code, message) {
|
|
6
|
+
return { type: "error", requestId, code, message };
|
|
7
|
+
}
|
|
8
|
+
function buildSuccessResponse(requestId, data) {
|
|
9
|
+
return { type: "success", requestId, data };
|
|
10
|
+
}
|
|
11
|
+
async function importBootstrapModule(moduleUrl) {
|
|
12
|
+
if (typeof moduleUrl !== "string" || moduleUrl.trim().length === 0) {
|
|
13
|
+
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "A bootstrap module needs a non-empty module URL", {
|
|
14
|
+
moduleUrl
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
await import(moduleUrl);
|
|
18
|
+
}
|
|
19
|
+
function createActionHandler(executor) {
|
|
20
|
+
return async function handleAction(action, post) {
|
|
21
|
+
if (action.type === "shutdown") {
|
|
22
|
+
await executor.shutdown();
|
|
23
|
+
post(buildSuccessResponse(action.requestId, void 0));
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
if (action.type === "bootstrap") {
|
|
28
|
+
await importBootstrapModule(action.moduleUrl);
|
|
29
|
+
post(buildSuccessResponse(action.requestId, void 0));
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const result = await executor.execute(action);
|
|
33
|
+
post(buildSuccessResponse(action.requestId, result));
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const code = err?.code ?? "UNKNOWN_ERROR";
|
|
36
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
+
post(buildErrorResponse(action.requestId, code, message));
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
2
42
|
|
|
3
43
|
// src/workers/worker-entry.ts
|
|
4
44
|
function startWorker() {
|
|
@@ -15,30 +55,8 @@ async function setup() {
|
|
|
15
55
|
} catch {
|
|
16
56
|
parentPort = null;
|
|
17
57
|
}
|
|
18
|
-
const { createDirectExecutor } = await import('../direct-executor-
|
|
19
|
-
const
|
|
20
|
-
function buildErrorResponse(requestId, code, message) {
|
|
21
|
-
return { type: "error", requestId, code, message };
|
|
22
|
-
}
|
|
23
|
-
function buildSuccessResponse(requestId, data) {
|
|
24
|
-
return { type: "success", requestId, data };
|
|
25
|
-
}
|
|
26
|
-
async function handleAction(action, postFn) {
|
|
27
|
-
if (action.type === "shutdown") {
|
|
28
|
-
await executor.shutdown();
|
|
29
|
-
postFn(buildSuccessResponse(action.requestId, void 0));
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
try {
|
|
33
|
-
const result = await executor.execute(action);
|
|
34
|
-
postFn(buildSuccessResponse(action.requestId, result));
|
|
35
|
-
} catch (err) {
|
|
36
|
-
const code = err?.code ?? "UNKNOWN_ERROR";
|
|
37
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
-
postFn(buildErrorResponse(action.requestId, code, message));
|
|
39
|
-
}
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
58
|
+
const { createDirectExecutor } = await import('../direct-executor-YARCDMJF.mjs');
|
|
59
|
+
const handleAction = createActionHandler(createDirectExecutor());
|
|
42
60
|
if (parentPort) {
|
|
43
61
|
const port = parentPort;
|
|
44
62
|
port.on("message", (raw) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delali/narsil",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3-next.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A distributed full-text search engine with cross-language portable binary format.",
|
|
6
6
|
"author": "Delali (https://sondelali.com)",
|
|
@@ -82,6 +82,10 @@
|
|
|
82
82
|
"import": "./dist/vector/hnsw-build-worker.mjs",
|
|
83
83
|
"types": "./dist/vector/hnsw-build-worker.d.ts"
|
|
84
84
|
},
|
|
85
|
+
"./vector/search-worker": {
|
|
86
|
+
"import": "./dist/vector/search-worker.mjs",
|
|
87
|
+
"types": "./dist/vector/search-worker.d.ts"
|
|
88
|
+
},
|
|
85
89
|
"./embeddings/openai": {
|
|
86
90
|
"import": "./dist/embeddings/openai.mjs",
|
|
87
91
|
"types": "./dist/embeddings/openai.d.ts"
|
|
@@ -102,9 +106,29 @@
|
|
|
102
106
|
"import": "./dist/distribution/coordinator/etcd.mjs",
|
|
103
107
|
"types": "./dist/distribution/coordinator/etcd.d.ts"
|
|
104
108
|
},
|
|
109
|
+
"./distribution/transport/tcp": {
|
|
110
|
+
"import": "./dist/distribution/transport/tcp.mjs",
|
|
111
|
+
"types": "./dist/distribution/transport/tcp.d.ts"
|
|
112
|
+
},
|
|
113
|
+
"./distribution/transport/in-memory": {
|
|
114
|
+
"import": "./dist/distribution/transport/in-memory.mjs",
|
|
115
|
+
"types": "./dist/distribution/transport/in-memory.d.ts"
|
|
116
|
+
},
|
|
117
|
+
"./distribution/transport/grpc": {
|
|
118
|
+
"import": "./dist/distribution/transport/grpc.mjs",
|
|
119
|
+
"types": "./dist/distribution/transport/grpc.d.ts"
|
|
120
|
+
},
|
|
105
121
|
"./server": {
|
|
106
122
|
"import": "./dist/server.mjs",
|
|
107
123
|
"types": "./dist/server.d.ts"
|
|
124
|
+
},
|
|
125
|
+
"./client": {
|
|
126
|
+
"import": "./dist/client.mjs",
|
|
127
|
+
"types": "./dist/client.d.ts"
|
|
128
|
+
},
|
|
129
|
+
"./react": {
|
|
130
|
+
"import": "./dist/react.mjs",
|
|
131
|
+
"types": "./dist/react.d.ts"
|
|
108
132
|
}
|
|
109
133
|
},
|
|
110
134
|
"files": [
|
|
@@ -117,11 +141,17 @@
|
|
|
117
141
|
},
|
|
118
142
|
"devDependencies": {
|
|
119
143
|
"@biomejs/biome": "2.4.4",
|
|
144
|
+
"@grpc/grpc-js": "1.14.4",
|
|
120
145
|
"@microsoft/api-extractor": "7.58.12",
|
|
121
146
|
"@types/node": "22.15.32",
|
|
147
|
+
"@types/react": "19.2.14",
|
|
148
|
+
"@types/react-dom": "19.2.3",
|
|
122
149
|
"@vitest/coverage-v8": "4.0.18",
|
|
123
150
|
"etcd3": "1.1.2",
|
|
124
151
|
"fflate": "0.8.3",
|
|
152
|
+
"jsdom": "28.1.0",
|
|
153
|
+
"react": "19.2.4",
|
|
154
|
+
"react-dom": "19.2.4",
|
|
125
155
|
"selfsigned": "5.5.0",
|
|
126
156
|
"tsup": "8.5.1",
|
|
127
157
|
"tsx": "4.21.0",
|
|
@@ -132,19 +162,27 @@
|
|
|
132
162
|
"@delali/narsil-embeddings-transformers": "0.1.6"
|
|
133
163
|
},
|
|
134
164
|
"peerDependencies": {
|
|
165
|
+
"@grpc/grpc-js": "1.14.4",
|
|
135
166
|
"etcd3": "1.1.2",
|
|
167
|
+
"react": ">=19.2.0",
|
|
136
168
|
"uWebSockets.js": ">=20.0.0"
|
|
137
169
|
},
|
|
138
170
|
"peerDependenciesMeta": {
|
|
171
|
+
"@grpc/grpc-js": {
|
|
172
|
+
"optional": true
|
|
173
|
+
},
|
|
139
174
|
"etcd3": {
|
|
140
175
|
"optional": true
|
|
141
176
|
},
|
|
177
|
+
"react": {
|
|
178
|
+
"optional": true
|
|
179
|
+
},
|
|
142
180
|
"uWebSockets.js": {
|
|
143
181
|
"optional": true
|
|
144
182
|
}
|
|
145
183
|
},
|
|
146
184
|
"scripts": {
|
|
147
|
-
"build": "tsup && node scripts/check-browser-bundle.mjs && tsx scripts/api-surface.ts",
|
|
185
|
+
"build": "rm -rf dist && tsup && node scripts/check-browser-bundle.mjs && tsx scripts/api-surface.ts",
|
|
148
186
|
"test": "vitest run --exclude='**/vector/recall.test.ts'",
|
|
149
187
|
"test:etcd": "vitest run src/__tests__/distribution/coordinator/etcd-integration/*.test.ts",
|
|
150
188
|
"test:watch": "vitest",
|
|
@@ -159,6 +197,8 @@
|
|
|
159
197
|
"api:write": "tsx scripts/api-surface.ts --write",
|
|
160
198
|
"lookalikes": "tsx scripts/lookalike-counts.ts",
|
|
161
199
|
"lookalikes:collect": "tsx scripts/lookalike-counts.ts --collect",
|
|
200
|
+
"fold-table": "tsx scripts/fold-table.ts",
|
|
201
|
+
"fold-table:write": "tsx scripts/fold-table.ts --write",
|
|
162
202
|
"lint": "biome check src/",
|
|
163
203
|
"typecheck:src": "tsc --noEmit",
|
|
164
204
|
"typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
|
package/dist/chunk-FKK76OR7.mjs
DELETED
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
import { NarsilError, ErrorCodes } from './chunk-UNJZ7ZDB.mjs';
|
|
2
|
-
import { decode, encode } from '@msgpack/msgpack';
|
|
3
|
-
|
|
4
|
-
// src/distribution/coordinator/etcd/types.ts
|
|
5
|
-
var DEFAULT_ETCD_CONFIG = {
|
|
6
|
-
endpoints: ["http://localhost:2379"],
|
|
7
|
-
keyPrefix: "_narsil",
|
|
8
|
-
nodeHeartbeatTtlSeconds: 30,
|
|
9
|
-
leaseTtlSeconds: 15
|
|
10
|
-
};
|
|
11
|
-
var ETCD_KEY_NODES = "nodes";
|
|
12
|
-
var ETCD_KEY_ALLOCATION = "allocation";
|
|
13
|
-
var ETCD_KEY_PARTITION = "partition";
|
|
14
|
-
var ETCD_KEY_SCHEMA = "schema";
|
|
15
|
-
function buildKey(prefix, ...segments) {
|
|
16
|
-
return [prefix, ...segments].join("/");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// src/distribution/coordinator/etcd/keys.ts
|
|
20
|
-
var ETCD_KEY_LEASE = "lease";
|
|
21
|
-
var ETCD_KEY_GENERIC = "kv";
|
|
22
|
-
function coordinatorKeys(config) {
|
|
23
|
-
const prefix = config.keyPrefix;
|
|
24
|
-
return {
|
|
25
|
-
node: (nodeId) => buildKey(prefix, ETCD_KEY_NODES, nodeId),
|
|
26
|
-
nodePrefix: () => buildKey(prefix, ETCD_KEY_NODES),
|
|
27
|
-
allocation: (indexName) => buildKey(prefix, ETCD_KEY_ALLOCATION, indexName),
|
|
28
|
-
allocationPrefix: () => buildKey(prefix, ETCD_KEY_ALLOCATION),
|
|
29
|
-
partition: (indexName, partitionId) => buildKey(prefix, ETCD_KEY_PARTITION, indexName, String(partitionId)),
|
|
30
|
-
schema: (indexName) => buildKey(prefix, ETCD_KEY_SCHEMA, indexName),
|
|
31
|
-
schemaPrefix: () => buildKey(prefix, ETCD_KEY_SCHEMA),
|
|
32
|
-
lease: (key) => buildKey(prefix, ETCD_KEY_LEASE, key),
|
|
33
|
-
generic: (key) => buildKey(prefix, ETCD_KEY_GENERIC, key)
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function extractSuffix(fullKey, prefix) {
|
|
37
|
-
return fullKey.slice(prefix.length + 1);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// src/distribution/coordinator/etcd/leases.ts
|
|
41
|
-
var LeaseManager = class {
|
|
42
|
-
leases = /* @__PURE__ */ new Map();
|
|
43
|
-
track(key, lease, nodeId) {
|
|
44
|
-
this.leases.set(key, { lease, nodeId, key });
|
|
45
|
-
}
|
|
46
|
-
get(key) {
|
|
47
|
-
return this.leases.get(key);
|
|
48
|
-
}
|
|
49
|
-
getByNodeId(key, nodeId) {
|
|
50
|
-
const entry = this.leases.get(key);
|
|
51
|
-
if (entry === void 0 || entry.nodeId !== nodeId) {
|
|
52
|
-
return void 0;
|
|
53
|
-
}
|
|
54
|
-
return entry;
|
|
55
|
-
}
|
|
56
|
-
remove(key) {
|
|
57
|
-
const entry = this.leases.get(key);
|
|
58
|
-
if (entry !== void 0) {
|
|
59
|
-
this.leases.delete(key);
|
|
60
|
-
}
|
|
61
|
-
return entry;
|
|
62
|
-
}
|
|
63
|
-
async revokeAll() {
|
|
64
|
-
const revokePromises = [];
|
|
65
|
-
for (const [, entry] of this.leases) {
|
|
66
|
-
revokePromises.push(
|
|
67
|
-
entry.lease.revoke().catch(() => {
|
|
68
|
-
})
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
this.leases.clear();
|
|
72
|
-
await Promise.all(revokePromises);
|
|
73
|
-
}
|
|
74
|
-
entries() {
|
|
75
|
-
return this.leases.entries();
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// src/distribution/coordinator/etcd/loader.ts
|
|
80
|
-
async function loadEtcd3Module() {
|
|
81
|
-
try {
|
|
82
|
-
return await import('etcd3');
|
|
83
|
-
} catch (err) {
|
|
84
|
-
throw new NarsilError(
|
|
85
|
-
ErrorCodes.COORDINATOR_DEPENDENCY_MISSING,
|
|
86
|
-
"The `etcd3` package is not installed. Install it with `pnpm add etcd3` (or the npm or yarn equivalent) to use createEtcdCoordinator.",
|
|
87
|
-
{ cause: err instanceof Error ? err.message : String(err) }
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function serializeAllocationTable(table) {
|
|
92
|
-
const serializable = {
|
|
93
|
-
indexName: table.indexName,
|
|
94
|
-
version: table.version,
|
|
95
|
-
replicationFactor: table.replicationFactor,
|
|
96
|
-
assignments: Array.from(table.assignments.entries())
|
|
97
|
-
};
|
|
98
|
-
return new Uint8Array(encode(serializable));
|
|
99
|
-
}
|
|
100
|
-
function deserializeAllocationTable(data) {
|
|
101
|
-
const raw = decode(data);
|
|
102
|
-
if (typeof raw.indexName !== "string") {
|
|
103
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "AllocationTable missing or invalid indexName field");
|
|
104
|
-
}
|
|
105
|
-
if (typeof raw.version !== "number") {
|
|
106
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "AllocationTable missing or invalid version field");
|
|
107
|
-
}
|
|
108
|
-
if (!Array.isArray(raw.assignments)) {
|
|
109
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "AllocationTable missing or invalid assignments field");
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
indexName: raw.indexName,
|
|
113
|
-
version: raw.version,
|
|
114
|
-
replicationFactor: typeof raw.replicationFactor === "number" ? raw.replicationFactor : 0,
|
|
115
|
-
assignments: new Map(raw.assignments)
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function serializeNodeRegistration(reg) {
|
|
119
|
-
return new Uint8Array(encode(reg));
|
|
120
|
-
}
|
|
121
|
-
function deserializeNodeRegistration(data) {
|
|
122
|
-
const raw = decode(data);
|
|
123
|
-
if (typeof raw.nodeId !== "string") {
|
|
124
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "NodeRegistration missing or invalid nodeId field");
|
|
125
|
-
}
|
|
126
|
-
if (typeof raw.address !== "string") {
|
|
127
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "NodeRegistration missing or invalid address field");
|
|
128
|
-
}
|
|
129
|
-
return raw;
|
|
130
|
-
}
|
|
131
|
-
function serializeSchema(schema) {
|
|
132
|
-
return Buffer.from(new Uint8Array(encode(schema)));
|
|
133
|
-
}
|
|
134
|
-
function deserializeSchema(data) {
|
|
135
|
-
return decode(data);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// src/distribution/coordinator/etcd/validation.ts
|
|
139
|
-
var MAX_NODE_ID_LENGTH = 255;
|
|
140
|
-
var VALID_PARTITION_STATES = /* @__PURE__ */ new Set(["UNASSIGNED", "INITIALISING", "ACTIVE", "MIGRATING", "DECOMMISSIONING"]);
|
|
141
|
-
var MAX_WATCHERS = 64;
|
|
142
|
-
function validateNodeId(nodeId) {
|
|
143
|
-
if (nodeId.length === 0) {
|
|
144
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "Node ID must not be empty");
|
|
145
|
-
}
|
|
146
|
-
if (nodeId.length > MAX_NODE_ID_LENGTH) {
|
|
147
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, `Node ID exceeds the ${MAX_NODE_ID_LENGTH} character limit`, {
|
|
148
|
-
nodeId,
|
|
149
|
-
length: nodeId.length
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
if (nodeId.includes("/") || nodeId.includes("\\") || nodeId.includes("..") || nodeId.includes("\0")) {
|
|
153
|
-
throw new NarsilError(
|
|
154
|
-
ErrorCodes.CONFIG_INVALID,
|
|
155
|
-
"Node ID contains forbidden characters (/, \\, .., or null bytes)",
|
|
156
|
-
{
|
|
157
|
-
nodeId
|
|
158
|
-
}
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
function validatePartitionState(value) {
|
|
163
|
-
if (!VALID_PARTITION_STATES.has(value)) {
|
|
164
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, `Unknown partition state '${value}'`, {
|
|
165
|
-
state: value,
|
|
166
|
-
validStates: Array.from(VALID_PARTITION_STATES)
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
return value;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// src/distribution/coordinator/etcd/index.ts
|
|
173
|
-
async function createEtcdCoordinator(config) {
|
|
174
|
-
const resolvedConfig = {
|
|
175
|
-
...DEFAULT_ETCD_CONFIG,
|
|
176
|
-
...config
|
|
177
|
-
};
|
|
178
|
-
const etcd3Module = await loadEtcd3Module();
|
|
179
|
-
const client = new etcd3Module.Etcd3({ hosts: resolvedConfig.endpoints });
|
|
180
|
-
const leaseManager = new LeaseManager();
|
|
181
|
-
const watchers = /* @__PURE__ */ new Set();
|
|
182
|
-
let isShutdown = false;
|
|
183
|
-
function assertNotShutdown() {
|
|
184
|
-
if (isShutdown) {
|
|
185
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, "Coordinator has been shut down");
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const keys = coordinatorKeys(resolvedConfig);
|
|
189
|
-
function addWatcher(watcher) {
|
|
190
|
-
if (watchers.size >= MAX_WATCHERS) {
|
|
191
|
-
watcher.cancel().catch(() => {
|
|
192
|
-
});
|
|
193
|
-
throw new NarsilError(ErrorCodes.CONFIG_INVALID, `Maximum watcher limit (${MAX_WATCHERS}) reached`, {
|
|
194
|
-
currentCount: watchers.size
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
watchers.add(watcher);
|
|
198
|
-
}
|
|
199
|
-
function removeWatcher(watcher) {
|
|
200
|
-
watcher.cancel().catch(() => {
|
|
201
|
-
});
|
|
202
|
-
watchers.delete(watcher);
|
|
203
|
-
}
|
|
204
|
-
const coordinator = {
|
|
205
|
-
async registerNode(registration) {
|
|
206
|
-
assertNotShutdown();
|
|
207
|
-
validateNodeId(registration.nodeId);
|
|
208
|
-
const ttl = resolvedConfig.nodeHeartbeatTtlSeconds;
|
|
209
|
-
const lease = client.lease(ttl);
|
|
210
|
-
await lease.grant();
|
|
211
|
-
const key = keys.node(registration.nodeId);
|
|
212
|
-
const data = Buffer.from(serializeNodeRegistration(registration));
|
|
213
|
-
await lease.put(key).value(data).exec();
|
|
214
|
-
const existing = leaseManager.get(key);
|
|
215
|
-
if (existing !== void 0) {
|
|
216
|
-
await existing.lease.revoke().catch(() => {
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
leaseManager.track(key, lease, registration.nodeId);
|
|
220
|
-
},
|
|
221
|
-
async deregisterNode(nodeId) {
|
|
222
|
-
assertNotShutdown();
|
|
223
|
-
validateNodeId(nodeId);
|
|
224
|
-
const key = keys.node(nodeId);
|
|
225
|
-
const entry = leaseManager.remove(key);
|
|
226
|
-
if (entry !== void 0) {
|
|
227
|
-
await entry.lease.revoke().catch(() => {
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
await client.delete().key(key).exec();
|
|
231
|
-
},
|
|
232
|
-
async listNodes() {
|
|
233
|
-
assertNotShutdown();
|
|
234
|
-
const prefix = keys.nodePrefix();
|
|
235
|
-
const result = await client.getAll().prefix(prefix).buffers();
|
|
236
|
-
const nodes = [];
|
|
237
|
-
for (const key of Object.keys(result)) {
|
|
238
|
-
const buf = result[key];
|
|
239
|
-
if (buf !== void 0) {
|
|
240
|
-
nodes.push(deserializeNodeRegistration(buf));
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return nodes;
|
|
244
|
-
},
|
|
245
|
-
async watchNodes(handler) {
|
|
246
|
-
assertNotShutdown();
|
|
247
|
-
const prefix = keys.nodePrefix();
|
|
248
|
-
const watcher = await client.watch().prefix(prefix).create();
|
|
249
|
-
addWatcher(watcher);
|
|
250
|
-
watcher.on("put", (kv) => {
|
|
251
|
-
try {
|
|
252
|
-
const registration = deserializeNodeRegistration(kv.value);
|
|
253
|
-
handler({ type: "node_joined", nodeId: registration.nodeId, registration });
|
|
254
|
-
} catch (_) {
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
watcher.on("delete", (kv) => {
|
|
258
|
-
const fullKey = kv.key.toString();
|
|
259
|
-
const nodeId = extractSuffix(fullKey, prefix);
|
|
260
|
-
handler({ type: "node_left", nodeId, registration: null });
|
|
261
|
-
});
|
|
262
|
-
return () => removeWatcher(watcher);
|
|
263
|
-
},
|
|
264
|
-
async getAllocation(indexName) {
|
|
265
|
-
assertNotShutdown();
|
|
266
|
-
const key = keys.allocation(indexName);
|
|
267
|
-
const buf = await client.get(key).buffer();
|
|
268
|
-
if (buf === null) {
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
271
|
-
return deserializeAllocationTable(buf);
|
|
272
|
-
},
|
|
273
|
-
async putAllocation(indexName, table, expectedVersion) {
|
|
274
|
-
assertNotShutdown();
|
|
275
|
-
const key = keys.allocation(indexName);
|
|
276
|
-
const data = Buffer.from(serializeAllocationTable(table));
|
|
277
|
-
if (expectedVersion === void 0) {
|
|
278
|
-
await client.put(key).value(data).exec();
|
|
279
|
-
return true;
|
|
280
|
-
}
|
|
281
|
-
if (expectedVersion === null) {
|
|
282
|
-
const txn2 = await client.if(key, "Create", "==", 0).then(client.put(key).value(data)).commit();
|
|
283
|
-
return txn2.succeeded;
|
|
284
|
-
}
|
|
285
|
-
const response = await client.get(key).exec();
|
|
286
|
-
if (response.kvs.length === 0) {
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
const kv = response.kvs[0];
|
|
290
|
-
const currentTable = deserializeAllocationTable(Buffer.from(kv.value));
|
|
291
|
-
if (currentTable.version !== expectedVersion) {
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
const txn = await client.if(key, "Mod", "==", kv.mod_revision).then(client.put(key).value(data)).commit();
|
|
295
|
-
return txn.succeeded;
|
|
296
|
-
},
|
|
297
|
-
async watchAllocation(handler) {
|
|
298
|
-
assertNotShutdown();
|
|
299
|
-
const prefix = keys.allocationPrefix();
|
|
300
|
-
const watcher = await client.watch().prefix(prefix).create();
|
|
301
|
-
addWatcher(watcher);
|
|
302
|
-
watcher.on("put", (kv) => {
|
|
303
|
-
try {
|
|
304
|
-
const table = deserializeAllocationTable(kv.value);
|
|
305
|
-
handler({ indexName: table.indexName, table });
|
|
306
|
-
} catch (_) {
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
return () => removeWatcher(watcher);
|
|
310
|
-
},
|
|
311
|
-
async getPartitionState(indexName, partitionId) {
|
|
312
|
-
assertNotShutdown();
|
|
313
|
-
const key = keys.partition(indexName, partitionId);
|
|
314
|
-
const value = await client.get(key).string();
|
|
315
|
-
if (value === null) {
|
|
316
|
-
return "UNASSIGNED";
|
|
317
|
-
}
|
|
318
|
-
return validatePartitionState(value);
|
|
319
|
-
},
|
|
320
|
-
async putPartitionState(indexName, partitionId, state) {
|
|
321
|
-
assertNotShutdown();
|
|
322
|
-
const key = keys.partition(indexName, partitionId);
|
|
323
|
-
await client.put(key).value(state).exec();
|
|
324
|
-
},
|
|
325
|
-
async acquireLease(key, nodeId, ttlMs) {
|
|
326
|
-
assertNotShutdown();
|
|
327
|
-
const etcdKey = keys.lease(key);
|
|
328
|
-
const ttlSeconds = Math.max(1, Math.ceil(ttlMs / 1e3));
|
|
329
|
-
const existing = leaseManager.get(etcdKey);
|
|
330
|
-
if (existing !== void 0 && existing.nodeId === nodeId) {
|
|
331
|
-
try {
|
|
332
|
-
await existing.lease.keepaliveOnce();
|
|
333
|
-
return true;
|
|
334
|
-
} catch (_) {
|
|
335
|
-
leaseManager.remove(etcdKey);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const lease = client.lease(ttlSeconds);
|
|
339
|
-
await lease.grant();
|
|
340
|
-
const txn = await client.if(etcdKey, "Create", "==", 0).then(lease.put(etcdKey).value(nodeId)).commit();
|
|
341
|
-
if (!txn.succeeded) {
|
|
342
|
-
await lease.revoke().catch(() => {
|
|
343
|
-
});
|
|
344
|
-
const currentValue = await client.get(etcdKey).string();
|
|
345
|
-
return currentValue === nodeId;
|
|
346
|
-
}
|
|
347
|
-
leaseManager.track(etcdKey, lease, nodeId);
|
|
348
|
-
return true;
|
|
349
|
-
},
|
|
350
|
-
async renewLease(key, nodeId, _ttlMs) {
|
|
351
|
-
assertNotShutdown();
|
|
352
|
-
const etcdKey = keys.lease(key);
|
|
353
|
-
const entry = leaseManager.getByNodeId(etcdKey, nodeId);
|
|
354
|
-
if (entry === void 0) {
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
try {
|
|
358
|
-
await entry.lease.keepaliveOnce();
|
|
359
|
-
return true;
|
|
360
|
-
} catch (_) {
|
|
361
|
-
leaseManager.remove(etcdKey);
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
},
|
|
365
|
-
async releaseLease(key) {
|
|
366
|
-
assertNotShutdown();
|
|
367
|
-
const etcdKey = keys.lease(key);
|
|
368
|
-
const entry = leaseManager.remove(etcdKey);
|
|
369
|
-
if (entry !== void 0) {
|
|
370
|
-
await entry.lease.revoke().catch(() => {
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
await client.delete().key(etcdKey).exec();
|
|
374
|
-
},
|
|
375
|
-
async get(key) {
|
|
376
|
-
assertNotShutdown();
|
|
377
|
-
const etcdKey = keys.generic(key);
|
|
378
|
-
const buf = await client.get(etcdKey).buffer();
|
|
379
|
-
if (buf === null) {
|
|
380
|
-
return null;
|
|
381
|
-
}
|
|
382
|
-
return new Uint8Array(buf);
|
|
383
|
-
},
|
|
384
|
-
async compareAndSet(key, expected, value) {
|
|
385
|
-
assertNotShutdown();
|
|
386
|
-
const etcdKey = keys.generic(key);
|
|
387
|
-
const newValue = Buffer.from(value);
|
|
388
|
-
if (expected === null) {
|
|
389
|
-
const txn2 = await client.if(etcdKey, "Create", "==", 0).then(client.put(etcdKey).value(newValue)).commit();
|
|
390
|
-
return txn2.succeeded;
|
|
391
|
-
}
|
|
392
|
-
const expectedBuf = Buffer.from(expected);
|
|
393
|
-
const txn = await client.if(etcdKey, "Value", "==", expectedBuf).then(client.put(etcdKey).value(newValue)).commit();
|
|
394
|
-
return txn.succeeded;
|
|
395
|
-
},
|
|
396
|
-
async getSchema(indexName) {
|
|
397
|
-
assertNotShutdown();
|
|
398
|
-
const key = keys.schema(indexName);
|
|
399
|
-
const buf = await client.get(key).buffer();
|
|
400
|
-
if (buf === null) {
|
|
401
|
-
return null;
|
|
402
|
-
}
|
|
403
|
-
return deserializeSchema(buf);
|
|
404
|
-
},
|
|
405
|
-
async putSchema(indexName, schema) {
|
|
406
|
-
assertNotShutdown();
|
|
407
|
-
const key = keys.schema(indexName);
|
|
408
|
-
await client.put(key).value(serializeSchema(schema)).exec();
|
|
409
|
-
},
|
|
410
|
-
async watchSchemas(handler) {
|
|
411
|
-
assertNotShutdown();
|
|
412
|
-
const prefix = keys.schemaPrefix();
|
|
413
|
-
const watcher = await client.watch().prefix(prefix).create();
|
|
414
|
-
addWatcher(watcher);
|
|
415
|
-
watcher.on("put", (kv) => {
|
|
416
|
-
try {
|
|
417
|
-
const schema = deserializeSchema(kv.value);
|
|
418
|
-
const fullKey = kv.key.toString();
|
|
419
|
-
const indexName = extractSuffix(fullKey, prefix);
|
|
420
|
-
handler({ type: "schema_created", indexName, schema });
|
|
421
|
-
} catch (_) {
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
watcher.on("delete", (kv) => {
|
|
425
|
-
const fullKey = kv.key.toString();
|
|
426
|
-
const indexName = extractSuffix(fullKey, prefix);
|
|
427
|
-
handler({ type: "schema_dropped", indexName, schema: null });
|
|
428
|
-
});
|
|
429
|
-
return () => removeWatcher(watcher);
|
|
430
|
-
},
|
|
431
|
-
async getLeaseHolder(key) {
|
|
432
|
-
assertNotShutdown();
|
|
433
|
-
const etcdKey = keys.lease(key);
|
|
434
|
-
const value = await client.get(etcdKey).string();
|
|
435
|
-
return value ?? null;
|
|
436
|
-
},
|
|
437
|
-
async shutdown() {
|
|
438
|
-
if (isShutdown) {
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
isShutdown = true;
|
|
442
|
-
const cancelPromises = [];
|
|
443
|
-
for (const watcher of watchers) {
|
|
444
|
-
cancelPromises.push(watcher.cancel().catch(() => {
|
|
445
|
-
}));
|
|
446
|
-
}
|
|
447
|
-
await Promise.all(cancelPromises);
|
|
448
|
-
watchers.clear();
|
|
449
|
-
await leaseManager.revokeAll();
|
|
450
|
-
client.close();
|
|
451
|
-
}
|
|
452
|
-
};
|
|
453
|
-
return coordinator;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export { createEtcdCoordinator, validateNodeId };
|