iii-sdk 0.0.2-alpha
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 +100 -0
- package/dist/iii-dNb0FMyR.d.mts +418 -0
- package/dist/iii-dNb0FMyR.d.mts.map +1 -0
- package/dist/iii-wiV77JNv.d.cts +418 -0
- package/dist/iii-wiV77JNv.d.cts.map +1 -0
- package/dist/index.cjs +642 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +51 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +51 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +610 -0
- package/dist/index.mjs.map +1 -0
- package/dist/otel-worker-gauges-ELciXZRg.mjs +908 -0
- package/dist/otel-worker-gauges-ELciXZRg.mjs.map +1 -0
- package/dist/otel-worker-gauges-bAp_yKcU.cjs +1064 -0
- package/dist/otel-worker-gauges-bAp_yKcU.cjs.map +1 -0
- package/dist/stream-BB7BoxzH.d.mts +97 -0
- package/dist/stream-BB7BoxzH.d.mts.map +1 -0
- package/dist/stream-BBAV0zc7.d.cts +97 -0
- package/dist/stream-BBAV0zc7.d.cts.map +1 -0
- package/dist/stream.cjs +0 -0
- package/dist/stream.d.cts +2 -0
- package/dist/stream.d.mts +2 -0
- package/dist/stream.mjs +1 -0
- package/dist/telemetry.cjs +33 -0
- package/dist/telemetry.d.cts +112 -0
- package/dist/telemetry.d.cts.map +1 -0
- package/dist/telemetry.d.mts +112 -0
- package/dist/telemetry.d.mts.map +1 -0
- package/dist/telemetry.mjs +3 -0
- package/package.json +59 -0
- package/tests/healthcheck.test.ts +53 -0
- package/tests/setup.ts +48 -0
- package/tests/state.test.ts +149 -0
- package/tests/stream.test.ts +217 -0
- package/tests/types.ts +54 -0
- package/tests/utils.ts +83 -0
- package/vitest.config.ts +10 -0
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iii-sdk",
|
|
3
|
+
"version": "0.0.2-alpha",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"author": "III",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"description": "III SDK for Node.js",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"iii",
|
|
15
|
+
"sdk"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.mjs",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
},
|
|
23
|
+
"./stream": {
|
|
24
|
+
"types": "./dist/stream.d.ts",
|
|
25
|
+
"import": "./dist/stream.mjs",
|
|
26
|
+
"require": "./dist/stream.cjs"
|
|
27
|
+
},
|
|
28
|
+
"./telemetry": {
|
|
29
|
+
"types": "./dist/telemetry.d.ts",
|
|
30
|
+
"import": "./dist/telemetry.mjs",
|
|
31
|
+
"require": "./dist/telemetry.cjs"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@opentelemetry/api": "^1.9.0",
|
|
36
|
+
"@opentelemetry/api-logs": "^0.57.0",
|
|
37
|
+
"@opentelemetry/core": "^1.30.0",
|
|
38
|
+
"@opentelemetry/instrumentation": "^0.57.0",
|
|
39
|
+
"@opentelemetry/otlp-transformer": "^0.57.0",
|
|
40
|
+
"@opentelemetry/resources": "^1.30.0",
|
|
41
|
+
"@opentelemetry/sdk-logs": "^0.57.0",
|
|
42
|
+
"@opentelemetry/sdk-metrics": "^1.30.0",
|
|
43
|
+
"@opentelemetry/sdk-trace-base": "^1.30.0",
|
|
44
|
+
"@opentelemetry/sdk-trace-node": "^1.30.0",
|
|
45
|
+
"@opentelemetry/semantic-conventions": "^1.28.0",
|
|
46
|
+
"ws": "^8.18.3"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/ws": "^8.18.1",
|
|
50
|
+
"tsdown": "^0.17.0",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"vitest": "^2.1.0"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsdown",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:watch": "vitest"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { ApiRequest, ApiResponse } from '../src'
|
|
3
|
+
import { skipIfServerUnavailable } from './setup'
|
|
4
|
+
import { execute, httpRequest, iii } from './utils'
|
|
5
|
+
|
|
6
|
+
describe.skipIf(skipIfServerUnavailable())('Healthcheck Endpoint', () => {
|
|
7
|
+
it('should register a healthcheck function and trigger', async () => {
|
|
8
|
+
const functionId = 'test.healthcheck'
|
|
9
|
+
|
|
10
|
+
iii.registerFunction({ id: functionId }, async (_req: ApiRequest): Promise<ApiResponse> => {
|
|
11
|
+
return {
|
|
12
|
+
status_code: 200,
|
|
13
|
+
body: {
|
|
14
|
+
status: 'healthy',
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
service: 'iii-sdk-test',
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
await execute(async () => {
|
|
22
|
+
const response = await httpRequest('GET', '/health')
|
|
23
|
+
expect(response.status).toBe(404)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const trigger = iii.registerTrigger({
|
|
27
|
+
trigger_type: 'api',
|
|
28
|
+
function_id: functionId,
|
|
29
|
+
config: {
|
|
30
|
+
api_path: 'health',
|
|
31
|
+
http_method: 'GET',
|
|
32
|
+
description: 'Healthcheck endpoint',
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await execute(async () => {
|
|
37
|
+
const response = await httpRequest('GET', '/health')
|
|
38
|
+
|
|
39
|
+
expect(response.status).toBe(200)
|
|
40
|
+
expect(response.data).toHaveProperty('status', 'healthy')
|
|
41
|
+
expect(response.data).toHaveProperty('service', 'iii-sdk-test')
|
|
42
|
+
expect(response.data).toHaveProperty('timestamp')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
trigger.unregister()
|
|
46
|
+
|
|
47
|
+
// there's an issue with unregistering
|
|
48
|
+
// await execute(async () => {
|
|
49
|
+
// const response = await httpRequest('GET', '/health')
|
|
50
|
+
// expect(response.status).toBe(404)
|
|
51
|
+
// })
|
|
52
|
+
})
|
|
53
|
+
})
|
package/tests/setup.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { afterAll, beforeAll } from 'vitest'
|
|
2
|
+
import { checkServerAvailability, iii } from './utils'
|
|
3
|
+
|
|
4
|
+
const isCI = Boolean(process.env.CI)
|
|
5
|
+
const hasExplicitServerUrl = Boolean(process.env.III_BRIDGE_URL || process.env.III_HTTP_URL)
|
|
6
|
+
|
|
7
|
+
let serverAvailable = false
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
if (isCI && !hasExplicitServerUrl) {
|
|
11
|
+
console.warn('Running in CI without explicit server URL. Skipping integration tests.')
|
|
12
|
+
console.warn('To run tests in CI, set III_BRIDGE_URL and III_HTTP_URL environment variables,')
|
|
13
|
+
console.warn('or ensure the III Engine server is started before running tests.')
|
|
14
|
+
serverAvailable = false
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
console.log(
|
|
19
|
+
`Checking server availability at: ${process.env.III_HTTP_URL ?? 'http://localhost:3111'}`,
|
|
20
|
+
)
|
|
21
|
+
serverAvailable = await checkServerAvailability()
|
|
22
|
+
|
|
23
|
+
if (!serverAvailable) {
|
|
24
|
+
console.warn('III Engine server is not available. Skipping integration tests.')
|
|
25
|
+
console.warn(`Expected server at: ${process.env.III_HTTP_URL ?? 'http://localhost:3111'}`)
|
|
26
|
+
console.warn('To run tests locally, start the III Engine server first.')
|
|
27
|
+
} else {
|
|
28
|
+
console.log('III Engine server is available. Running integration tests.')
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const sdk = iii as { shutdown?: () => Promise<void> }
|
|
35
|
+
if (sdk.shutdown) {
|
|
36
|
+
await sdk.shutdown()
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Error shutting down SDK:', error)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export function skipIfServerUnavailable(): boolean {
|
|
44
|
+
if (isCI && !hasExplicitServerUrl) {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { skipIfServerUnavailable } from './setup'
|
|
3
|
+
import { iii } from './utils'
|
|
4
|
+
import type { StateSetResult } from './types'
|
|
5
|
+
|
|
6
|
+
type TestData = {
|
|
7
|
+
name?: string
|
|
8
|
+
value: number
|
|
9
|
+
updated?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe.skipIf(skipIfServerUnavailable())('State Operations', () => {
|
|
13
|
+
const testGroupId = 'test-group'
|
|
14
|
+
const testItemId = 'test-item'
|
|
15
|
+
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
await iii
|
|
18
|
+
.call('state.delete', { group_id: testGroupId, item_id: testItemId })
|
|
19
|
+
.catch(() => void 0)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('state.set', () => {
|
|
23
|
+
it('should set a new state item', async () => {
|
|
24
|
+
const testData = {
|
|
25
|
+
name: 'Test Item',
|
|
26
|
+
value: 42,
|
|
27
|
+
metadata: { created: new Date().toISOString() },
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = await iii.call('state.set', {
|
|
31
|
+
group_id: testGroupId,
|
|
32
|
+
item_id: testItemId,
|
|
33
|
+
data: testData,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(result).toBeDefined()
|
|
37
|
+
expect(result).toEqual({ old_value: null, new_value: testData })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should overwrite an existing state item', async () => {
|
|
41
|
+
const initialData: TestData = { value: 1 }
|
|
42
|
+
const updatedData: TestData = { value: 2, updated: true }
|
|
43
|
+
|
|
44
|
+
await iii.call('state.set', {
|
|
45
|
+
group_id: testGroupId,
|
|
46
|
+
item_id: testItemId,
|
|
47
|
+
data: initialData,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const result: StateSetResult = await iii.call('state.set', {
|
|
51
|
+
group_id: testGroupId,
|
|
52
|
+
item_id: testItemId,
|
|
53
|
+
data: updatedData,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
expect(result.old_value).toEqual(initialData)
|
|
57
|
+
expect(result.new_value).toEqual(updatedData)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('state.get', () => {
|
|
62
|
+
it('should get an existing state item', async () => {
|
|
63
|
+
const testData: TestData = { name: 'Test', value: 100 }
|
|
64
|
+
|
|
65
|
+
await iii.call('state.set', {
|
|
66
|
+
group_id: testGroupId,
|
|
67
|
+
item_id: testItemId,
|
|
68
|
+
data: testData,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const result: TestData = await iii.call('state.get', {
|
|
72
|
+
group_id: testGroupId,
|
|
73
|
+
item_id: testItemId,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
expect(result).toBeDefined()
|
|
77
|
+
expect(result).toEqual(testData)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should return null for non-existent item', async () => {
|
|
81
|
+
const result = await iii.call('state.get', {
|
|
82
|
+
group_id: testGroupId,
|
|
83
|
+
item_id: 'non-existent-item',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(result).toBeUndefined()
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('state.delete', () => {
|
|
91
|
+
it('should delete an existing state item', async () => {
|
|
92
|
+
await iii.call('state.set', {
|
|
93
|
+
group_id: testGroupId,
|
|
94
|
+
item_id: testItemId,
|
|
95
|
+
data: { test: true },
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
await iii.call('state.delete', {
|
|
99
|
+
group_id: testGroupId,
|
|
100
|
+
item_id: testItemId,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const result = await iii.call('state.get', {
|
|
104
|
+
group_id: testGroupId,
|
|
105
|
+
item_id: testItemId,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
expect(result).toBeUndefined()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should handle deleting non-existent item gracefully', async () => {
|
|
112
|
+
await expect(
|
|
113
|
+
iii.call('state.delete', {
|
|
114
|
+
group_id: testGroupId,
|
|
115
|
+
item_id: 'non-existent',
|
|
116
|
+
}),
|
|
117
|
+
).resolves.not.toThrow()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('state.list', () => {
|
|
122
|
+
it('should get all items in a group', async () => {
|
|
123
|
+
type TestDataWithId = TestData & { id: string }
|
|
124
|
+
|
|
125
|
+
const groupId = `state-${Date.now()}`
|
|
126
|
+
const items: TestDataWithId[] = [
|
|
127
|
+
{ id: 'state-item1', value: 1 },
|
|
128
|
+
{ id: 'state-item2', value: 2 },
|
|
129
|
+
{ id: 'state-item3', value: 3 },
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
// Set multiple items
|
|
133
|
+
for (const item of items) {
|
|
134
|
+
await iii.call('state.set', {
|
|
135
|
+
group_id: groupId,
|
|
136
|
+
item_id: item.id,
|
|
137
|
+
data: item,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result: TestDataWithId[] = await iii.call('state.list', { group_id: groupId })
|
|
142
|
+
const sort = (a: TestDataWithId, b: TestDataWithId) => a.id.localeCompare(b.id)
|
|
143
|
+
|
|
144
|
+
expect(Array.isArray(result)).toBe(true)
|
|
145
|
+
expect(result.length).toBeGreaterThanOrEqual(items.length)
|
|
146
|
+
expect(result.sort(sort)).toEqual(items.sort(sort))
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
})
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { skipIfServerUnavailable } from './setup'
|
|
3
|
+
import { iii, sleep } from './utils'
|
|
4
|
+
import type { StreamSetInput, StreamSetResult } from '../src/stream'
|
|
5
|
+
|
|
6
|
+
type TestData = {
|
|
7
|
+
name?: string
|
|
8
|
+
value: number
|
|
9
|
+
updated?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe.skipIf(skipIfServerUnavailable())('Stream Operations', () => {
|
|
13
|
+
const testStreamName = 'test-stream'
|
|
14
|
+
const testGroupId = 'test-group'
|
|
15
|
+
const testItemId = 'test-item'
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
await iii
|
|
19
|
+
.call('stream.delete', {
|
|
20
|
+
stream_name: testStreamName,
|
|
21
|
+
group_id: testGroupId,
|
|
22
|
+
item_id: testItemId,
|
|
23
|
+
})
|
|
24
|
+
.catch(() => void 0)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('stream.set', () => {
|
|
28
|
+
it('should set a new stream item', async () => {
|
|
29
|
+
const testData = {
|
|
30
|
+
name: 'Test Item',
|
|
31
|
+
value: 42,
|
|
32
|
+
metadata: { created: new Date().toISOString() },
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = await iii.call<StreamSetInput, StreamSetResult<TestData>>('stream.set', {
|
|
36
|
+
stream_name: testStreamName,
|
|
37
|
+
group_id: testGroupId,
|
|
38
|
+
item_id: testItemId,
|
|
39
|
+
data: testData,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(result).toBeDefined()
|
|
43
|
+
expect(result).toEqual({ old_value: null, new_value: testData })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should overwrite an existing stream item', async () => {
|
|
47
|
+
const initialData: TestData = { value: 1 }
|
|
48
|
+
const updatedData: TestData = { value: 2, updated: true }
|
|
49
|
+
|
|
50
|
+
await iii.call('stream.set', {
|
|
51
|
+
stream_name: testStreamName,
|
|
52
|
+
group_id: testGroupId,
|
|
53
|
+
item_id: testItemId,
|
|
54
|
+
data: initialData,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const result: StreamSetResult<any> = await iii.call('stream.set', {
|
|
58
|
+
stream_name: testStreamName,
|
|
59
|
+
group_id: testGroupId,
|
|
60
|
+
item_id: testItemId,
|
|
61
|
+
data: updatedData,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(result.old_value).toEqual(initialData)
|
|
65
|
+
expect(result.new_value).toEqual(updatedData)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('stream.get', () => {
|
|
70
|
+
it('should get an existing stream item', async () => {
|
|
71
|
+
const testData: TestData = { name: 'Test', value: 100 }
|
|
72
|
+
|
|
73
|
+
await iii.call('stream.set', {
|
|
74
|
+
stream_name: testStreamName,
|
|
75
|
+
group_id: testGroupId,
|
|
76
|
+
item_id: testItemId,
|
|
77
|
+
data: testData,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const result: TestData = await iii.call('stream.get', {
|
|
81
|
+
stream_name: testStreamName,
|
|
82
|
+
group_id: testGroupId,
|
|
83
|
+
item_id: testItemId,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(result).toBeDefined()
|
|
87
|
+
expect(result).toEqual(testData)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should return null for non-existent item', async () => {
|
|
91
|
+
const result = await iii.call('stream.get', {
|
|
92
|
+
stream_name: testStreamName,
|
|
93
|
+
group_id: testGroupId,
|
|
94
|
+
item_id: 'non-existent-item',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(result).toBeUndefined()
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('stream.delete', () => {
|
|
102
|
+
it('should delete an existing stream item', async () => {
|
|
103
|
+
await iii.call('stream.set', {
|
|
104
|
+
stream_name: testStreamName,
|
|
105
|
+
group_id: testGroupId,
|
|
106
|
+
item_id: testItemId,
|
|
107
|
+
data: { test: true },
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
await iii.call('stream.delete', {
|
|
111
|
+
stream_name: testStreamName,
|
|
112
|
+
group_id: testGroupId,
|
|
113
|
+
item_id: testItemId,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const result = await iii.call('stream.get', {
|
|
117
|
+
stream_name: testStreamName,
|
|
118
|
+
group_id: testGroupId,
|
|
119
|
+
item_id: testItemId,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
expect(result).toBeUndefined()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should handle deleting non-existent item gracefully', async () => {
|
|
126
|
+
await expect(
|
|
127
|
+
iii.call('stream.delete', {
|
|
128
|
+
stream_name: testStreamName,
|
|
129
|
+
group_id: testGroupId,
|
|
130
|
+
item_id: 'non-existent',
|
|
131
|
+
}),
|
|
132
|
+
).resolves.not.toThrow()
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('stream.list', () => {
|
|
137
|
+
it('should get all items in a group', async () => {
|
|
138
|
+
type TestDataWithId = TestData & { id: string }
|
|
139
|
+
|
|
140
|
+
const groupId = `stream-${Date.now()}`
|
|
141
|
+
const items: TestDataWithId[] = [
|
|
142
|
+
{ id: 'stream-item1', value: 1 },
|
|
143
|
+
{ id: 'stream-item2', value: 2 },
|
|
144
|
+
{ id: 'stream-item3', value: 3 },
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
// Set multiple items
|
|
148
|
+
for (const item of items) {
|
|
149
|
+
await iii.call('stream.set', {
|
|
150
|
+
stream_name: testStreamName,
|
|
151
|
+
group_id: groupId,
|
|
152
|
+
item_id: item.id,
|
|
153
|
+
data: item,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const result: TestDataWithId[] = await iii.call('stream.list', {
|
|
158
|
+
stream_name: testStreamName,
|
|
159
|
+
group_id: groupId,
|
|
160
|
+
})
|
|
161
|
+
const sort = (a: TestDataWithId, b: TestDataWithId) => a.id.localeCompare(b.id)
|
|
162
|
+
|
|
163
|
+
expect(Array.isArray(result)).toBe(true)
|
|
164
|
+
expect(result.length).toBeGreaterThanOrEqual(items.length)
|
|
165
|
+
expect(result.sort(sort)).toEqual(items.sort(sort))
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('stream custom operations', () => {
|
|
170
|
+
it('should perform a custom operation on a stream item', async () => {
|
|
171
|
+
const testStreamName = `test-stream-${Date.now()}`
|
|
172
|
+
const state: Map<string, TestData> = new Map()
|
|
173
|
+
|
|
174
|
+
iii.createStream(testStreamName, {
|
|
175
|
+
get: async input => state.get(`${input.group_id}::${input.item_id}`),
|
|
176
|
+
set: async input => {
|
|
177
|
+
const key = `${input.group_id}::${input.item_id}`
|
|
178
|
+
const oldValue = state.get(key)
|
|
179
|
+
state.set(key, input.data)
|
|
180
|
+
|
|
181
|
+
return { old_value: oldValue, new_value: input.data }
|
|
182
|
+
},
|
|
183
|
+
delete: async input => {
|
|
184
|
+
const oldValue = state.get(`${input.group_id}::${input.item_id}`)
|
|
185
|
+
state.delete(`${input.group_id}::${input.item_id}`)
|
|
186
|
+
return { old_value: oldValue }
|
|
187
|
+
},
|
|
188
|
+
list: async input => {
|
|
189
|
+
return Array.from(state.keys())
|
|
190
|
+
.filter(key => key.startsWith(`${input.group_id}::`))
|
|
191
|
+
.map(key => state.get(key))
|
|
192
|
+
},
|
|
193
|
+
listGroups: async () => Array.from(state.keys()),
|
|
194
|
+
update: async () => {
|
|
195
|
+
throw new Error('Not implemented')
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
await sleep(1_000)
|
|
200
|
+
|
|
201
|
+
const testData: TestData = { name: 'Test', value: 100 }
|
|
202
|
+
const getArgs = {
|
|
203
|
+
stream_name: testStreamName,
|
|
204
|
+
group_id: testGroupId,
|
|
205
|
+
item_id: testItemId,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await iii.call('stream.set', { ...getArgs, data: testData })
|
|
209
|
+
|
|
210
|
+
expect(state.get(`${testGroupId}::${testItemId}`)).toEqual(testData)
|
|
211
|
+
|
|
212
|
+
await expect(iii.call('stream.get', getArgs)).resolves.toEqual(testData)
|
|
213
|
+
await iii.call('stream.delete', getArgs)
|
|
214
|
+
await expect(iii.call('stream.get', getArgs)).resolves.toEqual(undefined)
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
})
|
package/tests/types.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { UpdateOp, StreamSetResult } from '../src/stream'
|
|
2
|
+
|
|
3
|
+
export interface StateSetResult {
|
|
4
|
+
// biome-ignore lint/suspicious/noExplicitAny: any is fine here
|
|
5
|
+
old_value?: any
|
|
6
|
+
// biome-ignore lint/suspicious/noExplicitAny: any is fine here
|
|
7
|
+
new_value?: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type { StreamSetResult }
|
|
11
|
+
|
|
12
|
+
export interface StateSetInput {
|
|
13
|
+
group_id: string
|
|
14
|
+
item_id: string
|
|
15
|
+
// biome-ignore lint/suspicious/noExplicitAny: any is fine here
|
|
16
|
+
data: any
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface StateGetInput {
|
|
20
|
+
group_id: string
|
|
21
|
+
item_id: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface StateDeleteInput {
|
|
25
|
+
group_id: string
|
|
26
|
+
item_id: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StateUpdateInput {
|
|
30
|
+
group_id: string
|
|
31
|
+
item_id: string
|
|
32
|
+
ops: UpdateOp[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StateGetGroupInput {
|
|
36
|
+
group_id: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export enum StateEventType {
|
|
40
|
+
Created = 'state:created',
|
|
41
|
+
Updated = 'state:updated',
|
|
42
|
+
Deleted = 'state:deleted',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface StateEventData {
|
|
46
|
+
type: string
|
|
47
|
+
event_type: StateEventType
|
|
48
|
+
group_id: string
|
|
49
|
+
item_id: string
|
|
50
|
+
// biome-ignore lint/suspicious/noExplicitAny: any is fine here
|
|
51
|
+
old_value?: any
|
|
52
|
+
// biome-ignore lint/suspicious/noExplicitAny: any is fine here
|
|
53
|
+
new_value?: any
|
|
54
|
+
}
|
package/tests/utils.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// import { iii } from 'iii-sdk'
|
|
2
|
+
import { init } from '../src/index'
|
|
3
|
+
|
|
4
|
+
const ENGINE_WS_URL = process.env.III_BRIDGE_URL ?? 'ws://localhost:49134'
|
|
5
|
+
const ENGINE_HTTP_URL = process.env.III_HTTP_URL ?? 'http://localhost:3111'
|
|
6
|
+
const RETRY_LIMIT = 100
|
|
7
|
+
const DELAY_MS = 100
|
|
8
|
+
|
|
9
|
+
export const engineWsUrl = ENGINE_WS_URL
|
|
10
|
+
export const engineHttpUrl = ENGINE_HTTP_URL
|
|
11
|
+
|
|
12
|
+
export const iii = init(engineWsUrl, {
|
|
13
|
+
reconnectionConfig: {
|
|
14
|
+
maxRetries: 3,
|
|
15
|
+
initialDelayMs: 100,
|
|
16
|
+
maxDelayMs: 1000,
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export async function checkServerAvailability(): Promise<boolean> {
|
|
21
|
+
try {
|
|
22
|
+
const controller = new AbortController()
|
|
23
|
+
const timeoutId = setTimeout(() => controller.abort(), 2000)
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const response = await fetch(ENGINE_HTTP_URL, {
|
|
27
|
+
method: 'GET',
|
|
28
|
+
signal: controller.signal,
|
|
29
|
+
})
|
|
30
|
+
clearTimeout(timeoutId)
|
|
31
|
+
return response.status < 500
|
|
32
|
+
} catch {
|
|
33
|
+
clearTimeout(timeoutId)
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function httpRequest(
|
|
42
|
+
method: string,
|
|
43
|
+
path: string,
|
|
44
|
+
// biome-ignore lint/suspicious/noExplicitAny: any is fine here
|
|
45
|
+
body?: any,
|
|
46
|
+
// biome-ignore lint/suspicious/noExplicitAny: any is fine here
|
|
47
|
+
): Promise<{ status: number; data: any }> {
|
|
48
|
+
const url = `${engineHttpUrl}${path}`
|
|
49
|
+
const options: RequestInit = { method, headers: { 'Content-Type': 'application/json' } }
|
|
50
|
+
|
|
51
|
+
if (body) {
|
|
52
|
+
options.body = JSON.stringify(body)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const response = await fetch(url, options)
|
|
56
|
+
const data = await response.json().catch(() => ({}))
|
|
57
|
+
|
|
58
|
+
return { status: response.status, data }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function sleep(duration: number): Promise<void> {
|
|
62
|
+
return new Promise(resolve => {
|
|
63
|
+
setTimeout(() => resolve(), duration)
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function execute<T>(operation: () => Promise<T>): Promise<T> {
|
|
68
|
+
let currentAttempt = 0
|
|
69
|
+
|
|
70
|
+
while (true) {
|
|
71
|
+
try {
|
|
72
|
+
return await operation()
|
|
73
|
+
} catch (err) {
|
|
74
|
+
currentAttempt++
|
|
75
|
+
|
|
76
|
+
if (currentAttempt >= RETRY_LIMIT) {
|
|
77
|
+
throw err
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await sleep(DELAY_MS)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|