@agentxjs/node-platform 2.0.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.
- package/README.md +134 -0
- package/dist/WebSocketConnection-BUL85bFC.d.ts +66 -0
- package/dist/WebSocketFactory-SDWPRZVB.js +8 -0
- package/dist/WebSocketFactory-SDWPRZVB.js.map +1 -0
- package/dist/chunk-BBZV6B5R.js +264 -0
- package/dist/chunk-BBZV6B5R.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-PK2K7CCJ.js +213 -0
- package/dist/chunk-PK2K7CCJ.js.map +1 -0
- package/dist/chunk-TXESAX3X.js +361 -0
- package/dist/chunk-TXESAX3X.js.map +1 -0
- package/dist/chunk-V664KD3R.js +14 -0
- package/dist/chunk-V664KD3R.js.map +1 -0
- package/dist/index.d.ts +140 -0
- package/dist/index.js +223 -0
- package/dist/index.js.map +1 -0
- package/dist/mq/index.d.ts +63 -0
- package/dist/mq/index.js +10 -0
- package/dist/mq/index.js.map +1 -0
- package/dist/network/index.d.ts +17 -0
- package/dist/network/index.js +14 -0
- package/dist/network/index.js.map +1 -0
- package/dist/persistence/index.d.ts +175 -0
- package/dist/persistence/index.js +18 -0
- package/dist/persistence/index.js.map +1 -0
- package/package.json +50 -0
- package/src/bash/NodeBashProvider.ts +54 -0
- package/src/index.ts +151 -0
- package/src/logger/FileLoggerFactory.ts +175 -0
- package/src/logger/index.ts +5 -0
- package/src/mq/OffsetGenerator.ts +48 -0
- package/src/mq/SqliteMessageQueue.ts +240 -0
- package/src/mq/index.ts +30 -0
- package/src/network/WebSocketConnection.ts +206 -0
- package/src/network/WebSocketFactory.ts +17 -0
- package/src/network/WebSocketServer.ts +156 -0
- package/src/network/index.ts +32 -0
- package/src/persistence/Persistence.ts +53 -0
- package/src/persistence/StorageContainerRepository.ts +58 -0
- package/src/persistence/StorageImageRepository.ts +153 -0
- package/src/persistence/StorageSessionRepository.ts +171 -0
- package/src/persistence/index.ts +38 -0
- package/src/persistence/memory.ts +27 -0
- package/src/persistence/sqlite.ts +111 -0
- package/src/persistence/types.ts +32 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StorageContainerRepository,
|
|
3
|
+
StorageImageRepository,
|
|
4
|
+
StorageSessionRepository,
|
|
5
|
+
createPersistence,
|
|
6
|
+
memoryDriver,
|
|
7
|
+
sqliteDriver
|
|
8
|
+
} from "../chunk-TXESAX3X.js";
|
|
9
|
+
import "../chunk-DGUM43GV.js";
|
|
10
|
+
export {
|
|
11
|
+
StorageContainerRepository,
|
|
12
|
+
StorageImageRepository,
|
|
13
|
+
StorageSessionRepository,
|
|
14
|
+
createPersistence,
|
|
15
|
+
memoryDriver,
|
|
16
|
+
sqliteDriver
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentxjs/node-platform",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./persistence": {
|
|
14
|
+
"types": "./dist/persistence/index.d.ts",
|
|
15
|
+
"import": "./dist/persistence/index.js",
|
|
16
|
+
"default": "./dist/persistence/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./mq": {
|
|
19
|
+
"types": "./dist/mq/index.d.ts",
|
|
20
|
+
"import": "./dist/mq/index.js",
|
|
21
|
+
"default": "./dist/mq/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./network": {
|
|
24
|
+
"types": "./dist/network/index.d.ts",
|
|
25
|
+
"import": "./dist/network/index.js",
|
|
26
|
+
"default": "./dist/network/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "echo 'No tests yet'"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@agentxjs/core": "^2.0.0",
|
|
40
|
+
"commonxjs": "^0.1.1",
|
|
41
|
+
"execa": "9.6.1",
|
|
42
|
+
"rxjs": "^7.8.2",
|
|
43
|
+
"unstorage": "^1.10.2",
|
|
44
|
+
"ws": "^8.18.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/ws": "^8.5.10",
|
|
48
|
+
"typescript": "^5.3.3"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NodeBashProvider - Node.js implementation of BashProvider
|
|
3
|
+
*
|
|
4
|
+
* Uses execa for subprocess execution with proper timeout,
|
|
5
|
+
* error handling, and cross-platform shell support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execa } from "execa";
|
|
9
|
+
import type { BashProvider, BashResult, BashOptions } from "@agentxjs/core/bash";
|
|
10
|
+
import { createLogger } from "commonxjs/logger";
|
|
11
|
+
|
|
12
|
+
const logger = createLogger("node-platform/NodeBashProvider");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Default timeout: 30 seconds
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* NodeBashProvider - Executes shell commands via execa
|
|
21
|
+
*/
|
|
22
|
+
export class NodeBashProvider implements BashProvider {
|
|
23
|
+
readonly type = "child-process";
|
|
24
|
+
|
|
25
|
+
async execute(command: string, options?: BashOptions): Promise<BashResult> {
|
|
26
|
+
const timeout = options?.timeout ?? DEFAULT_TIMEOUT;
|
|
27
|
+
|
|
28
|
+
logger.debug("Executing command", {
|
|
29
|
+
command: command.substring(0, 100),
|
|
30
|
+
cwd: options?.cwd,
|
|
31
|
+
timeout,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const result = await execa({
|
|
35
|
+
shell: true,
|
|
36
|
+
cwd: options?.cwd,
|
|
37
|
+
timeout,
|
|
38
|
+
env: options?.env ? { ...process.env, ...options.env } : undefined,
|
|
39
|
+
reject: false,
|
|
40
|
+
})`${command}`;
|
|
41
|
+
|
|
42
|
+
logger.debug("Command completed", {
|
|
43
|
+
exitCode: result.exitCode,
|
|
44
|
+
stdoutLength: result.stdout.length,
|
|
45
|
+
stderrLength: result.stderr.length,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
stdout: result.stdout,
|
|
50
|
+
stderr: result.stderr,
|
|
51
|
+
exitCode: result.exitCode ?? 1,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @agentxjs/node-platform
|
|
3
|
+
*
|
|
4
|
+
* Node.js platform for AgentX.
|
|
5
|
+
* Provides implementations for persistence, bash, and network.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createNodePlatform } from "@agentxjs/node-platform";
|
|
10
|
+
*
|
|
11
|
+
* const platform = await createNodePlatform({ dataPath: "./data" });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { AgentXPlatform } from "@agentxjs/core/runtime";
|
|
16
|
+
import type { LogLevel } from "commonxjs/logger";
|
|
17
|
+
import { setLoggerFactory, ConsoleLogger } from "commonxjs/logger";
|
|
18
|
+
import { EventBusImpl } from "@agentxjs/core/event";
|
|
19
|
+
import { createPersistence, sqliteDriver } from "./persistence";
|
|
20
|
+
import { NodeBashProvider } from "./bash/NodeBashProvider";
|
|
21
|
+
import { FileLoggerFactory } from "./logger";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Options for creating a Node platform
|
|
26
|
+
*/
|
|
27
|
+
export interface NodePlatformOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Base path for data storage
|
|
30
|
+
* @default "./data"
|
|
31
|
+
*/
|
|
32
|
+
dataPath?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Directory for log files
|
|
36
|
+
* If provided, enables file logging instead of console
|
|
37
|
+
* @example ".agentx/logs"
|
|
38
|
+
*/
|
|
39
|
+
logDir?: string;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Log level
|
|
43
|
+
* @default "debug" for file logging, "info" for console
|
|
44
|
+
*/
|
|
45
|
+
logLevel?: LogLevel;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Deferred platform config - resolved lazily
|
|
50
|
+
*/
|
|
51
|
+
export interface DeferredPlatformConfig {
|
|
52
|
+
readonly __deferred: true;
|
|
53
|
+
readonly options: NodePlatformOptions;
|
|
54
|
+
resolve(): Promise<AgentXPlatform>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create a Node.js platform configuration (deferred initialization)
|
|
59
|
+
*
|
|
60
|
+
* Use this for function-style API. The platform is initialized lazily.
|
|
61
|
+
*
|
|
62
|
+
* @param options - Platform options
|
|
63
|
+
* @returns Deferred platform config
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const server = await createServer({
|
|
68
|
+
* platform: nodePlatform({ dataPath: "./data" }),
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function nodePlatform(options: NodePlatformOptions = {}): DeferredPlatformConfig {
|
|
73
|
+
return {
|
|
74
|
+
__deferred: true,
|
|
75
|
+
options,
|
|
76
|
+
resolve: () => createNodePlatform(options),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a Node.js platform for AgentX (immediate initialization)
|
|
82
|
+
*
|
|
83
|
+
* @param options - Platform options
|
|
84
|
+
* @returns AgentXPlatform instance
|
|
85
|
+
*/
|
|
86
|
+
export async function createNodePlatform(
|
|
87
|
+
options: NodePlatformOptions = {}
|
|
88
|
+
): Promise<AgentXPlatform> {
|
|
89
|
+
const dataPath = options.dataPath ?? "./data";
|
|
90
|
+
|
|
91
|
+
// Configure logging
|
|
92
|
+
if (options.logDir) {
|
|
93
|
+
const loggerFactory = new FileLoggerFactory({
|
|
94
|
+
logDir: options.logDir,
|
|
95
|
+
level: options.logLevel ?? "debug",
|
|
96
|
+
});
|
|
97
|
+
setLoggerFactory(loggerFactory);
|
|
98
|
+
} else if (options.logLevel) {
|
|
99
|
+
setLoggerFactory({
|
|
100
|
+
getLogger: (name: string) => new ConsoleLogger(name, { level: options.logLevel }),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create persistence with SQLite
|
|
105
|
+
const persistence = await createPersistence(sqliteDriver({ path: join(dataPath, "agentx.db") }));
|
|
106
|
+
|
|
107
|
+
// Create bash provider
|
|
108
|
+
const bashProvider = new NodeBashProvider();
|
|
109
|
+
|
|
110
|
+
// Create event bus
|
|
111
|
+
const eventBus = new EventBusImpl();
|
|
112
|
+
|
|
113
|
+
// Create WebSocket factory (uses ws library for Node.js)
|
|
114
|
+
const { createNodeWebSocket } = await import("./network/WebSocketFactory");
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
containerRepository: persistence.containers,
|
|
118
|
+
imageRepository: persistence.images,
|
|
119
|
+
sessionRepository: persistence.sessions,
|
|
120
|
+
eventBus,
|
|
121
|
+
bashProvider,
|
|
122
|
+
webSocketFactory: createNodeWebSocket,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if value is a deferred platform config
|
|
128
|
+
*/
|
|
129
|
+
export function isDeferredPlatform(value: unknown): value is DeferredPlatformConfig {
|
|
130
|
+
return (
|
|
131
|
+
typeof value === "object" &&
|
|
132
|
+
value !== null &&
|
|
133
|
+
"__deferred" in value &&
|
|
134
|
+
(value as DeferredPlatformConfig).__deferred === true
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Re-export persistence
|
|
139
|
+
export * from "./persistence";
|
|
140
|
+
|
|
141
|
+
// Re-export bash
|
|
142
|
+
export { NodeBashProvider } from "./bash/NodeBashProvider";
|
|
143
|
+
|
|
144
|
+
// Re-export mq
|
|
145
|
+
export { SqliteMessageQueue, OffsetGenerator } from "./mq";
|
|
146
|
+
|
|
147
|
+
// Re-export network
|
|
148
|
+
export { WebSocketServer, WebSocketConnection } from "./network";
|
|
149
|
+
|
|
150
|
+
// Re-export logger
|
|
151
|
+
export { FileLoggerFactory, type FileLoggerFactoryOptions } from "./logger";
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileLoggerFactory - File-based logger for Node.js
|
|
3
|
+
*
|
|
4
|
+
* Writes logs to a file instead of console.
|
|
5
|
+
* Useful for TUI applications where console output interferes with the UI.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* tail -f .agentx/logs/app.log
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { appendFileSync, mkdirSync, existsSync } from "node:fs";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import type { Logger, LoggerFactory, LogContext, LogLevel } from "commonxjs/logger";
|
|
14
|
+
|
|
15
|
+
export interface FileLoggerOptions {
|
|
16
|
+
level?: LogLevel;
|
|
17
|
+
timestamps?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class FileLogger implements Logger {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly level: LogLevel;
|
|
23
|
+
private readonly timestamps: boolean;
|
|
24
|
+
private readonly filePath: string;
|
|
25
|
+
private initialized = false;
|
|
26
|
+
|
|
27
|
+
constructor(name: string, filePath: string, options: FileLoggerOptions = {}) {
|
|
28
|
+
this.name = name;
|
|
29
|
+
this.filePath = filePath;
|
|
30
|
+
this.level = options.level ?? "debug";
|
|
31
|
+
this.timestamps = options.timestamps ?? true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private ensureDir(): void {
|
|
35
|
+
if (this.initialized) return;
|
|
36
|
+
const dir = dirname(this.filePath);
|
|
37
|
+
if (!existsSync(dir)) {
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
this.initialized = true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
debug(message: string, context?: LogContext): void {
|
|
44
|
+
if (this.isDebugEnabled()) {
|
|
45
|
+
this.log("DEBUG", message, context);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
info(message: string, context?: LogContext): void {
|
|
50
|
+
if (this.isInfoEnabled()) {
|
|
51
|
+
this.log("INFO", message, context);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
warn(message: string, context?: LogContext): void {
|
|
56
|
+
if (this.isWarnEnabled()) {
|
|
57
|
+
this.log("WARN", message, context);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
error(message: string | Error, context?: LogContext): void {
|
|
62
|
+
if (this.isErrorEnabled()) {
|
|
63
|
+
if (message instanceof Error) {
|
|
64
|
+
this.log("ERROR", message.message, { ...context, stack: message.stack });
|
|
65
|
+
} else {
|
|
66
|
+
this.log("ERROR", message, context);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
isDebugEnabled(): boolean {
|
|
72
|
+
return this.getLevelValue(this.level) <= this.getLevelValue("debug");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
isInfoEnabled(): boolean {
|
|
76
|
+
return this.getLevelValue(this.level) <= this.getLevelValue("info");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
isWarnEnabled(): boolean {
|
|
80
|
+
return this.getLevelValue(this.level) <= this.getLevelValue("warn");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
isErrorEnabled(): boolean {
|
|
84
|
+
return this.getLevelValue(this.level) <= this.getLevelValue("error");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private getLevelValue(level: LogLevel): number {
|
|
88
|
+
const levels: Record<LogLevel, number> = {
|
|
89
|
+
debug: 0,
|
|
90
|
+
info: 1,
|
|
91
|
+
warn: 2,
|
|
92
|
+
error: 3,
|
|
93
|
+
silent: 4,
|
|
94
|
+
};
|
|
95
|
+
return levels[level];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private log(level: string, message: string, context?: LogContext): void {
|
|
99
|
+
this.ensureDir();
|
|
100
|
+
|
|
101
|
+
const parts: string[] = [];
|
|
102
|
+
|
|
103
|
+
if (this.timestamps) {
|
|
104
|
+
parts.push(new Date().toISOString());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
parts.push(level.padEnd(5));
|
|
108
|
+
parts.push(`[${this.name}]`);
|
|
109
|
+
parts.push(message);
|
|
110
|
+
|
|
111
|
+
let logLine = parts.join(" ");
|
|
112
|
+
|
|
113
|
+
if (context && Object.keys(context).length > 0) {
|
|
114
|
+
logLine += " " + JSON.stringify(context);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
logLine += "\n";
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
appendFileSync(this.filePath, logLine);
|
|
121
|
+
} catch {
|
|
122
|
+
// Fallback to stderr if file write fails
|
|
123
|
+
process.stderr.write(`[FileLogger] Failed to write: ${logLine}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* FileLoggerFactory options
|
|
130
|
+
*/
|
|
131
|
+
export interface FileLoggerFactoryOptions {
|
|
132
|
+
/**
|
|
133
|
+
* Directory for log files
|
|
134
|
+
*/
|
|
135
|
+
logDir: string;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Log level
|
|
139
|
+
* @default "debug"
|
|
140
|
+
*/
|
|
141
|
+
level?: LogLevel;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Log file name
|
|
145
|
+
* @default "app.log"
|
|
146
|
+
*/
|
|
147
|
+
filename?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* FileLoggerFactory - Creates FileLogger instances
|
|
152
|
+
*/
|
|
153
|
+
export class FileLoggerFactory implements LoggerFactory {
|
|
154
|
+
private readonly filePath: string;
|
|
155
|
+
private readonly level: LogLevel;
|
|
156
|
+
private readonly loggers: Map<string, FileLogger> = new Map();
|
|
157
|
+
|
|
158
|
+
constructor(options: FileLoggerFactoryOptions) {
|
|
159
|
+
this.filePath = join(options.logDir, options.filename ?? "app.log");
|
|
160
|
+
this.level = options.level ?? "debug";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getLogger(name: string): Logger {
|
|
164
|
+
if (this.loggers.has(name)) {
|
|
165
|
+
return this.loggers.get(name)!;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const logger = new FileLogger(name, this.filePath, {
|
|
169
|
+
level: this.level,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
this.loggers.set(name, logger);
|
|
173
|
+
return logger;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -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
|
+
}
|