effect 3.18.5 → 3.19.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.
@@ -0,0 +1,387 @@
1
+ /**
2
+ * @since 3.19.0
3
+ * @experimental
4
+ */
5
+ import { dual } from "./Function.js"
6
+ import * as Hash from "./Hash.js"
7
+ import * as Inspectable from "./Inspectable.js"
8
+ import * as Iterable from "./Iterable.js"
9
+ import { type Pipeable, pipeArguments } from "./Pipeable.js"
10
+ import { hasProperty } from "./Predicate.js"
11
+ import * as PrimaryKey from "./PrimaryKey.js"
12
+
13
+ const TypeId = "~effect/cluster/HashRing" as const
14
+
15
+ /**
16
+ * @since 3.19.0
17
+ * @category Models
18
+ * @experimental
19
+ */
20
+ export interface HashRing<A extends PrimaryKey.PrimaryKey> extends Pipeable, Iterable<A> {
21
+ readonly [TypeId]: typeof TypeId
22
+ readonly baseWeight: number
23
+ totalWeightCache: number
24
+ readonly nodes: Map<string, [node: A, weight: number]>
25
+ ring: Array<[hash: number, node: string]>
26
+ }
27
+
28
+ /**
29
+ * @since 3.19.0
30
+ * @category Guards
31
+ * @experimental
32
+ */
33
+ export const isHashRing = (u: unknown): u is HashRing<any> => hasProperty(u, TypeId)
34
+
35
+ /**
36
+ * @since 3.19.0
37
+ * @category Constructors
38
+ * @experimental
39
+ */
40
+ export const make = <A extends PrimaryKey.PrimaryKey>(options?: {
41
+ readonly baseWeight?: number | undefined
42
+ }): HashRing<A> => {
43
+ const self = Object.create(Proto)
44
+ self.baseWeight = Math.max(options?.baseWeight ?? 128, 1)
45
+ self.totalWeightCache = 0
46
+ self.nodes = new Map()
47
+ self.ring = []
48
+ return self
49
+ }
50
+
51
+ const Proto = {
52
+ [TypeId]: TypeId,
53
+ [Symbol.iterator]<A extends PrimaryKey.PrimaryKey>(this: HashRing<A>): Iterator<A> {
54
+ return Iterable.map(this.nodes.values(), ([n]) => n)[Symbol.iterator]()
55
+ },
56
+ pipe() {
57
+ return pipeArguments(this, arguments)
58
+ },
59
+ ...Inspectable.BaseProto,
60
+ toJSON(this: HashRing<any>) {
61
+ return {
62
+ _id: "HashRing",
63
+ baseWeight: this.baseWeight,
64
+ nodes: this.ring.map(([, n]) => this.nodes.get(n)![0])
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Add new nodes to the ring. If a node already exists in the ring, it
71
+ * will be updated. For example, you can use this to update the node's weight.
72
+ *
73
+ * @since 3.19.0
74
+ * @category Combinators
75
+ * @experimental
76
+ */
77
+ export const addMany: {
78
+ /**
79
+ * Add new nodes to the ring. If a node already exists in the ring, it
80
+ * will be updated. For example, you can use this to update the node's weight.
81
+ *
82
+ * @since 3.19.0
83
+ * @category Combinators
84
+ * @experimental
85
+ */
86
+ <A extends PrimaryKey.PrimaryKey>(
87
+ nodes: Iterable<A>,
88
+ options?: {
89
+ readonly weight?: number | undefined
90
+ }
91
+ ): (self: HashRing<A>) => HashRing<A>
92
+ /**
93
+ * Add new nodes to the ring. If a node already exists in the ring, it
94
+ * will be updated. For example, you can use this to update the node's weight.
95
+ *
96
+ * @since 3.19.0
97
+ * @category Combinators
98
+ * @experimental
99
+ */
100
+ <A extends PrimaryKey.PrimaryKey>(
101
+ self: HashRing<A>,
102
+ nodes: Iterable<A>,
103
+ options?: {
104
+ readonly weight?: number | undefined
105
+ }
106
+ ): HashRing<A>
107
+ } = dual(
108
+ (args) => isHashRing(args[0]),
109
+ <A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, nodes: Iterable<A>, options?: {
110
+ readonly weight?: number | undefined
111
+ }): HashRing<A> => {
112
+ const weight = Math.max(options?.weight ?? 1, 0.1)
113
+ const keys: Array<string> = []
114
+ let toRemove: Set<string> | undefined
115
+ for (const node of nodes) {
116
+ const key = PrimaryKey.value(node)
117
+ const entry = self.nodes.get(key)
118
+ if (entry) {
119
+ if (entry[1] === weight) continue
120
+ toRemove ??= new Set()
121
+ toRemove.add(key)
122
+ self.totalWeightCache -= entry[1]
123
+ self.totalWeightCache += weight
124
+ entry[1] = weight
125
+ } else {
126
+ self.nodes.set(key, [node, weight])
127
+ self.totalWeightCache += weight
128
+ }
129
+ keys.push(key)
130
+ }
131
+ if (toRemove) {
132
+ self.ring = self.ring.filter(([, n]) => !toRemove.has(n))
133
+ }
134
+ addNodesToRing(self, keys, Math.round(weight * self.baseWeight))
135
+ return self
136
+ }
137
+ )
138
+
139
+ function addNodesToRing<A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, keys: Array<string>, weight: number) {
140
+ for (let i = weight; i > 0; i--) {
141
+ for (let j = 0; j < keys.length; j++) {
142
+ const key = keys[j]
143
+ self.ring.push([
144
+ Hash.string(`${key}:${i}`),
145
+ key
146
+ ])
147
+ }
148
+ }
149
+ self.ring.sort((a, b) => a[0] - b[0])
150
+ }
151
+
152
+ /**
153
+ * Add a new node to the ring. If the node already exists in the ring, it
154
+ * will be updated. For example, you can use this to update the node's weight.
155
+ *
156
+ * @since 3.19.0
157
+ * @category Combinators
158
+ * @experimental
159
+ */
160
+ export const add: {
161
+ /**
162
+ * Add a new node to the ring. If the node already exists in the ring, it
163
+ * will be updated. For example, you can use this to update the node's weight.
164
+ *
165
+ * @since 3.19.0
166
+ * @category Combinators
167
+ * @experimental
168
+ */
169
+ <A extends PrimaryKey.PrimaryKey>(
170
+ node: A,
171
+ options?: {
172
+ readonly weight?: number | undefined
173
+ }
174
+ ): (self: HashRing<A>) => HashRing<A>
175
+ /**
176
+ * Add a new node to the ring. If the node already exists in the ring, it
177
+ * will be updated. For example, you can use this to update the node's weight.
178
+ *
179
+ * @since 3.19.0
180
+ * @category Combinators
181
+ * @experimental
182
+ */
183
+ <A extends PrimaryKey.PrimaryKey>(
184
+ self: HashRing<A>,
185
+ node: A,
186
+ options?: {
187
+ readonly weight?: number | undefined
188
+ }
189
+ ): HashRing<A>
190
+ } = dual((args) => isHashRing(args[0]), <A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, node: A, options?: {
191
+ readonly weight?: number | undefined
192
+ }): HashRing<A> => addMany(self, [node], options))
193
+
194
+ /**
195
+ * Removes the node from the ring. No-op's if the node does not exist.
196
+ *
197
+ * @since 3.19.0
198
+ * @category Combinators
199
+ * @experimental
200
+ */
201
+ export const remove: {
202
+ /**
203
+ * Removes the node from the ring. No-op's if the node does not exist.
204
+ *
205
+ * @since 3.19.0
206
+ * @category Combinators
207
+ * @experimental
208
+ */
209
+ <A extends PrimaryKey.PrimaryKey>(node: A): (self: HashRing<A>) => HashRing<A>
210
+ /**
211
+ * Removes the node from the ring. No-op's if the node does not exist.
212
+ *
213
+ * @since 3.19.0
214
+ * @category Combinators
215
+ * @experimental
216
+ */
217
+ <A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, node: A): HashRing<A>
218
+ } = dual(2, <A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, node: A): HashRing<A> => {
219
+ const key = PrimaryKey.value(node)
220
+ const entry = self.nodes.get(key)
221
+ if (entry) {
222
+ self.nodes.delete(key)
223
+ self.ring = self.ring.filter(([, n]) => n !== key)
224
+ self.totalWeightCache -= entry[1]
225
+ }
226
+ return self
227
+ })
228
+
229
+ /**
230
+ * @since 3.19.0
231
+ * @category Combinators
232
+ * @experimental
233
+ */
234
+ export const has: {
235
+ /**
236
+ * @since 3.19.0
237
+ * @category Combinators
238
+ * @experimental
239
+ */
240
+ <A extends PrimaryKey.PrimaryKey>(node: A): (self: HashRing<A>) => boolean
241
+ /**
242
+ * @since 3.19.0
243
+ * @category Combinators
244
+ * @experimental
245
+ */
246
+ <A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, node: A): boolean
247
+ } = dual(
248
+ 2,
249
+ <A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, node: A): boolean => self.nodes.has(PrimaryKey.value(node))
250
+ )
251
+
252
+ /**
253
+ * Gets the node which should handle the given input. Returns undefined if
254
+ * the hashring has no elements with weight.
255
+ *
256
+ * @since 3.19.0
257
+ * @category Combinators
258
+ * @experimental
259
+ */
260
+ export const get = <A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, input: string): A | undefined => {
261
+ if (self.ring.length === 0) {
262
+ return undefined
263
+ }
264
+ const index = getIndexForInput(self, Hash.string(input))[0]
265
+ const node = self.ring[index][1]!
266
+ return self.nodes.get(node)![0]
267
+ }
268
+
269
+ /**
270
+ * Distributes `count` shards across the nodes in the ring, attempting to
271
+ * balance the number of shards allocated to each node. Returns undefined if
272
+ * the hashring has no elements with weight.
273
+ *
274
+ * @since 3.19.0
275
+ * @category Combinators
276
+ * @experimental
277
+ */
278
+ export const getShards = <A extends PrimaryKey.PrimaryKey>(self: HashRing<A>, count: number): Array<A> | undefined => {
279
+ if (self.ring.length === 0) {
280
+ return undefined
281
+ }
282
+
283
+ const shards = new Array<A>(count)
284
+
285
+ // for tracking how many shards have been allocated to each node
286
+ const allocations = new Map<string, number>()
287
+ // for tracking which shards still need to be allocated
288
+ const remaining = new Set<number>()
289
+ // for tracking which nodes have reached the max allocation
290
+ const exclude = new Set<string>()
291
+
292
+ // First pass - allocate the closest nodes, skipping nodes that have reached
293
+ // max
294
+ const distances = new Array<[shard: number, node: string, distance: number]>(count)
295
+ for (let shard = 0; shard < count; shard++) {
296
+ const hash = (shardHashes[shard] ??= Hash.string(`shard-${shard}`))
297
+ const [index, distance] = getIndexForInput(self, hash)
298
+ const node = self.ring[index][1]!
299
+ distances[shard] = [shard, node, distance]
300
+ remaining.add(shard)
301
+ }
302
+ distances.sort((a, b) => a[2] - b[2])
303
+ for (let i = 0; i < count; i++) {
304
+ const [shard, node] = distances[i]
305
+ if (exclude.has(node)) continue
306
+ const [value, weight] = self.nodes.get(node)!
307
+ shards[shard] = value
308
+ remaining.delete(shard)
309
+ const nodeCount = (allocations.get(node) ?? 0) + 1
310
+ allocations.set(node, nodeCount)
311
+ const maxPerNode = Math.max(1, Math.floor(count * (weight / self.totalWeightCache)))
312
+ if (nodeCount >= maxPerNode) {
313
+ exclude.add(node)
314
+ }
315
+ }
316
+
317
+ // Second pass - allocate any remaining shards, skipping nodes that have
318
+ // reached max
319
+ let allAtMax = exclude.size === self.nodes.size
320
+ remaining.forEach((shard) => {
321
+ const index = getIndexForInput(self, shardHashes[shard], allAtMax ? undefined : exclude)[0]
322
+ const node = self.ring[index][1]
323
+ const [value, weight] = self.nodes.get(node)!
324
+ shards[shard] = value
325
+
326
+ if (allAtMax) return
327
+ const nodeCount = (allocations.get(node) ?? 0) + 1
328
+ allocations.set(node, nodeCount)
329
+ const maxPerNode = Math.max(1, Math.floor(count * (weight / self.totalWeightCache)))
330
+ if (nodeCount >= maxPerNode) {
331
+ exclude.add(node)
332
+ if (exclude.size === self.nodes.size) {
333
+ allAtMax = true
334
+ }
335
+ }
336
+ })
337
+
338
+ return shards
339
+ }
340
+
341
+ const shardHashes: Array<number> = []
342
+
343
+ function getIndexForInput<A extends PrimaryKey.PrimaryKey>(
344
+ self: HashRing<A>,
345
+ hash: number,
346
+ exclude?: ReadonlySet<string> | undefined
347
+ ): readonly [index: number, distance: number] {
348
+ const ring = self.ring
349
+ const len = ring.length
350
+
351
+ let mid: number
352
+ let lo = 0
353
+ let hi = len - 1
354
+
355
+ while (lo <= hi) {
356
+ mid = ((lo + hi) / 2) >>> 0
357
+ if (ring[mid][0] >= hash) {
358
+ hi = mid - 1
359
+ } else {
360
+ lo = mid + 1
361
+ }
362
+ }
363
+ const a = lo === len ? lo - 1 : lo
364
+ const distA = Math.abs(ring[a][0] - hash)
365
+ if (exclude === undefined) {
366
+ const b = lo - 1
367
+ if (b < 0) {
368
+ return [a, distA]
369
+ }
370
+ const distB = Math.abs(ring[b][0] - hash)
371
+ return distA <= distB ? [a, distA] : [b, distB]
372
+ } else if (!exclude.has(ring[a][1])) {
373
+ return [a, distA]
374
+ }
375
+ const range = Math.max(lo, len - lo)
376
+ for (let i = 1; i < range; i++) {
377
+ let index = lo - i
378
+ if (index >= 0 && index < len && !exclude.has(ring[index][1])) {
379
+ return [index, Math.abs(ring[index][0] - hash)]
380
+ }
381
+ index = lo + i
382
+ if (index >= 0 && index < len && !exclude.has(ring[index][1])) {
383
+ return [index, Math.abs(ring[index][0] - hash)]
384
+ }
385
+ }
386
+ return [a, distA]
387
+ }
package/src/Types.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  * @since 2.0.0
5
5
  */
6
6
 
7
- type _TupleOf<T, N extends number, R extends Array<unknown>> = R["length"] extends N ? R : _TupleOf<T, N, [T, ...R]>
7
+ type _TupleOf<T, N extends number, R extends Array<unknown>> = `${N}` extends `-${number}` ? never
8
+ : R["length"] extends N ? R
9
+ : _TupleOf<T, N, [T, ...R]>
8
10
 
9
11
  /**
10
12
  * Represents a tuple with a fixed number of elements of type `T`.
package/src/index.ts CHANGED
@@ -376,6 +376,12 @@ export * as Hash from "./Hash.js"
376
376
  */
377
377
  export * as HashMap from "./HashMap.js"
378
378
 
379
+ /**
380
+ * @since 3.19.0
381
+ * @experimental
382
+ */
383
+ export * as HashRing from "./HashRing.js"
384
+
379
385
  /**
380
386
  * # HashSet
381
387
  *
@@ -1,4 +1,4 @@
1
- let moduleVersion = "3.18.5"
1
+ let moduleVersion = "3.19.0"
2
2
 
3
3
  export const getCurrentVersion = () => moduleVersion
4
4