@agentxjs/core 1.9.1-dev

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 (77) hide show
  1. package/package.json +31 -0
  2. package/src/agent/AgentStateMachine.ts +151 -0
  3. package/src/agent/README.md +296 -0
  4. package/src/agent/__tests__/AgentStateMachine.test.ts +346 -0
  5. package/src/agent/__tests__/createAgent.test.ts +728 -0
  6. package/src/agent/__tests__/engine/internal/messageAssemblerProcessor.test.ts +567 -0
  7. package/src/agent/__tests__/engine/internal/stateEventProcessor.test.ts +315 -0
  8. package/src/agent/__tests__/engine/internal/turnTrackerProcessor.test.ts +340 -0
  9. package/src/agent/__tests__/engine/mealy/Mealy.test.ts +370 -0
  10. package/src/agent/__tests__/engine/mealy/Store.test.ts +123 -0
  11. package/src/agent/__tests__/engine/mealy/combinators.test.ts +322 -0
  12. package/src/agent/createAgent.ts +467 -0
  13. package/src/agent/engine/AgentProcessor.ts +106 -0
  14. package/src/agent/engine/MealyMachine.ts +184 -0
  15. package/src/agent/engine/internal/index.ts +35 -0
  16. package/src/agent/engine/internal/messageAssemblerProcessor.ts +550 -0
  17. package/src/agent/engine/internal/stateEventProcessor.ts +313 -0
  18. package/src/agent/engine/internal/turnTrackerProcessor.ts +239 -0
  19. package/src/agent/engine/mealy/Mealy.ts +308 -0
  20. package/src/agent/engine/mealy/Processor.ts +70 -0
  21. package/src/agent/engine/mealy/Sink.ts +56 -0
  22. package/src/agent/engine/mealy/Source.ts +51 -0
  23. package/src/agent/engine/mealy/Store.ts +98 -0
  24. package/src/agent/engine/mealy/combinators.ts +176 -0
  25. package/src/agent/engine/mealy/index.ts +45 -0
  26. package/src/agent/index.ts +106 -0
  27. package/src/agent/types/engine.ts +395 -0
  28. package/src/agent/types/event.ts +478 -0
  29. package/src/agent/types/index.ts +197 -0
  30. package/src/agent/types/message.ts +387 -0
  31. package/src/common/index.ts +8 -0
  32. package/src/common/logger/ConsoleLogger.ts +137 -0
  33. package/src/common/logger/LoggerFactoryImpl.ts +123 -0
  34. package/src/common/logger/index.ts +26 -0
  35. package/src/common/logger/types.ts +98 -0
  36. package/src/container/Container.ts +185 -0
  37. package/src/container/index.ts +44 -0
  38. package/src/container/types.ts +71 -0
  39. package/src/driver/index.ts +42 -0
  40. package/src/driver/types.ts +363 -0
  41. package/src/event/EventBus.ts +260 -0
  42. package/src/event/README.md +237 -0
  43. package/src/event/__tests__/EventBus.test.ts +251 -0
  44. package/src/event/index.ts +46 -0
  45. package/src/event/types/agent.ts +512 -0
  46. package/src/event/types/base.ts +241 -0
  47. package/src/event/types/bus.ts +429 -0
  48. package/src/event/types/command.ts +749 -0
  49. package/src/event/types/container.ts +471 -0
  50. package/src/event/types/driver.ts +452 -0
  51. package/src/event/types/index.ts +26 -0
  52. package/src/event/types/session.ts +314 -0
  53. package/src/image/Image.ts +203 -0
  54. package/src/image/index.ts +36 -0
  55. package/src/image/types.ts +77 -0
  56. package/src/index.ts +20 -0
  57. package/src/mq/OffsetGenerator.ts +48 -0
  58. package/src/mq/README.md +166 -0
  59. package/src/mq/__tests__/OffsetGenerator.test.ts +121 -0
  60. package/src/mq/index.ts +18 -0
  61. package/src/mq/types.ts +172 -0
  62. package/src/network/RpcClient.ts +455 -0
  63. package/src/network/index.ts +76 -0
  64. package/src/network/jsonrpc.ts +336 -0
  65. package/src/network/protocol.ts +90 -0
  66. package/src/network/types.ts +284 -0
  67. package/src/persistence/index.ts +27 -0
  68. package/src/persistence/types.ts +226 -0
  69. package/src/runtime/AgentXRuntime.ts +501 -0
  70. package/src/runtime/index.ts +56 -0
  71. package/src/runtime/types.ts +236 -0
  72. package/src/session/Session.ts +71 -0
  73. package/src/session/index.ts +25 -0
  74. package/src/session/types.ts +77 -0
  75. package/src/workspace/index.ts +27 -0
  76. package/src/workspace/types.ts +131 -0
  77. package/tsconfig.json +10 -0
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Session Events - Session lifecycle, persist, and action events
3
+ *
4
+ * Events for session operations in the event system.
5
+ */
6
+
7
+ import type { SystemEvent } from "./base";
8
+
9
+ // ============================================================================
10
+ // Session Lifecycle Events
11
+ // ============================================================================
12
+
13
+ /**
14
+ * Base SessionLifecycleEvent
15
+ */
16
+ interface BaseSessionLifecycleEvent<T extends string, D = unknown> extends SystemEvent<
17
+ T,
18
+ D,
19
+ "session",
20
+ "lifecycle",
21
+ "notification"
22
+ > {}
23
+
24
+ /**
25
+ * SessionCreatedEvent - Session was created
26
+ */
27
+ export interface SessionCreatedEvent extends BaseSessionLifecycleEvent<
28
+ "session_created",
29
+ {
30
+ sessionId: string;
31
+ imageId: string;
32
+ containerId: string;
33
+ title?: string;
34
+ createdAt: number;
35
+ }
36
+ > {}
37
+
38
+ /**
39
+ * SessionDestroyedEvent - Session was destroyed
40
+ */
41
+ export interface SessionDestroyedEvent extends BaseSessionLifecycleEvent<
42
+ "session_destroyed",
43
+ {
44
+ sessionId: string;
45
+ reason?: string;
46
+ }
47
+ > {}
48
+
49
+ /**
50
+ * SessionLifecycleEvent - All session lifecycle events
51
+ */
52
+ export type SessionLifecycleEvent = SessionCreatedEvent | SessionDestroyedEvent;
53
+
54
+ /**
55
+ * Type guard: is this a SessionLifecycleEvent?
56
+ */
57
+ export function isSessionLifecycleEvent(event: {
58
+ source?: string;
59
+ category?: string;
60
+ }): event is SessionLifecycleEvent {
61
+ return event.source === "session" && event.category === "lifecycle";
62
+ }
63
+
64
+ // ============================================================================
65
+ // Session Persist Events
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Base SessionPersistRequest
70
+ */
71
+ interface BaseSessionPersistRequest<T extends string, D = unknown> extends SystemEvent<
72
+ T,
73
+ D,
74
+ "session",
75
+ "persist",
76
+ "request"
77
+ > {}
78
+
79
+ /**
80
+ * Base SessionPersistResult
81
+ */
82
+ interface BaseSessionPersistResult<T extends string, D = unknown> extends SystemEvent<
83
+ T,
84
+ D,
85
+ "session",
86
+ "persist",
87
+ "result"
88
+ > {}
89
+
90
+ /**
91
+ * SessionSaveRequest - Request to save session
92
+ */
93
+ export interface SessionSaveRequest extends BaseSessionPersistRequest<
94
+ "session_save_request",
95
+ {
96
+ sessionId: string;
97
+ title?: string;
98
+ metadata?: Record<string, unknown>;
99
+ }
100
+ > {}
101
+
102
+ /**
103
+ * SessionSavedEvent - Session was saved
104
+ */
105
+ export interface SessionSavedEvent extends BaseSessionPersistResult<
106
+ "session_saved",
107
+ {
108
+ sessionId: string;
109
+ savedAt: number;
110
+ }
111
+ > {}
112
+
113
+ /**
114
+ * MessagePersistRequest - Request to persist a message
115
+ */
116
+ export interface MessagePersistRequest extends BaseSessionPersistRequest<
117
+ "message_persist_request",
118
+ {
119
+ sessionId: string;
120
+ messageId: string;
121
+ role: "user" | "assistant" | "tool_call" | "tool_result";
122
+ content: unknown;
123
+ }
124
+ > {}
125
+
126
+ /**
127
+ * MessagePersistedEvent - Message was persisted
128
+ */
129
+ export interface MessagePersistedEvent extends BaseSessionPersistResult<
130
+ "message_persisted",
131
+ {
132
+ sessionId: string;
133
+ messageId: string;
134
+ savedAt: number;
135
+ }
136
+ > {}
137
+
138
+ /**
139
+ * SessionPersistEvent - All session persist events
140
+ */
141
+ export type SessionPersistEvent =
142
+ | SessionSaveRequest
143
+ | SessionSavedEvent
144
+ | MessagePersistRequest
145
+ | MessagePersistedEvent;
146
+
147
+ /**
148
+ * Session persist request events
149
+ */
150
+ export type SessionPersistRequestEvent = SessionSaveRequest | MessagePersistRequest;
151
+
152
+ /**
153
+ * Session persist result events
154
+ */
155
+ export type SessionPersistResultEvent = SessionSavedEvent | MessagePersistedEvent;
156
+
157
+ /**
158
+ * Type guard: is this a SessionPersistEvent?
159
+ */
160
+ export function isSessionPersistEvent(event: {
161
+ source?: string;
162
+ category?: string;
163
+ }): event is SessionPersistEvent {
164
+ return event.source === "session" && event.category === "persist";
165
+ }
166
+
167
+ // ============================================================================
168
+ // Session Action Events
169
+ // ============================================================================
170
+
171
+ /**
172
+ * Base SessionActionRequest
173
+ */
174
+ interface BaseSessionActionRequest<T extends string, D = unknown> extends SystemEvent<
175
+ T,
176
+ D,
177
+ "session",
178
+ "action",
179
+ "request"
180
+ > {}
181
+
182
+ /**
183
+ * Base SessionActionResult
184
+ */
185
+ interface BaseSessionActionResult<T extends string, D = unknown> extends SystemEvent<
186
+ T,
187
+ D,
188
+ "session",
189
+ "action",
190
+ "result"
191
+ > {}
192
+
193
+ /**
194
+ * SessionResumeRequest - Request to resume a session
195
+ */
196
+ export interface SessionResumeRequest extends BaseSessionActionRequest<
197
+ "session_resume_request",
198
+ {
199
+ sessionId: string;
200
+ containerId?: string;
201
+ }
202
+ > {}
203
+
204
+ /**
205
+ * SessionResumedEvent - Session was resumed
206
+ */
207
+ export interface SessionResumedEvent extends BaseSessionActionResult<
208
+ "session_resumed",
209
+ {
210
+ sessionId: string;
211
+ agentId: string;
212
+ resumedAt: number;
213
+ }
214
+ > {}
215
+
216
+ /**
217
+ * SessionForkRequest - Request to fork a session
218
+ */
219
+ export interface SessionForkRequest extends BaseSessionActionRequest<
220
+ "session_fork_request",
221
+ {
222
+ sessionId: string;
223
+ newTitle?: string;
224
+ }
225
+ > {}
226
+
227
+ /**
228
+ * SessionForkedEvent - Session was forked
229
+ */
230
+ export interface SessionForkedEvent extends BaseSessionActionResult<
231
+ "session_forked",
232
+ {
233
+ originalSessionId: string;
234
+ newSessionId: string;
235
+ newImageId: string;
236
+ forkedAt: number;
237
+ }
238
+ > {}
239
+
240
+ /**
241
+ * SessionTitleUpdateRequest - Request to update session title
242
+ */
243
+ export interface SessionTitleUpdateRequest extends BaseSessionActionRequest<
244
+ "session_title_update_request",
245
+ {
246
+ sessionId: string;
247
+ title: string;
248
+ }
249
+ > {}
250
+
251
+ /**
252
+ * SessionTitleUpdatedEvent - Session title was updated
253
+ */
254
+ export interface SessionTitleUpdatedEvent extends BaseSessionActionResult<
255
+ "session_title_updated",
256
+ {
257
+ sessionId: string;
258
+ title: string;
259
+ updatedAt: number;
260
+ }
261
+ > {}
262
+
263
+ /**
264
+ * SessionActionEvent - All session action events
265
+ */
266
+ export type SessionActionEvent =
267
+ | SessionResumeRequest
268
+ | SessionResumedEvent
269
+ | SessionForkRequest
270
+ | SessionForkedEvent
271
+ | SessionTitleUpdateRequest
272
+ | SessionTitleUpdatedEvent;
273
+
274
+ /**
275
+ * Session action request events
276
+ */
277
+ export type SessionActionRequestEvent =
278
+ | SessionResumeRequest
279
+ | SessionForkRequest
280
+ | SessionTitleUpdateRequest;
281
+
282
+ /**
283
+ * Session action result events
284
+ */
285
+ export type SessionActionResultEvent =
286
+ | SessionResumedEvent
287
+ | SessionForkedEvent
288
+ | SessionTitleUpdatedEvent;
289
+
290
+ /**
291
+ * Type guard: is this a SessionActionEvent?
292
+ */
293
+ export function isSessionActionEvent(event: {
294
+ source?: string;
295
+ category?: string;
296
+ }): event is SessionActionEvent {
297
+ return event.source === "session" && event.category === "action";
298
+ }
299
+
300
+ // ============================================================================
301
+ // Session Event Union
302
+ // ============================================================================
303
+
304
+ /**
305
+ * SessionEvent - All session events
306
+ */
307
+ export type SessionEvent = SessionLifecycleEvent | SessionPersistEvent | SessionActionEvent;
308
+
309
+ /**
310
+ * Type guard: is this a session event?
311
+ */
312
+ export function isSessionEvent(event: { source?: string }): event is SessionEvent {
313
+ return event.source === "session";
314
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Image - Persistent conversation entity
3
+ *
4
+ * Image is the primary entity that users interact with (displayed as "conversation").
5
+ * Agent is a transient runtime instance created from Image.
6
+ */
7
+
8
+ import { createLogger } from "commonxjs/logger";
9
+ import type { Image, ImageRecord, ImageContext, ImageCreateConfig } from "./types";
10
+
11
+ const logger = createLogger("image/Image");
12
+
13
+ /**
14
+ * ImageImpl - Image implementation
15
+ */
16
+ export class ImageImpl implements Image {
17
+ private constructor(
18
+ private readonly record: ImageRecord,
19
+ private readonly context: ImageContext
20
+ ) {}
21
+
22
+ // ==================== Getters ====================
23
+
24
+ get imageId(): string {
25
+ return this.record.imageId;
26
+ }
27
+
28
+ get containerId(): string {
29
+ return this.record.containerId;
30
+ }
31
+
32
+ get sessionId(): string {
33
+ return this.record.sessionId;
34
+ }
35
+
36
+ get name(): string {
37
+ return this.record.name;
38
+ }
39
+
40
+ get description(): string | undefined {
41
+ return this.record.description;
42
+ }
43
+
44
+ get systemPrompt(): string | undefined {
45
+ return this.record.systemPrompt;
46
+ }
47
+
48
+ get mcpServers() {
49
+ return this.record.mcpServers;
50
+ }
51
+
52
+ get createdAt(): number {
53
+ return this.record.createdAt;
54
+ }
55
+
56
+ get updatedAt(): number {
57
+ return this.record.updatedAt;
58
+ }
59
+
60
+ // ==================== Static Factory Methods ====================
61
+
62
+ /**
63
+ * Create a new image (conversation)
64
+ */
65
+ static async create(config: ImageCreateConfig, context: ImageContext): Promise<ImageImpl> {
66
+ const now = Date.now();
67
+ const imageId = ImageImpl.generateImageId();
68
+ const sessionId = ImageImpl.generateSessionId();
69
+
70
+ // Create image record
71
+ const record: ImageRecord = {
72
+ imageId,
73
+ containerId: config.containerId,
74
+ sessionId,
75
+ name: config.name ?? "New Conversation",
76
+ description: config.description,
77
+ systemPrompt: config.systemPrompt,
78
+ mcpServers: config.mcpServers,
79
+ createdAt: now,
80
+ updatedAt: now,
81
+ };
82
+
83
+ // Persist image
84
+ await context.imageRepository.saveImage(record);
85
+
86
+ // Create associated session (for message storage)
87
+ await context.sessionRepository.saveSession({
88
+ sessionId,
89
+ imageId,
90
+ containerId: config.containerId,
91
+ createdAt: now,
92
+ updatedAt: now,
93
+ });
94
+
95
+ logger.info("Image created", {
96
+ imageId,
97
+ sessionId,
98
+ containerId: config.containerId,
99
+ name: record.name,
100
+ });
101
+
102
+ return new ImageImpl(record, context);
103
+ }
104
+
105
+ /**
106
+ * Load an existing image from storage
107
+ */
108
+ static async load(imageId: string, context: ImageContext): Promise<ImageImpl | null> {
109
+ const record = await context.imageRepository.findImageById(imageId);
110
+ if (!record) {
111
+ logger.debug("Image not found", { imageId });
112
+ return null;
113
+ }
114
+
115
+ logger.debug("Image loaded", { imageId, name: record.name });
116
+ return new ImageImpl(record, context);
117
+ }
118
+
119
+ /**
120
+ * List all images in a container
121
+ */
122
+ static async listByContainer(containerId: string, context: ImageContext): Promise<ImageRecord[]> {
123
+ return context.imageRepository.findImagesByContainerId(containerId);
124
+ }
125
+
126
+ /**
127
+ * List all images
128
+ */
129
+ static async listAll(context: ImageContext): Promise<ImageRecord[]> {
130
+ return context.imageRepository.findAllImages();
131
+ }
132
+
133
+ // ==================== Instance Methods ====================
134
+
135
+ /**
136
+ * Update image metadata
137
+ */
138
+ async update(updates: { name?: string; description?: string }): Promise<Image> {
139
+ const now = Date.now();
140
+ const updatedRecord: ImageRecord = {
141
+ ...this.record,
142
+ name: updates.name ?? this.record.name,
143
+ description: updates.description ?? this.record.description,
144
+ updatedAt: now,
145
+ };
146
+
147
+ await this.context.imageRepository.saveImage(updatedRecord);
148
+
149
+ logger.info("Image updated", { imageId: this.imageId, updates });
150
+ return new ImageImpl(updatedRecord, this.context);
151
+ }
152
+
153
+ /**
154
+ * Delete this image and its session
155
+ */
156
+ async delete(): Promise<void> {
157
+ // Delete session first (including messages)
158
+ await this.context.sessionRepository.deleteSession(this.sessionId);
159
+
160
+ // Delete image
161
+ await this.context.imageRepository.deleteImage(this.imageId);
162
+
163
+ logger.info("Image deleted", { imageId: this.imageId, sessionId: this.sessionId });
164
+ }
165
+
166
+ /**
167
+ * Get the underlying record
168
+ */
169
+ toRecord(): ImageRecord {
170
+ return { ...this.record };
171
+ }
172
+
173
+ // ==================== Private Helpers ====================
174
+
175
+ private static generateImageId(): string {
176
+ const timestamp = Date.now().toString(36);
177
+ const random = Math.random().toString(36).substring(2, 8);
178
+ return `img_${timestamp}_${random}`;
179
+ }
180
+
181
+ private static generateSessionId(): string {
182
+ const timestamp = Date.now().toString(36);
183
+ const random = Math.random().toString(36).substring(2, 8);
184
+ return `sess_${timestamp}_${random}`;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Create a new Image
190
+ */
191
+ export async function createImage(
192
+ config: ImageCreateConfig,
193
+ context: ImageContext
194
+ ): Promise<Image> {
195
+ return ImageImpl.create(config, context);
196
+ }
197
+
198
+ /**
199
+ * Load an existing Image
200
+ */
201
+ export async function loadImage(imageId: string, context: ImageContext): Promise<Image | null> {
202
+ return ImageImpl.load(imageId, context);
203
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Image Module
3
+ *
4
+ * Manages persistent conversation entities (Images).
5
+ *
6
+ * Usage:
7
+ * ```typescript
8
+ * import { createImage, loadImage, type ImageRepository } from "@agentxjs/core/image";
9
+ *
10
+ * const context = {
11
+ * imageRepository: myImageRepository,
12
+ * sessionRepository: mySessionRepository,
13
+ * };
14
+ *
15
+ * // Create new image
16
+ * const image = await createImage({
17
+ * containerId: "container-1",
18
+ * name: "My Conversation",
19
+ * systemPrompt: "You are helpful",
20
+ * }, context);
21
+ *
22
+ * // Load existing image
23
+ * const existing = await loadImage("img_xxx", context);
24
+ * ```
25
+ */
26
+
27
+ export type {
28
+ ImageMetadata,
29
+ ImageRecord,
30
+ ImageRepository,
31
+ Image,
32
+ ImageContext,
33
+ ImageCreateConfig,
34
+ } from "./types";
35
+
36
+ export { ImageImpl, createImage, loadImage } from "./Image";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Image Types
3
+ *
4
+ * Image is the persistent representation of a conversation.
5
+ * Agent is a transient runtime instance created from Image.
6
+ *
7
+ * Lifecycle:
8
+ * - create() → ImageRecord (persistent) + SessionRecord (for messages)
9
+ * - run() → Agent (runtime, in-memory)
10
+ * - stop() / server restart → Agent destroyed, Image remains
11
+ */
12
+
13
+ import type { McpServerConfig } from "../driver/types";
14
+ import type { SessionRepository } from "../persistence/types";
15
+
16
+ // ============================================================================
17
+ // Re-export from persistence (storage schema)
18
+ // ============================================================================
19
+
20
+ export type { ImageMetadata, ImageRecord, ImageRepository } from "../persistence/types";
21
+
22
+ // ============================================================================
23
+ // Image Interface
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Image - Persistent conversation entity
28
+ */
29
+ export interface Image {
30
+ readonly imageId: string;
31
+ readonly containerId: string;
32
+ readonly sessionId: string;
33
+ readonly name: string;
34
+ readonly description: string | undefined;
35
+ readonly systemPrompt: string | undefined;
36
+ readonly mcpServers: Record<string, McpServerConfig> | undefined;
37
+ readonly createdAt: number;
38
+ readonly updatedAt: number;
39
+
40
+ /**
41
+ * Update image metadata (name, description)
42
+ */
43
+ update(updates: { name?: string; description?: string }): Promise<Image>;
44
+
45
+ /**
46
+ * Delete this image and its session
47
+ */
48
+ delete(): Promise<void>;
49
+
50
+ /**
51
+ * Get the underlying record
52
+ */
53
+ toRecord(): import("../persistence/types").ImageRecord;
54
+ }
55
+
56
+ // ============================================================================
57
+ // Image Configuration
58
+ // ============================================================================
59
+
60
+ /**
61
+ * Context needed by Image operations
62
+ */
63
+ export interface ImageContext {
64
+ imageRepository: import("../persistence/types").ImageRepository;
65
+ sessionRepository: SessionRepository;
66
+ }
67
+
68
+ /**
69
+ * Configuration for creating a new Image
70
+ */
71
+ export interface ImageCreateConfig {
72
+ containerId: string;
73
+ name?: string;
74
+ description?: string;
75
+ systemPrompt?: string;
76
+ mcpServers?: Record<string, McpServerConfig>;
77
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @agentxjs/core
3
+ *
4
+ * Core library for AgentX framework.
5
+ * Provides types, common utilities, and agent implementation.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ // ============================================================================
11
+ // Common Utilities
12
+ // ============================================================================
13
+
14
+ export * from "./common";
15
+
16
+ // ============================================================================
17
+ // Agent (includes all types)
18
+ // ============================================================================
19
+
20
+ export * from "./agent";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * OffsetGenerator - Generates monotonically increasing offsets
3
+ *
4
+ * Format: "{timestamp_base36}-{sequence_padded}"
5
+ * Example: "lq5x4g2-0001"
6
+ *
7
+ * This format ensures:
8
+ * - Lexicographic ordering matches temporal ordering
9
+ * - Multiple events in same millisecond get unique offsets
10
+ * - Human-readable and compact
11
+ */
12
+ export class OffsetGenerator {
13
+ private lastTimestamp = 0;
14
+ private sequence = 0;
15
+
16
+ /**
17
+ * Generate a new offset
18
+ */
19
+ generate(): string {
20
+ const now = Date.now();
21
+
22
+ if (now === this.lastTimestamp) {
23
+ this.sequence++;
24
+ } else {
25
+ this.lastTimestamp = now;
26
+ this.sequence = 0;
27
+ }
28
+
29
+ const timestampPart = now.toString(36);
30
+ const sequencePart = this.sequence.toString().padStart(4, "0");
31
+
32
+ return `${timestampPart}-${sequencePart}`;
33
+ }
34
+
35
+ /**
36
+ * Compare two offsets
37
+ * @returns negative if a < b, 0 if a == b, positive if a > b
38
+ */
39
+ static compare(a: string, b: string): number {
40
+ const [aTime, aSeq] = a.split("-");
41
+ const [bTime, bSeq] = b.split("-");
42
+
43
+ const timeDiff = parseInt(aTime, 36) - parseInt(bTime, 36);
44
+ if (timeDiff !== 0) return timeDiff;
45
+
46
+ return parseInt(aSeq) - parseInt(bSeq);
47
+ }
48
+ }