1/**
2 * The `ShardId` module models the address of a shard inside an Effect Cluster
3 * shard group. A shard id is made from a string `group` and numeric `id`, and
4 * the module gives that pair stable equality, hashing, primary-key behavior,
5 * schema support, and conversion to and from the `group:id` string form used by
6 * routing and storage boundaries.
7 *
8 * @stability unstable
9 * @since 4.0.0
10 */
11import * as Equal from "../Equal.ts"
12import * as Hash from "../Hash.ts"
13import { hasProperty } from "../Predicate.ts"
14import * as PrimaryKey from "../PrimaryKey.ts"
15import * as S from "../Schema.ts"
16import * as SchemaGetter from "../SchemaGetter.ts"
17
18const TypeId = "~effect/cluster/ShardId"
19
20/**
21 * Identifier for a shard within a shard group, with equality, hashing, and primary
22 * key behavior based on the `group:id` string form.
23 *
24 * @stability unstable
25 * @category models
26 * @since 4.0.0
27 */
28export interface ShardId extends Equal.Equal, Hash.Hash, PrimaryKey.PrimaryKey {
29 readonly [TypeId]: typeof TypeId
30 readonly group: string
31 readonly id: number
32}
33
34/**
35 * Returns `true` when the value carries the `ShardId` runtime marker.
36 *
37 * @stability unstable
38 * @category guards
39 * @since 4.0.0
40 */
41export const isShardId = (u: unknown): u is ShardId => hasProperty(u, TypeId)
42
43/**
44 * Schema for shard identifiers encoded as `{ group, id }` objects and decoded
45 * via `make`.
46 *
47 * @stability unstable
48 * @category schemas
49 * @since 4.0.0
50 */
51export const ShardId = S.declare(isShardId, {
52 toCodecJson: () =>
53 S.link<ShardId>()(
54 S.Struct({
55 group: S.String,
56 id: S.Int
57 }),
58 {
59 decode: SchemaGetter.transform(({ group, id }) => make(group, id)),
60 encode: SchemaGetter.passthrough()
61 }
62 )
63})
64
65/**
66 * Creates or reuses the cached `ShardId` for the specified shard group and numeric
67 * id.
68 *
69 * **When to use**
70 *
71 * Use to create a `ShardId` when the shard group and numeric id are already
72 * known, such as after a routing decision or after decoding stored shard-id
73 * parts.
74 *
75 * **Details**
76 *
77 * Repeated calls with the same `group` and `id` return the same cached
78 * `ShardId` instance. The returned value stores those fields, compares by
79 * `group` and `id`, formats as `group:id`, and uses that string form for
80 * hashing and primary keys.
81 *
82 * **Gotchas**
83 *
84 * `make` does not compute a shard from an entity id or check whether the shard
85 * belongs to the current sharding configuration. Pass the shard group and
86 * numeric id produced by the routing or storage layer.
87 *
88 * @see {@link toString} for formatting an existing shard id as `group:id`
89 * @see {@link fromString} for constructing a cached shard id from the `group:id` string form
90 *
91 * @stability unstable
92 * @category constructors
93 * @since 4.0.0
94 */
95export const make = (group: string, id: number): ShardId => {
96 const key = `${group}:${id}`
97 let shardId = shardIdCache.get(key)
98 if (!shardId) {
99 shardId = makeProto(group, id)
100 shardIdCache.set(key, shardId)
101 }
102 return shardId
103}
104
105const shardIdCache = new Map<string, ShardId>()
106
107const makeProto = (group: string, id: number): ShardId => {
108 const self = Object.create(ShardIdProto)
109 self.group = group
110 self.id = id
111 return self
112}
113
114const ShardIdProto = {
115 [TypeId]: TypeId,
116 [Equal.symbol](this: ShardId, that: ShardId): boolean {
117 return this.group === that.group && this.id === that.id
118 },
119 [Hash.symbol](this: ShardId): number {
120 return Hash.string(this.toString())
121 },
122 [PrimaryKey.symbol](this: ShardId): string {
123 return this.toString()
124 },
125 toString(this: ShardId): string {
126 return `${this.group}:${this.id}`
127 }
128}
129
130/**
131 * Formats a shard identifier as `group:id`.
132 *
133 * @stability unstable
134 * @category converting
135 * @since 4.0.0
136 */
137export const toString = (shardId: {
138 readonly group: string
139 readonly id: number
140}): string => {
141 return `${shardId.group}:${shardId.id}`
142}
143/**
144 * Parses a `group:id` string into plain shard id parts.
145 *
146 * **Details**
147 *
148 * Throws an `Error` when the string has no colon separator or the id segment is
149 * not numeric.
150 *
151 * @stability unstable
152 * @category decoding
153 * @since 4.0.0
154 */
155export function fromStringEncoded(s: string): {
156 readonly group: string
157 readonly id: number
158} {
159 const index = s.lastIndexOf(":")
160 if (index === -1) {
161 throw new Error(`Invalid ShardId format`)
162 }
163 const group = s.substring(0, index)
164 const id = Number(s.substring(index + 1))
165 if (isNaN(id)) {
166 throw new Error(`ShardId id must be a number`)
167 }
168 return { group, id }
169}
170
171/**
172 * Parses a `group:id` string into a cached `ShardId`.
173 *
174 * **Details**
175 *
176 * Throws an `Error` when the string has no colon separator or the id segment is
177 * not numeric.
178 *
179 * @stability unstable
180 * @category decoding
181 * @since 4.0.0
182 */
183export function fromString(s: string): ShardId {
184 const encoded = fromStringEncoded(s)
185 return make(encoded.group, encoded.id)
186}