@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,85 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import { createObjectStorage } from "./object-storage.js";
|
|
3
|
+
import { DEFAULT_DATA_PREFIX } from "./config.js";
|
|
4
|
+
import { finalizeShardMap, isTopologyResharding, normalizeShardMap, publishShardMap } from "./shard-map.js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_KEY = "cluster/shard-map.json";
|
|
7
|
+
|
|
8
|
+
export class ShardMapStore {
|
|
9
|
+
constructor({ storage, prefix = DEFAULT_DATA_PREFIX, key = DEFAULT_KEY, durable = false } = {}) {
|
|
10
|
+
if (!storage) throw new Error("storage is required");
|
|
11
|
+
this.storage = storage;
|
|
12
|
+
this.prefix = normalizePrefix(prefix);
|
|
13
|
+
this.key = key.replace(/^\/+/, "");
|
|
14
|
+
this.durable = durable === true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async load() {
|
|
18
|
+
const raw = await this.storage.get(this._key());
|
|
19
|
+
if (!raw) return null;
|
|
20
|
+
const payload = JSON.parse(raw);
|
|
21
|
+
const stored = payload.shardMap || payload;
|
|
22
|
+
const shardMap = normalizeShardMap(stored);
|
|
23
|
+
if (stored.version !== shardMap.version || !stored.topology) await this._write(shardMap);
|
|
24
|
+
return shardMap;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async save(shardMap) {
|
|
28
|
+
const current = await this.load();
|
|
29
|
+
return this._write(publishShardMap(current, shardMap));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async finalize(expectedTopologyId) {
|
|
33
|
+
const current = await this.load();
|
|
34
|
+
if (!current) throw new Error("cannot finalize a missing shard map");
|
|
35
|
+
return this._write(finalizeShardMap(current, expectedTopologyId));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async waitForActive(expectedTopologyId, { pollMs = 1000 } = {}) {
|
|
39
|
+
for (;;) {
|
|
40
|
+
const shardMap = await this.load();
|
|
41
|
+
if (!shardMap) throw new Error("shard map disappeared while waiting for reshard finalization");
|
|
42
|
+
if (shardMap.topology.id !== expectedTopologyId) {
|
|
43
|
+
throw new Error(`topology changed while waiting for ${expectedTopologyId}`);
|
|
44
|
+
}
|
|
45
|
+
if (!isTopologyResharding(shardMap)) return shardMap;
|
|
46
|
+
await sleep(Math.max(50, Number(pollMs) || 1000));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async _write(shardMap) {
|
|
51
|
+
const payload = {
|
|
52
|
+
version: 2,
|
|
53
|
+
updatedAt: new Date().toISOString(),
|
|
54
|
+
shardMap,
|
|
55
|
+
};
|
|
56
|
+
await this.storage.put(this._key(), JSON.stringify(payload), "application/json");
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_key() {
|
|
61
|
+
return `${this.prefix}/${this.key}`.replace(/^\/+/, "");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createShardMapStore(config = {}, persistenceConfig = {}) {
|
|
66
|
+
const mode = config.mode || "off";
|
|
67
|
+
if (mode === "off" || mode === false) return null;
|
|
68
|
+
const storage = createObjectStorage({
|
|
69
|
+
mode,
|
|
70
|
+
s3: {
|
|
71
|
+
...(persistenceConfig.s3 || {}),
|
|
72
|
+
...(config.s3 || {}),
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return new ShardMapStore({
|
|
76
|
+
storage,
|
|
77
|
+
prefix: config.prefix || persistenceConfig.prefix || DEFAULT_DATA_PREFIX,
|
|
78
|
+
key: config.key || DEFAULT_KEY,
|
|
79
|
+
durable: mode === "s3",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizePrefix(prefix) {
|
|
84
|
+
return String(prefix || "").replace(/^\/+|\/+$/g, "");
|
|
85
|
+
}
|
package/src/shard-map.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { randomUUID } from "./runtime.js";
|
|
2
|
+
|
|
3
|
+
export const GLOBAL_DATABASE = "*";
|
|
4
|
+
export const GLOBAL_COLLECTION = "*";
|
|
5
|
+
export const DEFAULT_SHARD = "default";
|
|
6
|
+
export const SHARD_MAP_VERSION = 2;
|
|
7
|
+
export const SHARD_PARTITIONER = "fnv1a32-mod-v1";
|
|
8
|
+
export const TOPOLOGY_ACTIVE = "active";
|
|
9
|
+
export const TOPOLOGY_RESHARDING = "resharding";
|
|
10
|
+
|
|
11
|
+
export class ShardMap {
|
|
12
|
+
constructor(map) {
|
|
13
|
+
this.shards = new Map();
|
|
14
|
+
this.version = SHARD_MAP_VERSION;
|
|
15
|
+
this.topology = null;
|
|
16
|
+
if (map) this.replace(map);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
replace(map) {
|
|
20
|
+
const normalized = normalizeShardMap(map);
|
|
21
|
+
this.shards.clear();
|
|
22
|
+
this.version = normalized.version;
|
|
23
|
+
this.topology = normalized.topology;
|
|
24
|
+
for (const [shard, shardConfig] of Object.entries(normalized.shards)) {
|
|
25
|
+
this._ensureShard(shard);
|
|
26
|
+
for (const peer of shardConfig.peers || []) this.addPeer(shard, peer);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
addPeer(shard, peer) {
|
|
31
|
+
const shardEntry = this._ensureShard(shard);
|
|
32
|
+
const key = peerKey(peer);
|
|
33
|
+
const existing = shardEntry.peers.get(key);
|
|
34
|
+
if (existing) return existing;
|
|
35
|
+
shardEntry.peers.set(key, peer);
|
|
36
|
+
return peer;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
removePeer(shard, peer) {
|
|
40
|
+
const shardEntry = this._getShard(shard);
|
|
41
|
+
if (!shardEntry) return false;
|
|
42
|
+
return shardEntry.peers.delete(peerKey(peer));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
peersFor(shard = DEFAULT_SHARD) {
|
|
46
|
+
return uniquePeers([
|
|
47
|
+
...this._peersFor(DEFAULT_SHARD),
|
|
48
|
+
...this._peersFor(shard),
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
shardForObject(database, collection, objectId) {
|
|
53
|
+
const shards = this.shardsForCollection(database, collection);
|
|
54
|
+
if (!shards.length) return DEFAULT_SHARD;
|
|
55
|
+
return shards[hashString(String(objectId)) % shards.length];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
shardsForCollection(database, collection) {
|
|
59
|
+
return [...this.shards.keys()].sort();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
syncTargets() {
|
|
63
|
+
return [...this.shards.entries()].map(([shardName, shard]) => ({
|
|
64
|
+
database: GLOBAL_DATABASE,
|
|
65
|
+
collection: GLOBAL_COLLECTION,
|
|
66
|
+
shard: shardName,
|
|
67
|
+
peers: [...shard.peers.values()],
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
allPeers() {
|
|
72
|
+
const peers = [];
|
|
73
|
+
for (const shard of this.shards.values()) peers.push(...shard.peers.values());
|
|
74
|
+
return uniquePeers(peers);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
snapshot() {
|
|
78
|
+
const shards = {};
|
|
79
|
+
for (const [shardName, shard] of this.shards) {
|
|
80
|
+
shards[shardName] = {
|
|
81
|
+
peers: [...shard.peers.values()].map(describePeer),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
version: this.version,
|
|
86
|
+
topology: cloneTopology(this.topology),
|
|
87
|
+
shards,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_ensureShard(shard) {
|
|
92
|
+
let shardEntry = this.shards.get(shard);
|
|
93
|
+
if (!shardEntry) {
|
|
94
|
+
shardEntry = { peers: new Map() };
|
|
95
|
+
this.shards.set(shard, shardEntry);
|
|
96
|
+
}
|
|
97
|
+
return shardEntry;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_getShard(shard) {
|
|
101
|
+
return this.shards.get(shard);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_peersFor(shard) {
|
|
105
|
+
return [...(this._getShard(shard)?.peers.values() || [])];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function normalizeShardMap(map) {
|
|
110
|
+
if (!map?.shards || typeof map.shards !== "object" || Array.isArray(map.shards)) {
|
|
111
|
+
throw new Error("shard map must define top-level shards");
|
|
112
|
+
}
|
|
113
|
+
if (map.version !== undefined && ![1, SHARD_MAP_VERSION].includes(Number(map.version))) {
|
|
114
|
+
throw new Error(`unsupported shard map version: ${map.version}`);
|
|
115
|
+
}
|
|
116
|
+
const shards = cloneShards(map.shards);
|
|
117
|
+
const shardNames = Object.keys(shards).sort();
|
|
118
|
+
const topology = normalizeTopology(map.topology, shardNames);
|
|
119
|
+
validateTopology(topology, shardNames);
|
|
120
|
+
return {
|
|
121
|
+
version: SHARD_MAP_VERSION,
|
|
122
|
+
topology,
|
|
123
|
+
shards,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function publishShardMap(currentMap, desiredMap) {
|
|
128
|
+
if (!currentMap || !Object.keys(currentMap.shards || {}).length) return normalizeShardMap(desiredMap);
|
|
129
|
+
const current = normalizeShardMap(currentMap);
|
|
130
|
+
const desired = normalizeDesiredShardMap(desiredMap, current.topology.partitioner);
|
|
131
|
+
const currentSignature = partitionSignature(current);
|
|
132
|
+
const desiredSignature = partitionSignature(desired);
|
|
133
|
+
|
|
134
|
+
if (current.topology.state === TOPOLOGY_RESHARDING) {
|
|
135
|
+
if (currentSignature !== desiredSignature) {
|
|
136
|
+
throw new Error("cannot start another topology change while resharding is unfinished");
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...current,
|
|
140
|
+
shards: assignPeerLayout(Object.keys(current.shards), desired.shards),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (currentSignature === desiredSignature) {
|
|
145
|
+
return {
|
|
146
|
+
...current,
|
|
147
|
+
shards: assignPeerLayout(Object.keys(current.shards), desired.shards),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const topologyId = transitionTopologyId(current.topology.id, desiredSignature);
|
|
152
|
+
const targetShardNames = Object.keys(desired.shards)
|
|
153
|
+
.sort()
|
|
154
|
+
.map((_, index) => topologyShardName(topologyId, index));
|
|
155
|
+
return {
|
|
156
|
+
version: SHARD_MAP_VERSION,
|
|
157
|
+
topology: {
|
|
158
|
+
id: topologyId,
|
|
159
|
+
state: TOPOLOGY_RESHARDING,
|
|
160
|
+
partitioner: desired.topology.partitioner,
|
|
161
|
+
source: {
|
|
162
|
+
id: current.topology.id,
|
|
163
|
+
shards: Object.keys(current.shards).sort(),
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
shards: assignPeerLayout(targetShardNames, desired.shards),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function finalizeShardMap(map, expectedTopologyId = null) {
|
|
171
|
+
const normalized = normalizeShardMap(map);
|
|
172
|
+
if (expectedTopologyId && normalized.topology.id !== expectedTopologyId) {
|
|
173
|
+
throw new Error(`topology changed while finalizing ${expectedTopologyId}`);
|
|
174
|
+
}
|
|
175
|
+
if (normalized.topology.state !== TOPOLOGY_RESHARDING) return normalized;
|
|
176
|
+
return {
|
|
177
|
+
...normalized,
|
|
178
|
+
topology: {
|
|
179
|
+
id: normalized.topology.id,
|
|
180
|
+
state: TOPOLOGY_ACTIVE,
|
|
181
|
+
partitioner: normalized.topology.partitioner,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function partitionSignature(map) {
|
|
187
|
+
const normalized = map?.topology && map?.shards ? map : normalizeShardMap(map);
|
|
188
|
+
return `${normalized.topology.partitioner}:${Object.keys(normalized.shards).length}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function isTopologyResharding(map) {
|
|
192
|
+
return map?.topology?.state === TOPOLOGY_RESHARDING;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function topologySourceShards(map) {
|
|
196
|
+
return isTopologyResharding(map) ? [...map.topology.source.shards] : [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function peerKey(peer) {
|
|
200
|
+
return peer.url || peer.nodeId || peer.key;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function uniquePeers(peers) {
|
|
204
|
+
const out = new Map();
|
|
205
|
+
for (const peer of peers) out.set(peerKey(peer), peer);
|
|
206
|
+
return [...out.values()];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function describePeer(peer) {
|
|
210
|
+
return {
|
|
211
|
+
...(peer.url ? { url: peer.url } : {}),
|
|
212
|
+
...(peer.nodeId ? { nodeId: peer.nodeId } : {}),
|
|
213
|
+
...(peer.key && !peer.url ? { key: peer.key } : {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeDesiredShardMap(map, fallbackPartitioner) {
|
|
218
|
+
if (!map?.shards || typeof map.shards !== "object" || Array.isArray(map.shards)) {
|
|
219
|
+
throw new Error("shard map must define top-level shards");
|
|
220
|
+
}
|
|
221
|
+
const shards = cloneShards(map.shards);
|
|
222
|
+
const partitioner = map.topology?.partitioner || fallbackPartitioner || SHARD_PARTITIONER;
|
|
223
|
+
if (partitioner !== SHARD_PARTITIONER) throw new Error(`unsupported shard partitioner: ${partitioner}`);
|
|
224
|
+
return {
|
|
225
|
+
version: SHARD_MAP_VERSION,
|
|
226
|
+
topology: {
|
|
227
|
+
id: map.topology?.id || "desired",
|
|
228
|
+
state: TOPOLOGY_ACTIVE,
|
|
229
|
+
partitioner,
|
|
230
|
+
},
|
|
231
|
+
shards,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeTopology(topology, shardNames) {
|
|
236
|
+
const partitioner = topology?.partitioner || SHARD_PARTITIONER;
|
|
237
|
+
const id = topology?.id || initialTopologyId(shardNames, partitioner);
|
|
238
|
+
const state = topology?.state || TOPOLOGY_ACTIVE;
|
|
239
|
+
return {
|
|
240
|
+
id: String(id),
|
|
241
|
+
state,
|
|
242
|
+
partitioner,
|
|
243
|
+
...(topology?.source
|
|
244
|
+
? {
|
|
245
|
+
source: {
|
|
246
|
+
id: String(topology.source.id || ""),
|
|
247
|
+
shards: [...new Set((topology.source.shards || []).map(String))].sort(),
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
: {}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function validateTopology(topology, shardNames) {
|
|
255
|
+
if (!topology.id) throw new Error("shard map topology.id is required");
|
|
256
|
+
if (topology.partitioner !== SHARD_PARTITIONER) {
|
|
257
|
+
throw new Error(`unsupported shard partitioner: ${topology.partitioner}`);
|
|
258
|
+
}
|
|
259
|
+
if (![TOPOLOGY_ACTIVE, TOPOLOGY_RESHARDING].includes(topology.state)) {
|
|
260
|
+
throw new Error(`unsupported shard topology state: ${topology.state}`);
|
|
261
|
+
}
|
|
262
|
+
if (topology.state === TOPOLOGY_ACTIVE && topology.source) {
|
|
263
|
+
throw new Error("active shard topology must not define a source");
|
|
264
|
+
}
|
|
265
|
+
if (topology.state !== TOPOLOGY_RESHARDING) return;
|
|
266
|
+
if (!topology.source?.id || !topology.source.shards.length) {
|
|
267
|
+
throw new Error("resharding topology must define its source id and shards");
|
|
268
|
+
}
|
|
269
|
+
const target = new Set(shardNames);
|
|
270
|
+
for (const shard of topology.source.shards) {
|
|
271
|
+
if (target.has(shard)) throw new Error(`source and target shard names overlap: ${shard}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function initialTopologyId(shardNames, partitioner) {
|
|
276
|
+
return `t-${shortHash(`initial|${partitioner}|${shardNames.join("|")}`)}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function transitionTopologyId(sourceId, signature) {
|
|
280
|
+
const entropy = randomUUID().replaceAll("-", "").slice(0, 12);
|
|
281
|
+
return `t-${shortHash(`${sourceId}|${signature}`).slice(0, 8)}-${entropy}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function topologyShardName(topologyId, index) {
|
|
285
|
+
return `${topologyId}-s${String(index).padStart(4, "0")}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function assignPeerLayout(targetNames, desiredShards) {
|
|
289
|
+
const desiredEntries = Object.entries(desiredShards).sort(([a], [b]) => a.localeCompare(b));
|
|
290
|
+
const exact = targetNames.length === desiredEntries.length
|
|
291
|
+
&& targetNames.every((name) => Object.hasOwn(desiredShards, name));
|
|
292
|
+
return Object.fromEntries([...targetNames].sort().map((name, index) => [
|
|
293
|
+
name,
|
|
294
|
+
cloneShard(exact ? desiredShards[name] : desiredEntries[index]?.[1]),
|
|
295
|
+
]));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function cloneShards(shards) {
|
|
299
|
+
return Object.fromEntries(Object.entries(shards).map(([name, config]) => [String(name), cloneShard(config)]));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function cloneShard(config = {}) {
|
|
303
|
+
return {
|
|
304
|
+
peers: [...(config?.peers || [])],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function cloneTopology(topology) {
|
|
309
|
+
if (!topology) return null;
|
|
310
|
+
return {
|
|
311
|
+
id: topology.id,
|
|
312
|
+
state: topology.state,
|
|
313
|
+
partitioner: topology.partitioner,
|
|
314
|
+
...(topology.source
|
|
315
|
+
? { source: { id: topology.source.id, shards: [...topology.source.shards] } }
|
|
316
|
+
: {}),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function shortHash(value) {
|
|
321
|
+
const forward = hashString(value).toString(16).padStart(8, "0");
|
|
322
|
+
const reverse = hashString([...value].reverse().join("")).toString(16).padStart(8, "0");
|
|
323
|
+
return `${forward}${reverse}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function hashString(value) {
|
|
327
|
+
let h = 0x811c9dc5;
|
|
328
|
+
for (let i = 0; i < value.length; i++) {
|
|
329
|
+
h ^= value.charCodeAt(i);
|
|
330
|
+
h = Math.imul(h, 0x01000193);
|
|
331
|
+
}
|
|
332
|
+
return h >>> 0;
|
|
333
|
+
}
|