1import {
2 EnvironmentId,
3 ProjectId,
4 ThreadId,
5 type ScopedProjectRef,
6 type ScopedThreadRef,
7} from "@t3tools/contracts";
8import * as Schema from "effect/Schema";
9
10export class InvalidScopedProjectKeyError extends Schema.TaggedError<InvalidScopedProjectKeyError>()(
11 "InvalidScopedProjectKeyError",
12 {
13 key: Schema.String,
14 },
15) {
16 override get message(): string {
17 return `Invalid scoped project atom key: ${JSON.stringify(this.key)}.`;
18 }
19}
20
21export class InvalidScopedThreadKeyError extends Schema.TaggedError<InvalidScopedThreadKeyError>()(
22 "InvalidScopedThreadKeyError",
23 {
24 key: Schema.String,
25 },
26) {
27 override get message(): string {
28 return `Invalid scoped thread atom key: ${JSON.stringify(this.key)}.`;
29 }
30}
31
32export class InvalidScopedProjectRefCollectionKeyError extends Schema.TaggedError<InvalidScopedProjectRefCollectionKeyError>()(
33 "InvalidScopedProjectRefCollectionKeyError",
34 {
35 key: Schema.String,
36 cause: Schema.Defect(),
37 },
38) {
39 override get message(): string {
40 return `Invalid scoped project reference collection atom key: ${JSON.stringify(this.key)}.`;
41 }
42}
43
44const decodeProjectRefCollectionKey = Schema.decodeUnknownSync(
45 Schema.Array(Schema.Tuple([Schema.String, Schema.String])),
46);
47
48export function projectKey(ref: ScopedProjectRef): string {
49 return `${ref.environmentId}\u0000${ref.projectId}`;
50}
51
52export function threadKey(ref: ScopedThreadRef): string {
53 return `${ref.environmentId}\u0000${ref.threadId}`;
54}
55
56export function projectRefCollectionKey(refs: ReadonlyArray<ScopedProjectRef>): string {
57 return JSON.stringify(refs.map((ref) => [ref.environmentId, ref.projectId]));
58}
59
60export function parseProjectKey(key: string): ScopedProjectRef {
61 const separator = key.indexOf("\u0000");
62 if (separator < 0) {
63 throw new InvalidScopedProjectKeyError({ key });
64 }
65 return {
66 environmentId: EnvironmentId.make(key.slice(0, separator)),
67 projectId: ProjectId.make(key.slice(separator + 1)),
68 };
69}
70
71export function parseProjectRefCollectionKey(key: string): ReadonlyArray<ScopedProjectRef> {
72 let entries: ReadonlyArray<readonly [string, string]>;
73 try {
74 entries = decodeProjectRefCollectionKey(JSON.parse(key));
75 } catch (cause) {
76 throw new InvalidScopedProjectRefCollectionKeyError({ key, cause });
77 }
78 return entries.map(([environmentId, projectId]) => ({
79 environmentId: EnvironmentId.make(environmentId),
80 projectId: ProjectId.make(projectId),
81 }));
82}
83
84export function parseThreadKey(key: string): ScopedThreadRef {
85 const separator = key.indexOf("\u0000");
86 if (separator < 0) {
87 throw new InvalidScopedThreadKeyError({ key });
88 }
89 return {
90 environmentId: EnvironmentId.make(key.slice(0, separator)),
91 threadId: ThreadId.make(key.slice(separator + 1)),
92 };
93}
94
95export function projectRefsEqual(
96 left: ReadonlyArray<ScopedProjectRef>,
97 right: ReadonlyArray<ScopedProjectRef>,
98): boolean {
99 return (
100 left.length === right.length &&
101 left.every(
102 (ref, index) =>
103 ref.environmentId === right[index]?.environmentId &&
104 ref.projectId === right[index]?.projectId,
105 )
106 );
107}
108
109export function threadRefsEqual(
110 left: ReadonlyArray<ScopedThreadRef>,
111 right: ReadonlyArray<ScopedThreadRef>,
112): boolean {
113 return (
114 left.length === right.length &&
115 left.every(
116 (ref, index) =>
117 ref.environmentId === right[index]?.environmentId &&
118 ref.threadId === right[index]?.threadId,
119 )
120 );
121}
122
123export function arrayElementsEqual<A>(left: ReadonlyArray<A>, right: ReadonlyArray<A>): boolean {
124 return left.length === right.length && left.every((value, index) => value === right[index]);
125}