effect 3.18.4 → 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.
Files changed (51) hide show
  1. package/HashRing/package.json +6 -0
  2. package/dist/cjs/Array.js.map +1 -1
  3. package/dist/cjs/Effect.js.map +1 -1
  4. package/dist/cjs/Graph.js +290 -177
  5. package/dist/cjs/Graph.js.map +1 -1
  6. package/dist/cjs/HashRing.js +257 -0
  7. package/dist/cjs/HashRing.js.map +1 -0
  8. package/dist/cjs/JSONSchema.js +39 -8
  9. package/dist/cjs/JSONSchema.js.map +1 -1
  10. package/dist/cjs/TestClock.js +8 -8
  11. package/dist/cjs/TestClock.js.map +1 -1
  12. package/dist/cjs/index.js +4 -2
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/internal/version.js +1 -1
  15. package/dist/dts/Array.d.ts +3 -3
  16. package/dist/dts/Array.d.ts.map +1 -1
  17. package/dist/dts/Effect.d.ts +6 -1
  18. package/dist/dts/Effect.d.ts.map +1 -1
  19. package/dist/dts/Graph.d.ts +147 -49
  20. package/dist/dts/Graph.d.ts.map +1 -1
  21. package/dist/dts/HashRing.d.ts +158 -0
  22. package/dist/dts/HashRing.d.ts.map +1 -0
  23. package/dist/dts/JSONSchema.d.ts +3 -2
  24. package/dist/dts/JSONSchema.d.ts.map +1 -1
  25. package/dist/dts/Types.d.ts +1 -1
  26. package/dist/dts/Types.d.ts.map +1 -1
  27. package/dist/dts/index.d.ts +5 -0
  28. package/dist/dts/index.d.ts.map +1 -1
  29. package/dist/esm/Array.js.map +1 -1
  30. package/dist/esm/Effect.js.map +1 -1
  31. package/dist/esm/Graph.js +286 -175
  32. package/dist/esm/Graph.js.map +1 -1
  33. package/dist/esm/HashRing.js +245 -0
  34. package/dist/esm/HashRing.js.map +1 -0
  35. package/dist/esm/JSONSchema.js +35 -6
  36. package/dist/esm/JSONSchema.js.map +1 -1
  37. package/dist/esm/TestClock.js +8 -8
  38. package/dist/esm/TestClock.js.map +1 -1
  39. package/dist/esm/index.js +5 -0
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/internal/version.js +1 -1
  42. package/package.json +9 -1
  43. package/src/Array.ts +4 -4
  44. package/src/Effect.ts +6 -1
  45. package/src/Graph.ts +415 -218
  46. package/src/HashRing.ts +387 -0
  47. package/src/JSONSchema.ts +39 -9
  48. package/src/TestClock.ts +9 -9
  49. package/src/Types.ts +3 -1
  50. package/src/index.ts +6 -0
  51. package/src/internal/version.ts +1 -1
@@ -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/JSONSchema.ts CHANGED
@@ -163,7 +163,8 @@ export interface JsonSchema7Boolean extends JsonSchemaAnnotations {
163
163
  */
164
164
  export interface JsonSchema7Array extends JsonSchemaAnnotations {
165
165
  type: "array"
166
- items?: JsonSchema7 | Array<JsonSchema7>
166
+ items?: JsonSchema7 | Array<JsonSchema7> | false
167
+ prefixItems?: Array<JsonSchema7>
167
168
  minItems?: number
168
169
  maxItems?: number
169
170
  additionalItems?: JsonSchema7 | boolean
@@ -258,7 +259,7 @@ export const make = <A, I, R>(schema: Schema.Schema<A, I, R>): JsonSchema7Root =
258
259
  definitions
259
260
  })
260
261
  const out: JsonSchema7Root = {
261
- $schema,
262
+ $schema: getMetaSchemaUri("jsonSchema7"),
262
263
  $defs: {},
263
264
  ...jsonSchema
264
265
  }
@@ -270,12 +271,25 @@ export const make = <A, I, R>(schema: Schema.Schema<A, I, R>): JsonSchema7Root =
270
271
  return out
271
272
  }
272
273
 
273
- type Target = "jsonSchema7" | "jsonSchema2019-09" | "openApi3.1"
274
+ type Target = "jsonSchema7" | "jsonSchema2019-09" | "openApi3.1" | "jsonSchema2020-12"
274
275
 
275
276
  type TopLevelReferenceStrategy = "skip" | "keep"
276
277
 
277
278
  type AdditionalPropertiesStrategy = "allow" | "strict"
278
279
 
280
+ /** @internal */
281
+ export function getMetaSchemaUri(target: Target) {
282
+ switch (target) {
283
+ case "jsonSchema7":
284
+ return "http://json-schema.org/draft-07/schema#"
285
+ case "jsonSchema2019-09":
286
+ return "https://json-schema.org/draft/2019-09/schema"
287
+ case "jsonSchema2020-12":
288
+ case "openApi3.1":
289
+ return "https://json-schema.org/draft/2020-12/schema"
290
+ }
291
+ }
292
+
279
293
  /**
280
294
  * Returns a JSON Schema with additional options and definitions.
281
295
  *
@@ -365,8 +379,6 @@ const constEmptyStruct: JsonSchema7empty = {
365
379
  ]
366
380
  }
367
381
 
368
- const $schema = "http://json-schema.org/draft-07/schema#"
369
-
370
382
  function getRawDescription(annotated: AST.Annotated | undefined): string | undefined {
371
383
  if (annotated !== undefined) return Option.getOrUndefined(AST.getDescriptionAnnotation(annotated))
372
384
  }
@@ -529,6 +541,7 @@ function isContentSchemaSupported(options: GoOptions): boolean {
529
541
  case "jsonSchema7":
530
542
  return false
531
543
  case "jsonSchema2019-09":
544
+ case "jsonSchema2020-12":
532
545
  case "openApi3.1":
533
546
  return true
534
547
  }
@@ -716,7 +729,11 @@ function go(
716
729
  const len = ast.elements.length
717
730
  if (len > 0) {
718
731
  output.minItems = len - ast.elements.filter((element) => element.isOptional).length
719
- output.items = elements
732
+ if (options.target === "jsonSchema7") {
733
+ output.items = elements
734
+ } else {
735
+ output.prefixItems = elements
736
+ }
720
737
  }
721
738
  // ---------------------------------------------
722
739
  // handle rest element
@@ -726,9 +743,18 @@ function go(
726
743
  const head = rest[0]
727
744
  const isHomogeneous = restLength === 1 && ast.elements.every((e) => e.type === ast.rest[0].type)
728
745
  if (isHomogeneous) {
729
- output.items = head
746
+ if (options.target === "jsonSchema7") {
747
+ output.items = head
748
+ } else {
749
+ output.items = head
750
+ delete output.prefixItems
751
+ }
730
752
  } else {
731
- output.additionalItems = head
753
+ if (options.target === "jsonSchema7") {
754
+ output.additionalItems = head
755
+ } else {
756
+ output.items = head
757
+ }
732
758
  }
733
759
 
734
760
  // ---------------------------------------------
@@ -740,7 +766,11 @@ function go(
740
766
  }
741
767
  } else {
742
768
  if (len > 0) {
743
- output.additionalItems = false
769
+ if (options.target === "jsonSchema7") {
770
+ output.additionalItems = false
771
+ } else {
772
+ output.items = false
773
+ }
744
774
  } else {
745
775
  output.maxItems = 0
746
776
  }
package/src/TestClock.ts CHANGED
@@ -438,17 +438,17 @@ export class TestClockImpl implements TestClock {
438
438
  export const live = (data: Data): Layer.Layer<TestClock, never, Annotations.TestAnnotations | Live.TestLive> =>
439
439
  layer.scoped(
440
440
  TestClock,
441
- core.gen(function*($) {
442
- const live = yield* $(Live.TestLive)
443
- const annotations = yield* $(Annotations.TestAnnotations)
444
- const clockState = yield* $(core.sync(() => ref.unsafeMake(data)))
445
- const warningState = yield* $(circular.makeSynchronized(WarningData.start))
446
- const suspendedWarningState = yield* $(circular.makeSynchronized(SuspendedWarningData.start))
441
+ core.gen(function*() {
442
+ const live = yield* Live.TestLive
443
+ const annotations = yield* Annotations.TestAnnotations
444
+ const clockState = yield* core.sync(() => ref.unsafeMake(data))
445
+ const warningState = yield* circular.makeSynchronized(WarningData.start)
446
+ const suspendedWarningState = yield* circular.makeSynchronized(SuspendedWarningData.start)
447
447
  const testClock = new TestClockImpl(clockState, live, annotations, warningState, suspendedWarningState)
448
- yield* $(fiberRuntime.withClockScoped(testClock))
449
- yield* $(fiberRuntime.addFinalizer(
448
+ yield* fiberRuntime.withClockScoped(testClock)
449
+ yield* fiberRuntime.addFinalizer(
450
450
  () => core.zipRight(testClock.warningDone(), testClock.suspendedWarningDone())
451
- ))
451
+ )
452
452
  return testClock
453
453
  })
454
454
  )
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.4"
1
+ let moduleVersion = "3.19.0"
2
2
 
3
3
  export const getCurrentVersion = () => moduleVersion
4
4