Skip to content

Commit dceabc3

Browse files
committed
Improve ordering algorithm
1 parent 8d18926 commit dceabc3

File tree

5 files changed

+80
-99
lines changed

5 files changed

+80
-99
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
},
2828
"dependencies": {
2929
"@orbitdb/core": "^3.0.2",
30-
"helia": "^5.4.2"
30+
"helia": "^5.4.2",
31+
"it-all": "^3.0.9"
3132
},
3233
"devDependencies": {
3334
"@chainsafe/libp2p-gossipsub": "^14.1.1",

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/ordered-keyvalue.ts

Lines changed: 51 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import type { HeliaLibp2p } from "helia";
1313
import type { Libp2p } from "libp2p";
1414
import type { ServiceMap } from "@libp2p/interface";
15+
import itAll from 'it-all'
1516

1617
export type OrderedKeyValueDatabaseType = Awaited<
1718
ReturnType<ReturnType<typeof OrderedKeyValue>>
@@ -90,22 +91,41 @@ export const OrderedKeyValueApi = ({
9091
}: {
9192
database: InternalDatabase;
9293
}) => {
94+
const getScalePosition = async ({ key, position }: { key: string; position: number }): Promise<number> => {
95+
// Somewhat inefficient, I suppose, but we need to know which entries are already present.
96+
const entries = (await itAll(iterator())).sort((a, b) => a.position - b.position);
97+
98+
// Negative values mean insert from end of list.
99+
if (position < 0) position = entries.length - (position + 1)
100+
101+
// Find any previous position
102+
const previousPosition = entries.find(x=>x.key === key)?.position;
103+
104+
// If we are moving upwards, need to add 1 to adjust for the now-deleted slot where our entry used to be
105+
if (previousPosition !== undefined && position > previousPosition) position = position + 1
106+
107+
const beforePosition = entries[Math.min(position, entries.length) - 1]?.position;
108+
const afterPosition = entries[Math.max(position, 0)]?.position;
109+
110+
if (beforePosition === undefined) return afterPosition === undefined ? 0 : afterPosition - 1;
111+
return afterPosition === undefined ? beforePosition + 1 : beforePosition + (afterPosition - beforePosition) * Math.random()
112+
}
113+
93114
const put = async (
94115
key: string,
95116
value: DagCborEncodable,
96-
position?: number,
117+
position = -1,
97118
): Promise<string> => {
98-
const entryValue: { value: DagCborEncodable; position?: number } = {
119+
const entryValue: { value: DagCborEncodable; position: number } = {
99120
value,
121+
position: await getScalePosition({key, position}),
100122
};
101-
if (position !== undefined) {
102-
entryValue.position = position;
103-
}
104123
return database.addOperation({ op: "PUT", key, value: entryValue });
105124
};
106125

107-
const move = async (key: string, position: number): Promise<string> => {
108-
return database.addOperation({ op: "MOVE", key, value: position });
126+
const move = async (key: string, position: number): Promise<void> => {
127+
position = await getScalePosition({key, position});
128+
await database.addOperation({ op: "MOVE", key, value: position });
109129
};
110130

111131
const del = async (key: string): Promise<string> => {
@@ -114,11 +134,11 @@ export const OrderedKeyValueApi = ({
114134

115135
const get = async (
116136
key: string,
117-
): Promise<{ value: unknown; position?: number } | undefined> => {
137+
): Promise<DagCborEncodable | undefined> => {
118138
for await (const entry of database.log.traverse()) {
119139
const { op, key: k, value } = entry.payload;
120140
if (op === "PUT" && k === key) {
121-
return value as { value: unknown; position?: number };
141+
return (value as { value: DagCborEncodable; position: number }).value;
122142
} else if (op === "DEL" && k === key) {
123143
return undefined;
124144
}
@@ -139,58 +159,39 @@ export const OrderedKeyValueApi = ({
139159
unknown
140160
> {
141161
let count = 0;
142-
const orderedLogEntries: LogEntry<DagCborEncodable>[] = [];
143-
for await (const entry of database.log.traverse()) {
144-
orderedLogEntries.unshift(entry);
145-
}
162+
const keys: {[key: string]: true} = {};
163+
const positions: {[key: string]: number} = {};
146164

147-
let finalEntries: {
148-
key: string;
149-
value: unknown;
150-
position: number;
151-
hash: string;
152-
}[] = [];
153-
for (const entry of orderedLogEntries) {
165+
for await (const entry of database.log.traverse()) {
154166
const { op, key, value } = entry.payload;
155-
if (!key) return;
156-
157-
if (op === "PUT") {
158-
finalEntries = finalEntries.filter((e) => e.key !== key);
159167

160-
const putValue = value as { value: unknown; position?: number };
168+
if (!key || keys[key]) continue;
161169

170+
if (op === "PUT") {
162171
const hash = entry.hash;
163-
164-
const position =
165-
putValue.position !== undefined ? putValue.position : -1;
166-
finalEntries.push({
172+
const putValue = value as { value: unknown; position?: number };
173+
174+
keys[key] = true;
175+
count++;
176+
177+
yield {
167178
key,
168179
value: putValue.value,
169-
position,
180+
position: positions[key] ?? putValue.position ?? -1,
170181
hash,
171-
});
172-
count++;
173-
} else if (op === "MOVE") {
174-
const existingEntry = finalEntries.find((e) => e.key === key);
175-
if (existingEntry) {
176-
existingEntry.position = value as number;
177-
finalEntries = [
178-
...finalEntries.filter((e) => e.key !== key),
179-
existingEntry,
180-
];
181182
}
183+
184+
} else if (op === "MOVE") {
185+
if (positions[key] !== undefined || keys[key]) continue;
186+
positions[key] = value as number;
182187
} else if (op === "DEL") {
183-
finalEntries = finalEntries.filter((e) => e.key !== key);
188+
keys[key] = true;
184189
}
185190
if (amount !== undefined && count >= amount) {
186191
break;
187192
}
188193
}
189194

190-
// This is memory inefficient, but I haven't been able to think of a more elegant solution
191-
for (const entry of finalEntries) {
192-
yield entry;
193-
}
194195
};
195196

196197
const all = async () => {
@@ -203,26 +204,11 @@ export const OrderedKeyValueApi = ({
203204
for await (const entry of iterator()) {
204205
entries.push(entry);
205206
}
206-
207-
const values: {
208-
key: string;
209-
value: unknown;
210-
hash: string;
211-
}[] = [];
212-
213-
for (const entry of entries) {
214-
const position =
215-
entry.position >= 0
216-
? entry.position
217-
: entries.length + entry.position + 1;
218-
values.splice(position, 0, {
219-
key: entry.key,
220-
value: entry.value,
221-
hash: entry.hash,
222-
});
223-
}
224-
225-
return values;
207+
return entries.sort((a, b) => a.position - b.position).map(e=>({
208+
key: e.key,
209+
value: e.value,
210+
hash: e.hash,
211+
}));
226212
};
227213

228214
return {

test/ordered-keyvalue.spec.ts

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,7 @@ describe("OrderedKeyValue Database", () => {
105105

106106
const actual = await db.get(key);
107107

108-
expect(actual).to.deep.equal({ value });
109-
});
110-
111-
it("get a value with position", async () => {
112-
const value = "value1";
113-
const key = "key1";
114-
115-
await db.put(key, value, 3);
116-
117-
const actual = await db.get(key);
118-
119-
expect(actual).to.deep.equal({ value, position: 3 });
108+
expect(actual).to.deep.equal(value);
120109
});
121110

122111
it("override a key", async () => {
@@ -129,7 +118,7 @@ describe("OrderedKeyValue Database", () => {
129118

130119
const actual = await db.get(key);
131120

132-
expect(actual).to.deep.equal({ value: value2 });
121+
expect(actual).to.deep.equal(value2);
133122
});
134123

135124
it("del a value", async () => {
@@ -174,7 +163,7 @@ describe("OrderedKeyValue Database", () => {
174163
]);
175164
});
176165

177-
it("add a value - index 0", async () => {
166+
it("add a value: index 0", async () => {
178167
const value = "value1";
179168
const key = "key1";
180169

@@ -192,7 +181,7 @@ describe("OrderedKeyValue Database", () => {
192181
]);
193182
});
194183

195-
it("add a value - negative index", async () => {
184+
it("add a value: negative index", async () => {
196185
const value = "value1";
197186
const key = "key1";
198187

@@ -210,20 +199,20 @@ describe("OrderedKeyValue Database", () => {
210199
]);
211200
});
212201

213-
it("add a value - index > length", async () => {
214-
const value = "value1";
215-
const key = "key1";
202+
it("add a value: index > length", async () => {
203+
const value1 = "value1";
204+
const key1 = "key1";
216205

217206
const value2 = "value2";
218207
const key2 = "key2";
219208

220-
const hash = await db.put(key, value);
209+
const hash = await db.put(key1, value1);
221210
const hash2 = await db.put(key2, value2, 4);
222211

223212
const actual = await db.all();
224213

225214
expect(actual).to.deep.equal([
226-
{ value: value, key, hash },
215+
{ value: value1, key: key1, hash },
227216
{ value: value2, key: key2, hash: hash2 },
228217
]);
229218
});
@@ -246,7 +235,7 @@ describe("OrderedKeyValue Database", () => {
246235
]);
247236
});
248237

249-
it("move a value - index 0", async () => {
238+
it("move a value: index 0", async () => {
250239
const value = "value1";
251240
const key = "key1";
252241

@@ -264,7 +253,7 @@ describe("OrderedKeyValue Database", () => {
264253
]);
265254
});
266255

267-
it("move a value - negative index", async () => {
256+
it("move a value: negative index", async () => {
268257
const value = "value1";
269258
const key = "key1";
270259

@@ -282,7 +271,7 @@ describe("OrderedKeyValue Database", () => {
282271
]);
283272
});
284273

285-
it("move a value - index > length", async () => {
274+
it("move a value: index > length", async () => {
286275
const value = "value1";
287276
const key = "key1";
288277

@@ -309,13 +298,13 @@ describe("OrderedKeyValue Database", () => {
309298

310299
await db.put(key, value);
311300
const hash2 = await db.put(key2, value2);
312-
const hash1a = await db.put(key, value, 2);
301+
const hash1 = await db.put(key, value, 2);
313302

314303
const actual = await db.all();
315304

316305
expect(actual).to.deep.equal([
317306
{ value: value2, key: key2, hash: hash2 },
318-
{ value: value, key, hash: hash1a },
307+
{ value: value, key, hash: hash1 },
319308
]);
320309
});
321310

@@ -423,7 +412,7 @@ describe("OrderedKeyValue Database", () => {
423412

424413
const all = [];
425414
for await (const pair of db.iterator()) {
426-
all.push(pair);
415+
all.unshift(pair);
427416
}
428417

429418
expect(all).to.deep.equal(keyvalue);
@@ -474,11 +463,13 @@ describe("OrderedKeyValue Database", () => {
474463
await db.put("key6", "value6");
475464
await db.del("key6");
476465

466+
// Also check that moving values does not count towards
467+
// the returned amount.
468+
await db.move("key1", -1);
469+
477470
const all = [];
478-
let position = 0;
479471
for await (const { hash, value } of db.iterator()) {
480-
all.unshift({ hash, value, position });
481-
position++;
472+
all.unshift({ hash, value });
482473
}
483474
expect(all.length).to.equal(5);
484475
});

tsconfig.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@
1212
"moduleResolution": "node",
1313
"noFallthroughCasesInSwitch": true,
1414
"noImplicitReturns": true,
15-
"noUnusedLocals": false,
16-
"noUnusedParameters": false,
15+
"noUnusedLocals": true,
16+
"noUnusedParameters": true,
1717
"paths": {
1818
"@/*": ["src/*"]
1919
},
2020
"outDir": "./dist",
2121
"sourceMap": true,
2222
"strict": true,
23-
"strictPropertyInitialization": false,
24-
"strictNullChecks": false,
23+
"strictPropertyInitialization": true,
24+
"strictNullChecks": true,
2525
"target": "esnext",
2626
"useUnknownInCatchVariables": false,
2727

0 commit comments

Comments
 (0)