@peerbit/shared-log 9.1.2 → 9.2.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.
Files changed (66) hide show
  1. package/dist/benchmark/get-samples.js +2 -3
  2. package/dist/benchmark/get-samples.js.map +1 -1
  3. package/dist/benchmark/index.js +4 -6
  4. package/dist/benchmark/index.js.map +1 -1
  5. package/dist/benchmark/memory/child.d.ts +2 -0
  6. package/dist/benchmark/memory/child.d.ts.map +1 -0
  7. package/dist/benchmark/memory/child.js +149 -0
  8. package/dist/benchmark/memory/child.js.map +1 -0
  9. package/dist/benchmark/memory/index.d.ts +2 -0
  10. package/dist/benchmark/memory/index.d.ts.map +1 -0
  11. package/dist/benchmark/memory/index.js +81 -0
  12. package/dist/benchmark/memory/index.js.map +1 -0
  13. package/dist/benchmark/memory/utils.d.ts +13 -0
  14. package/dist/benchmark/memory/utils.d.ts.map +1 -0
  15. package/dist/benchmark/memory/utils.js +2 -0
  16. package/dist/benchmark/memory/utils.js.map +1 -0
  17. package/dist/benchmark/replication-prune.js +27 -25
  18. package/dist/benchmark/replication-prune.js.map +1 -1
  19. package/dist/benchmark/replication.js +15 -16
  20. package/dist/benchmark/replication.js.map +1 -1
  21. package/dist/src/debounce.d.ts +25 -0
  22. package/dist/src/debounce.d.ts.map +1 -0
  23. package/dist/src/debounce.js +130 -0
  24. package/dist/src/debounce.js.map +1 -0
  25. package/dist/src/index.d.ts +55 -21
  26. package/dist/src/index.d.ts.map +1 -1
  27. package/dist/src/index.js +867 -390
  28. package/dist/src/index.js.map +1 -1
  29. package/dist/src/pid.d.ts.map +1 -1
  30. package/dist/src/pid.js +23 -21
  31. package/dist/src/pid.js.map +1 -1
  32. package/dist/src/ranges.d.ts +104 -8
  33. package/dist/src/ranges.d.ts.map +1 -1
  34. package/dist/src/ranges.js +518 -76
  35. package/dist/src/ranges.js.map +1 -1
  36. package/dist/src/replication-domain-hash.d.ts.map +1 -1
  37. package/dist/src/replication-domain-hash.js.map +1 -1
  38. package/dist/src/replication-domain-time.d.ts.map +1 -1
  39. package/dist/src/replication-domain-time.js.map +1 -1
  40. package/dist/src/replication-domain.d.ts +22 -2
  41. package/dist/src/replication-domain.d.ts.map +1 -1
  42. package/dist/src/replication-domain.js +33 -0
  43. package/dist/src/replication-domain.js.map +1 -1
  44. package/dist/src/replication.d.ts +1 -55
  45. package/dist/src/replication.d.ts.map +1 -1
  46. package/dist/src/replication.js +5 -215
  47. package/dist/src/replication.js.map +1 -1
  48. package/dist/src/role.d.ts +1 -0
  49. package/dist/src/role.d.ts.map +1 -1
  50. package/dist/src/role.js +1 -0
  51. package/dist/src/role.js.map +1 -1
  52. package/dist/src/utils.d.ts +6 -0
  53. package/dist/src/utils.d.ts.map +1 -0
  54. package/dist/src/utils.js +39 -0
  55. package/dist/src/utils.js.map +1 -0
  56. package/package.json +5 -5
  57. package/src/debounce.ts +172 -0
  58. package/src/index.ts +1282 -562
  59. package/src/pid.ts +27 -25
  60. package/src/ranges.ts +794 -181
  61. package/src/replication-domain-hash.ts +3 -1
  62. package/src/replication-domain-time.ts +2 -1
  63. package/src/replication-domain.ts +68 -5
  64. package/src/replication.ts +9 -235
  65. package/src/role.ts +1 -0
  66. package/src/utils.ts +49 -0
@@ -1,6 +1,7 @@
1
1
  import { BinaryReader, BinaryWriter } from "@dao-xyz/borsh";
2
2
  import { sha256 } from "@peerbit/crypto";
3
3
  import type { ShallowOrFullEntry } from "@peerbit/log";
4
+ import type { EntryReplicated } from "./ranges.js";
4
5
  import {
5
6
  type Log,
6
7
  type ReplicationDomain,
@@ -15,7 +16,7 @@ export const hashToU32 = (hash: Uint8Array) => {
15
16
  };
16
17
 
17
18
  const hashTransformer: ReplicationDomainMapper<any> = async (
18
- entry: ShallowOrFullEntry<any>,
19
+ entry: ShallowOrFullEntry<any> | EntryReplicated,
19
20
  ) => {
20
21
  // For a fixed set or members, the choosen leaders will always be the same (address invariant)
21
22
  // This allows for that same content is always chosen to be distributed to same peers, to remove unecessary copies
@@ -29,6 +30,7 @@ const hashTransformer: ReplicationDomainMapper<any> = async (
29
30
  // convert hash of slot to a number
30
31
  return hashToU32(seed);
31
32
  };
33
+
32
34
  export type ReplicationDomainHash = ReplicationDomain<undefined, any>;
33
35
  export const createReplicationDomainHash: () => ReplicationDomainHash = () => {
34
36
  return {
@@ -1,4 +1,5 @@
1
1
  import type { ShallowOrFullEntry } from "@peerbit/log";
2
+ import type { EntryReplicated } from "./ranges.js";
2
3
  import {
3
4
  type ReplicationDomain,
4
5
  type ReplicationDomainMapper,
@@ -27,7 +28,7 @@ export const fromEntry = (
27
28
  const scalar = scalarNanoToUnit[unit];
28
29
  const originTime = +origin / scalarMilliToUnit[unit];
29
30
 
30
- const fn = (entry: ShallowOrFullEntry<any>) => {
31
+ const fn = (entry: ShallowOrFullEntry<any> | EntryReplicated) => {
31
32
  const cursor = entry.meta.clock.timestamp.wallTime / scalar;
32
33
  return Math.round(Number(cursor) - originTime);
33
34
  };
@@ -1,15 +1,14 @@
1
1
  import type { PublicSignKey } from "@peerbit/crypto";
2
2
  import { type Index } from "@peerbit/indexer-interface";
3
3
  import type { Entry, ShallowEntry } from "@peerbit/log";
4
- import type {
5
- ReplicationLimits,
6
- ReplicationRangeIndexable,
7
- } from "./replication.js";
4
+ import { debounceAcculmulator } from "./debounce.js";
5
+ import type { EntryReplicated, ReplicationRangeIndexable } from "./ranges.js";
6
+ import type { ReplicationLimits } from "./replication.js";
8
7
  import { MAX_U32 } from "./role.js";
9
8
 
10
9
  export type u32 = number;
11
10
  export type ReplicationDomainMapper<T> = (
12
- entry: Entry<T> | ShallowEntry,
11
+ entry: Entry<T> | ShallowEntry | EntryReplicated,
13
12
  ) => Promise<u32> | u32;
14
13
 
15
14
  export type Log = {
@@ -33,6 +32,64 @@ type CoverRange = {
33
32
  offset: number | PublicSignKey;
34
33
  length?: number;
35
34
  };
35
+ export type ReplicationChanges = ReplicationChange[];
36
+ export type ReplicationChange =
37
+ | {
38
+ type: "added";
39
+ range: ReplicationRangeIndexable;
40
+ }
41
+ | {
42
+ type: "removed";
43
+ range: ReplicationRangeIndexable;
44
+ }
45
+ | {
46
+ type: "updated";
47
+ range: ReplicationRangeIndexable;
48
+ prev: ReplicationRangeIndexable;
49
+ };
50
+
51
+ export const mergeReplicationChanges = (
52
+ changes: ReplicationChanges | ReplicationChanges[],
53
+ ): ReplicationChanges => {
54
+ let first = changes[0];
55
+ if (!Array.isArray(first)) {
56
+ return changes as ReplicationChanges;
57
+ }
58
+ return (changes as ReplicationChanges[]).flat();
59
+ };
60
+
61
+ export const debounceAggregationChanges = (
62
+ fn: (changeOrChanges: ReplicationChange[]) => void,
63
+ delay: number,
64
+ ) => {
65
+ return debounceAcculmulator(
66
+ (result) => {
67
+ return fn([...result.values()]);
68
+ },
69
+ () => {
70
+ let aggregated: Map<string, ReplicationChange> = new Map();
71
+ return {
72
+ add: (change: ReplicationChange) => {
73
+ const prev = aggregated.get(change.range.idString);
74
+ if (prev) {
75
+ if (prev.range.timestamp < change.range.timestamp) {
76
+ aggregated.set(change.range.idString, change);
77
+ }
78
+ } else {
79
+ aggregated.set(change.range.idString, change);
80
+ }
81
+ },
82
+ delete: (key: string) => {
83
+ aggregated.delete(key);
84
+ },
85
+ size: () => aggregated.size,
86
+ value: aggregated,
87
+ };
88
+ },
89
+ delay,
90
+ );
91
+ };
92
+
36
93
  export type ReplicationDomain<Args, T> = {
37
94
  type: string;
38
95
  fromEntry: ReplicationDomainMapper<T>;
@@ -40,6 +97,12 @@ export type ReplicationDomain<Args, T> = {
40
97
  args: Args | undefined,
41
98
  log: Log,
42
99
  ) => Promise<CoverRange> | CoverRange;
100
+
101
+ // to rebalance will return an async iterator of objects that will be added to the log
102
+ /* toRebalance(
103
+ change: ReplicationChange,
104
+ index: Index<EntryWithCoordinate>
105
+ ): AsyncIterable<{ gid: string, entries: { coordinate: number, hash: string }[] }> | Promise<AsyncIterable<{ gid: string, entries: EntryWithCoordinate[] }>>; */
43
106
  };
44
107
 
45
108
  export const uniformToU32 = (cursor: number) => {
@@ -6,244 +6,18 @@ import {
6
6
  variant,
7
7
  vec,
8
8
  } from "@dao-xyz/borsh";
9
- import { PublicSignKey, equals, randomBytes } from "@peerbit/crypto";
10
- import { type Index, id } from "@peerbit/indexer-interface";
9
+ import { randomBytes } from "@peerbit/crypto";
10
+ import { type Index } from "@peerbit/indexer-interface";
11
11
  import { TransportMessage } from "./message.js";
12
- import { MAX_U32, Observer, Replicator, Role, scaleToU32 } from "./role.js";
12
+ import {
13
+ ReplicationIntent,
14
+ ReplicationRange,
15
+ type ReplicationRangeIndexable,
16
+ } from "./ranges.js";
17
+ import { Observer, Replicator, Role } from "./role.js";
13
18
 
14
19
  export type ReplicationLimits = { min: MinReplicas; max?: MinReplicas };
15
20
 
16
- export enum ReplicationIntent {
17
- NonStrict = 0, // indicates that the segment will be replicated and nearby data might be replicated as well
18
- Strict = 1, // only replicate data in the segment to the specified replicator, not any other data
19
- }
20
-
21
- export const getSegmentsFromOffsetAndRange = (
22
- offset: number,
23
- factor: number,
24
- ): [[number, number], [number, number]] => {
25
- let start1 = offset;
26
- let end1Unscaled = offset + factor; // only add factor if it is not 1 to prevent numerical issues (like (0.9 + 1) % 1 => 0.8999999)
27
- let end1 = Math.min(end1Unscaled, MAX_U32);
28
- return [
29
- [start1, end1],
30
- end1Unscaled > MAX_U32
31
- ? [0, (factor !== MAX_U32 ? offset + factor : offset) % MAX_U32]
32
- : [start1, end1],
33
- ];
34
- };
35
-
36
- @variant(0)
37
- export class ReplicationRange {
38
- @field({ type: Uint8Array })
39
- private id: Uint8Array;
40
-
41
- @field({ type: "u64" })
42
- timestamp: bigint;
43
-
44
- @field({ type: "u32" })
45
- private _offset: number;
46
-
47
- @field({ type: "u32" })
48
- private _factor: number;
49
-
50
- @field({ type: "u8" })
51
- mode: ReplicationIntent;
52
-
53
- constructor(properties: {
54
- id: Uint8Array;
55
- offset: number;
56
- factor: number;
57
- timestamp: bigint;
58
- mode: ReplicationIntent;
59
- }) {
60
- const { id, offset, factor, timestamp, mode } = properties;
61
- this.id = id;
62
- this._offset = offset;
63
- this._factor = factor;
64
- this.timestamp = timestamp;
65
- this.mode = mode;
66
- }
67
-
68
- get factor(): number {
69
- return this._factor;
70
- }
71
-
72
- get offset(): number {
73
- return this._offset;
74
- }
75
-
76
- toReplicationRangeIndexable(key: PublicSignKey): ReplicationRangeIndexable {
77
- return new ReplicationRangeIndexable({
78
- id: this.id,
79
- publicKeyHash: key.hashcode(),
80
- offset: this.offset,
81
- length: this.factor,
82
- timestamp: this.timestamp,
83
- mode: this.mode,
84
- });
85
- }
86
- }
87
-
88
- export class ReplicationRangeIndexable {
89
- @id({ type: Uint8Array })
90
- id: Uint8Array;
91
-
92
- @field({ type: "string" })
93
- hash: string;
94
-
95
- @field({ type: "u64" })
96
- timestamp: bigint;
97
-
98
- @field({ type: "u32" })
99
- start1: number;
100
-
101
- @field({ type: "u32" })
102
- end1: number;
103
-
104
- @field({ type: "u32" })
105
- start2: number;
106
-
107
- @field({ type: "u32" })
108
- end2: number;
109
-
110
- @field({ type: "u32" })
111
- width: number;
112
-
113
- @field({ type: "u8" })
114
- mode: ReplicationIntent;
115
-
116
- constructor(
117
- properties: {
118
- id?: Uint8Array;
119
- normalized?: boolean;
120
- offset: number;
121
- length: number;
122
- mode?: ReplicationIntent;
123
- timestamp?: bigint;
124
- } & ({ publicKeyHash: string } | { publicKey: PublicSignKey }),
125
- ) {
126
- this.id = properties.id ?? randomBytes(32);
127
- this.hash =
128
- (properties as { publicKeyHash: string }).publicKeyHash ||
129
- (properties as { publicKey: PublicSignKey }).publicKey.hashcode();
130
- if (!properties.normalized) {
131
- this.transform({ length: properties.length, offset: properties.offset });
132
- } else {
133
- this.transform({
134
- length: scaleToU32(properties.length),
135
- offset: scaleToU32(properties.offset),
136
- });
137
- }
138
-
139
- this.mode = properties.mode ?? ReplicationIntent.NonStrict;
140
- this.timestamp = properties.timestamp || BigInt(0);
141
- }
142
-
143
- private transform(properties: { offset: number; length: number }) {
144
- const ranges = getSegmentsFromOffsetAndRange(
145
- properties.offset,
146
- properties.length,
147
- );
148
- this.start1 = Math.round(ranges[0][0]);
149
- this.end1 = Math.round(ranges[0][1]);
150
- this.start2 = Math.round(ranges[1][0]);
151
- this.end2 = Math.round(ranges[1][1]);
152
-
153
- this.width =
154
- this.end1 -
155
- this.start1 +
156
- (this.end2 < this.end1 ? this.end2 - this.start2 : 0);
157
-
158
- if (
159
- this.start1 > 0xffffffff ||
160
- this.end1 > 0xffffffff ||
161
- this.start2 > 0xffffffff ||
162
- this.end2 > 0xffffffff ||
163
- this.width > 0xffffffff ||
164
- this.width < 0
165
- ) {
166
- throw new Error("Segment coordinate out of bounds");
167
- }
168
- }
169
-
170
- contains(point: number) {
171
- return (
172
- (point >= this.start1 && point < this.end1) ||
173
- (point >= this.start2 && point < this.end2)
174
- );
175
- }
176
-
177
- overlaps(other: ReplicationRangeIndexable, checkOther = true): boolean {
178
- if (
179
- this.contains(other.start1) ||
180
- this.contains(other.start2) ||
181
- this.contains(other.end1) ||
182
- this.contains(other.end2)
183
- ) {
184
- return true;
185
- }
186
-
187
- if (checkOther) {
188
- return other.overlaps(this, false);
189
- }
190
- return false;
191
- }
192
- toReplicationRange() {
193
- return new ReplicationRange({
194
- id: this.id,
195
- offset: this.start1,
196
- factor: this.width,
197
- timestamp: this.timestamp,
198
- mode: this.mode,
199
- });
200
- }
201
-
202
- distanceTo(point: number) {
203
- let wrappedPoint = MAX_U32 - point;
204
- return Math.min(
205
- Math.abs(this.start1 - point),
206
- Math.abs(this.end2 - point),
207
- Math.abs(this.start1 - wrappedPoint),
208
- Math.abs(this.end2 - wrappedPoint),
209
- );
210
- }
211
- get wrapped() {
212
- return this.end2 < this.end1;
213
- }
214
-
215
- get widthNormalized() {
216
- return this.width / MAX_U32;
217
- }
218
-
219
- equals(other: ReplicationRangeIndexable) {
220
- if (
221
- equals(this.id, other.id) &&
222
- this.hash === other.hash &&
223
- this.timestamp === other.timestamp &&
224
- this.mode === other.mode &&
225
- this.start1 === other.start1 &&
226
- this.end1 === other.end1 &&
227
- this.start2 === other.start2 &&
228
- this.end2 === other.end2 &&
229
- this.width === other.width
230
- ) {
231
- return true;
232
- }
233
-
234
- return false;
235
- }
236
-
237
- toString() {
238
- let roundToTwoDecimals = (num: number) => Math.round(num * 100) / 100;
239
-
240
- if (Math.abs(this.start1 - this.start2) < 0.0001) {
241
- return `([${roundToTwoDecimals(this.start1 / MAX_U32)}, ${roundToTwoDecimals(this.end1 / MAX_U32)}])`;
242
- }
243
- return `([${roundToTwoDecimals(this.start1 / MAX_U32)}, ${roundToTwoDecimals(this.end1 / MAX_U32)}] [${roundToTwoDecimals(this.start2 / MAX_U32)}, ${roundToTwoDecimals(this.end2 / MAX_U32)}])`;
244
- }
245
- }
246
-
247
21
  interface SharedLog {
248
22
  replicas: Partial<ReplicationLimits>;
249
23
  replicationIndex: Index<ReplicationRangeIndexable> | undefined;
@@ -343,7 +117,7 @@ export class StoppedReplicating extends TransportMessage {
343
117
  @variant(1)
344
118
  export class RelativeMinReplicas extends MinReplicas {
345
119
  _value: number; // (0, 1]
346
-
120
+
347
121
  constructor(value: number) {
348
122
  super();
349
123
  this._value = value;
package/src/role.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { field, variant, vec } from "@dao-xyz/borsh";
7
7
 
8
8
  export const MAX_U32 = 4294967295;
9
+ export const HALF_MAX_U32 = 2147483647; // rounded down
9
10
  export const scaleToU32 = (value: number) => Math.round(MAX_U32 * value);
10
11
 
11
12
  export const overlaps = (x1: number, x2: number, y1: number, y2: number) => {
package/src/utils.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { Entry, ShallowEntry } from "@peerbit/log";
2
+ import type { EntryWithRefs } from "./exchange-heads.js";
3
+ import { EntryReplicated } from "./ranges.js";
4
+
5
+ export const groupByGid = async <
6
+ T extends ShallowEntry | Entry<any> | EntryWithRefs<any> | EntryReplicated,
7
+ >(
8
+ entries: T[],
9
+ ): Promise<Map<string, T[]>> => {
10
+ const groupByGid: Map<string, T[]> = new Map();
11
+ for (const head of entries) {
12
+ const gid =
13
+ head instanceof Entry
14
+ ? (await head.getMeta()).gid
15
+ : head instanceof ShallowEntry
16
+ ? head.meta.gid
17
+ : head instanceof EntryReplicated
18
+ ? head.gid
19
+ : (await head.entry.getMeta()).gid;
20
+ let value = groupByGid.get(gid);
21
+ if (!value) {
22
+ value = [];
23
+ groupByGid.set(gid, value);
24
+ }
25
+ value.push(head);
26
+ }
27
+ return groupByGid;
28
+ };
29
+
30
+ export const groupByGidSync = async <T extends ShallowEntry | EntryReplicated>(
31
+ entries: T[],
32
+ ): Promise<Map<string, T[]>> => {
33
+ const groupByGid: Map<string, T[]> = new Map();
34
+ for (const head of entries) {
35
+ const gid =
36
+ head instanceof Entry
37
+ ? (await head.getMeta()).gid
38
+ : head instanceof ShallowEntry
39
+ ? head.meta.gid
40
+ : head.gid;
41
+ let value = groupByGid.get(gid);
42
+ if (!value) {
43
+ value = [];
44
+ groupByGid.set(gid, value);
45
+ }
46
+ value.push(head);
47
+ }
48
+ return groupByGid;
49
+ };