ts-procedures 5.2.0 → 5.4.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 +150 -0
- package/agent_config/bin/postinstall.mjs +105 -0
- package/agent_config/bin/setup.mjs +286 -0
- package/agent_config/claude-code/.claude-plugin/plugin.json +5 -0
- package/agent_config/claude-code/agents/ts-procedures-architect.md +188 -0
- package/agent_config/claude-code/skills/guide/SKILL.md +142 -0
- package/agent_config/claude-code/skills/guide/anti-patterns.md +608 -0
- package/agent_config/claude-code/skills/guide/api-reference.md +696 -0
- package/agent_config/claude-code/skills/guide/patterns.md +727 -0
- package/agent_config/claude-code/skills/review/SKILL.md +53 -0
- package/agent_config/claude-code/skills/review/checklist.md +163 -0
- package/agent_config/claude-code/skills/scaffold/SKILL.md +56 -0
- package/agent_config/claude-code/skills/scaffold/templates/express-rpc.md +134 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-rpc.md +139 -0
- package/agent_config/claude-code/skills/scaffold/templates/hono-stream.md +134 -0
- package/agent_config/claude-code/skills/scaffold/templates/procedure.md +77 -0
- package/agent_config/claude-code/skills/scaffold/templates/stream-procedure.md +113 -0
- package/agent_config/copilot/copilot-instructions.md +290 -0
- package/agent_config/cursor/cursorrules +290 -0
- package/agent_config/lib/install-claude.mjs +109 -0
- package/build/implementations/http/hono-api/index.d.ts +102 -0
- package/build/implementations/http/hono-api/index.js +339 -0
- package/build/implementations/http/hono-api/index.js.map +1 -0
- package/build/implementations/http/hono-api/index.test.d.ts +1 -0
- package/build/implementations/http/hono-api/index.test.js +983 -0
- package/build/implementations/http/hono-api/index.test.js.map +1 -0
- package/build/implementations/http/hono-api/types.d.ts +13 -0
- package/build/implementations/http/hono-api/types.js +2 -0
- package/build/implementations/http/hono-api/types.js.map +1 -0
- package/build/implementations/types.d.ts +44 -0
- package/build/index.d.ts +28 -6
- package/build/index.js +28 -0
- package/build/index.js.map +1 -1
- package/build/schema/compute-schema.d.ts +5 -0
- package/build/schema/compute-schema.js +8 -1
- package/build/schema/compute-schema.js.map +1 -1
- package/build/schema/parser.d.ts +6 -5
- package/build/schema/parser.js +54 -0
- package/build/schema/parser.js.map +1 -1
- package/package.json +14 -4
- package/src/implementations/http/README.md +45 -2
- package/src/implementations/http/hono-api/index.test.ts +1328 -0
- package/src/implementations/http/hono-api/index.ts +461 -0
- package/src/implementations/http/hono-api/types.ts +16 -0
- package/src/implementations/types.ts +52 -0
- package/src/index.ts +87 -10
- package/src/schema/compute-schema.ts +23 -2
- package/src/schema/parser.ts +70 -3
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Hono RPC Template: {{Name}}
|
|
2
|
+
|
|
3
|
+
## Implementation — `{{Name}}.rpc.ts`
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Procedures, ProcedureError, ProcedureValidationError } from 'ts-procedures'
|
|
7
|
+
import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
|
|
8
|
+
import type { RPCConfig } from 'ts-procedures/http'
|
|
9
|
+
import { Type } from 'typebox'
|
|
10
|
+
|
|
11
|
+
// ─── Context ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
type {{Name}}Context = {
|
|
14
|
+
userId: string
|
|
15
|
+
requestId: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Procedures ───────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const RPC = Procedures<{{Name}}Context, RPCConfig>()
|
|
21
|
+
|
|
22
|
+
export const { GetItem } = RPC.Create(
|
|
23
|
+
'GetItem',
|
|
24
|
+
{
|
|
25
|
+
scope: '{{name}}', // TODO: set URL scope
|
|
26
|
+
version: 1,
|
|
27
|
+
description: 'Fetch item by ID',
|
|
28
|
+
schema: {
|
|
29
|
+
params: Type.Object({
|
|
30
|
+
id: Type.String(),
|
|
31
|
+
}),
|
|
32
|
+
returnType: Type.Object({
|
|
33
|
+
id: Type.String(),
|
|
34
|
+
name: Type.String(),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
async (ctx, params) => {
|
|
39
|
+
// TODO: implement
|
|
40
|
+
return { id: params.id, name: 'Example' }
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
export const { ListItems } = RPC.Create(
|
|
45
|
+
'ListItems',
|
|
46
|
+
{
|
|
47
|
+
scope: '{{name}}',
|
|
48
|
+
version: 1,
|
|
49
|
+
description: 'List all items',
|
|
50
|
+
schema: {
|
|
51
|
+
params: Type.Object({
|
|
52
|
+
page: Type.Optional(Type.Number()),
|
|
53
|
+
limit: Type.Optional(Type.Number()),
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
async (ctx, params) => {
|
|
58
|
+
// TODO: implement
|
|
59
|
+
return { items: [], total: 0 }
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// ─── Hono App Builder ─────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
export const {{name}}App = new HonoRPCAppBuilder({
|
|
66
|
+
pathPrefix: '/api',
|
|
67
|
+
onError: (procedure, c, error) => {
|
|
68
|
+
if (error instanceof ProcedureValidationError) {
|
|
69
|
+
return c.json({
|
|
70
|
+
error: error.message,
|
|
71
|
+
details: error.errors,
|
|
72
|
+
procedure: error.procedureName,
|
|
73
|
+
}, 400)
|
|
74
|
+
} else if (error instanceof ProcedureError) {
|
|
75
|
+
return c.json({
|
|
76
|
+
error: error.message,
|
|
77
|
+
meta: error.meta,
|
|
78
|
+
procedure: error.procedureName,
|
|
79
|
+
}, 422)
|
|
80
|
+
}
|
|
81
|
+
return c.json({ error: 'Internal server error' }, 500)
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
.register(RPC, (c) => ({
|
|
85
|
+
userId: c.req.header('x-user-id') || 'anonymous',
|
|
86
|
+
requestId: c.req.header('x-request-id') || crypto.randomUUID(),
|
|
87
|
+
}))
|
|
88
|
+
.build()
|
|
89
|
+
|
|
90
|
+
// Route map:
|
|
91
|
+
// POST /api/{{name}}/get-item/1
|
|
92
|
+
// POST /api/{{name}}/list-items/1
|
|
93
|
+
|
|
94
|
+
// Documentation: {{name}}App.docs
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Test — `{{Name}}.rpc.test.ts`
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { describe, test, expect } from 'vitest'
|
|
101
|
+
import { {{name}}App } from './{{Name}}.rpc'
|
|
102
|
+
|
|
103
|
+
describe('{{Name}} RPC', () => {
|
|
104
|
+
test('GET item returns expected result', async () => {
|
|
105
|
+
const res = await {{name}}App.request('/api/{{name}}/get-item/1', {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'Content-Type': 'application/json' },
|
|
108
|
+
body: JSON.stringify({ id: 'item-1' }),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
expect(res.status).toBe(200)
|
|
112
|
+
const body = await res.json()
|
|
113
|
+
expect(body).toEqual({ id: 'item-1', name: 'Example' })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('returns 400 for invalid params', async () => {
|
|
117
|
+
const res = await {{name}}App.request('/api/{{name}}/get-item/1', {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify({}),
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(res.status).toBe(400)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('LIST items returns array', async () => {
|
|
127
|
+
const res = await {{name}}App.request('/api/{{name}}/list-items/1', {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'Content-Type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ page: 1, limit: 10 }),
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
expect(res.status).toBe(200)
|
|
134
|
+
const body = await res.json()
|
|
135
|
+
expect(body).toHaveProperty('items')
|
|
136
|
+
expect(body).toHaveProperty('total')
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
```
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Hono Stream Template: {{Name}}
|
|
2
|
+
|
|
3
|
+
## Implementation — `{{Name}}.stream-rpc.ts`
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Procedures } from 'ts-procedures'
|
|
7
|
+
import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
|
|
8
|
+
import type { RPCConfig } from 'ts-procedures/http'
|
|
9
|
+
import { Type } from 'typebox'
|
|
10
|
+
|
|
11
|
+
// ─── Context ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
type {{Name}}Context = {
|
|
14
|
+
userId: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Stream Procedures ────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const StreamRPC = Procedures<{{Name}}Context, RPCConfig>()
|
|
20
|
+
|
|
21
|
+
export const { {{Name}}Stream } = StreamRPC.CreateStream(
|
|
22
|
+
'{{Name}}Stream',
|
|
23
|
+
{
|
|
24
|
+
scope: '{{name}}', // TODO: set URL scope
|
|
25
|
+
version: 1,
|
|
26
|
+
description: 'TODO: describe what this streams',
|
|
27
|
+
schema: {
|
|
28
|
+
params: Type.Object({
|
|
29
|
+
// TODO: define input parameters
|
|
30
|
+
channel: Type.String(),
|
|
31
|
+
}),
|
|
32
|
+
yieldType: Type.Object({
|
|
33
|
+
// TODO: define shape of each yielded value
|
|
34
|
+
type: Type.String(),
|
|
35
|
+
payload: Type.Any(),
|
|
36
|
+
timestamp: Type.Number(),
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
async function* (ctx, params) {
|
|
41
|
+
// ctx.signal is ALWAYS present in stream handlers
|
|
42
|
+
|
|
43
|
+
// TODO: implement streaming logic
|
|
44
|
+
let counter = 0
|
|
45
|
+
while (!ctx.signal.aborted) {
|
|
46
|
+
const event = await pollForEvents(params.channel, { signal: ctx.signal })
|
|
47
|
+
|
|
48
|
+
// Use sse() to attach SSE metadata (event type, id, retry)
|
|
49
|
+
yield sse(
|
|
50
|
+
{ type: event.type, payload: event.data, timestamp: Date.now() },
|
|
51
|
+
{ event: event.type, id: String(counter++) }
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// Placeholder — replace with real event source
|
|
58
|
+
async function pollForEvents(channel: string, opts?: { signal?: AbortSignal }) {
|
|
59
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
60
|
+
return { type: 'update', data: { value: Math.random() } }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Hono Stream App Builder ──────────────────────────────
|
|
64
|
+
|
|
65
|
+
export const {{name}}StreamApp = new HonoStreamAppBuilder({
|
|
66
|
+
pathPrefix: '/api',
|
|
67
|
+
defaultStreamMode: 'sse', // or 'text' for newline-delimited JSON
|
|
68
|
+
onPreStreamError: (procedure, c, error) => {
|
|
69
|
+
return c.json({ error: error.message }, 400)
|
|
70
|
+
},
|
|
71
|
+
onMidStreamError: (procedure, c, error) => ({
|
|
72
|
+
data: { type: 'error', payload: { message: error.message }, timestamp: Date.now() },
|
|
73
|
+
closeStream: true,
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
.register(StreamRPC, (c) => ({
|
|
77
|
+
userId: c.req.header('x-user-id') || 'anonymous',
|
|
78
|
+
}))
|
|
79
|
+
.build()
|
|
80
|
+
|
|
81
|
+
// Routes:
|
|
82
|
+
// GET /api/{{name}}/{{name}}-stream/1?channel=test
|
|
83
|
+
// POST /api/{{name}}/{{name}}-stream/1 (body: { channel: "test" })
|
|
84
|
+
|
|
85
|
+
// SSE output per yield:
|
|
86
|
+
// event: {{Name}}Stream
|
|
87
|
+
// id: 0
|
|
88
|
+
// data: {"type":"update","payload":{...},"timestamp":1234567890}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Test — `{{Name}}.stream-rpc.test.ts`
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { describe, test, expect } from 'vitest'
|
|
95
|
+
import { {{name}}StreamApp } from './{{Name}}.stream-rpc'
|
|
96
|
+
|
|
97
|
+
describe('{{Name}} Stream RPC', () => {
|
|
98
|
+
test('GET returns SSE stream', async () => {
|
|
99
|
+
const res = await {{name}}StreamApp.request(
|
|
100
|
+
'/api/{{name}}/{{name}}-stream/1?channel=test'
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
expect(res.status).toBe(200)
|
|
104
|
+
expect(res.headers.get('content-type')).toContain('text/event-stream')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('POST returns SSE stream', async () => {
|
|
108
|
+
const res = await {{name}}StreamApp.request(
|
|
109
|
+
'/api/{{name}}/{{name}}-stream/1',
|
|
110
|
+
{
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify({ channel: 'test' }),
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
expect(res.status).toBe(200)
|
|
118
|
+
expect(res.headers.get('content-type')).toContain('text/event-stream')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('returns 400 for invalid params', async () => {
|
|
122
|
+
const res = await {{name}}StreamApp.request(
|
|
123
|
+
'/api/{{name}}/{{name}}-stream/1',
|
|
124
|
+
{
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: { 'Content-Type': 'application/json' },
|
|
127
|
+
body: JSON.stringify({}),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
expect(res.status).toBe(400)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Procedure Template: {{Name}}
|
|
2
|
+
|
|
3
|
+
## Implementation — `{{Name}}.procedure.ts`
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Procedures } from 'ts-procedures'
|
|
7
|
+
import { Type } from 'typebox'
|
|
8
|
+
|
|
9
|
+
// Define context type (adjust to match your app's context)
|
|
10
|
+
type AppContext = {
|
|
11
|
+
userId: string
|
|
12
|
+
signal?: AbortSignal
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { Create } = Procedures<AppContext>()
|
|
16
|
+
|
|
17
|
+
export const { {{Name}}, procedure: {{Name}}Procedure, info: {{Name}}Info } = Create(
|
|
18
|
+
'{{Name}}',
|
|
19
|
+
{
|
|
20
|
+
description: 'TODO: describe what {{Name}} does',
|
|
21
|
+
schema: {
|
|
22
|
+
params: Type.Object({
|
|
23
|
+
// TODO: define input parameters
|
|
24
|
+
id: Type.String(),
|
|
25
|
+
}),
|
|
26
|
+
returnType: Type.Object({
|
|
27
|
+
// TODO: define return shape (documentation only, not validated at runtime)
|
|
28
|
+
id: Type.String(),
|
|
29
|
+
// ...fields
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
async (ctx, params) => {
|
|
34
|
+
// params.id is guaranteed to be a string (validated by AJV)
|
|
35
|
+
|
|
36
|
+
// Use ctx.error() for business logic errors
|
|
37
|
+
// throw ctx.error('Not found', { code: 'NOT_FOUND', id: params.id })
|
|
38
|
+
|
|
39
|
+
// Pass ctx.signal to downstream async calls
|
|
40
|
+
// const data = await fetch(url, { signal: ctx.signal })
|
|
41
|
+
|
|
42
|
+
// TODO: implement handler logic
|
|
43
|
+
return { id: params.id }
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Test — `{{Name}}.procedure.test.ts`
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { describe, test, expect } from 'vitest'
|
|
52
|
+
import { ProcedureError, ProcedureValidationError } from 'ts-procedures'
|
|
53
|
+
import { {{Name}} } from './{{Name}}.procedure'
|
|
54
|
+
|
|
55
|
+
const mockContext = { userId: 'test-user' }
|
|
56
|
+
|
|
57
|
+
describe('{{Name}}', () => {
|
|
58
|
+
test('returns expected result for valid params', async () => {
|
|
59
|
+
const result = await {{Name}}(mockContext, { id: 'test-id' })
|
|
60
|
+
expect(result).toBeDefined()
|
|
61
|
+
expect(result.id).toBe('test-id')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('throws ProcedureValidationError for invalid params', async () => {
|
|
65
|
+
await expect(
|
|
66
|
+
{{Name}}(mockContext, {} as any)
|
|
67
|
+
).rejects.toThrow(ProcedureValidationError)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('throws ProcedureError for business logic failures', async () => {
|
|
71
|
+
// TODO: test ctx.error() scenarios
|
|
72
|
+
// await expect(
|
|
73
|
+
// {{Name}}(mockContext, { id: 'invalid' })
|
|
74
|
+
// ).rejects.toThrow(ProcedureError)
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Stream Procedure Template: {{Name}}
|
|
2
|
+
|
|
3
|
+
## Implementation — `{{Name}}.stream.ts`
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { Procedures } from 'ts-procedures'
|
|
7
|
+
import { Type } from 'typebox'
|
|
8
|
+
|
|
9
|
+
type AppContext = {
|
|
10
|
+
userId: string
|
|
11
|
+
signal?: AbortSignal
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { CreateStream } = Procedures<AppContext>()
|
|
15
|
+
|
|
16
|
+
export const { {{Name}}, procedure: {{Name}}Procedure, info: {{Name}}Info } = CreateStream(
|
|
17
|
+
'{{Name}}',
|
|
18
|
+
{
|
|
19
|
+
description: 'TODO: describe what {{Name}} streams',
|
|
20
|
+
schema: {
|
|
21
|
+
params: Type.Object({
|
|
22
|
+
// TODO: define input parameters
|
|
23
|
+
channel: Type.String(),
|
|
24
|
+
}),
|
|
25
|
+
yieldType: Type.Object({
|
|
26
|
+
// TODO: define shape of each yielded value
|
|
27
|
+
id: Type.String(),
|
|
28
|
+
data: Type.Any(),
|
|
29
|
+
timestamp: Type.Number(),
|
|
30
|
+
}),
|
|
31
|
+
},
|
|
32
|
+
// Set to true to validate each yielded value against yieldType schema
|
|
33
|
+
// validateYields: false,
|
|
34
|
+
},
|
|
35
|
+
async function* (ctx, params) {
|
|
36
|
+
// ctx.signal is ALWAYS present in stream handlers (guaranteed AbortSignal)
|
|
37
|
+
|
|
38
|
+
// TODO: implement streaming logic
|
|
39
|
+
// Example: poll-based streaming
|
|
40
|
+
let counter = 0
|
|
41
|
+
while (!ctx.signal.aborted) {
|
|
42
|
+
const data = await fetchLatestData(params.channel, { signal: ctx.signal })
|
|
43
|
+
|
|
44
|
+
yield {
|
|
45
|
+
id: String(counter++),
|
|
46
|
+
data,
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Wait between polls (pass signal for cancellation)
|
|
51
|
+
await new Promise((resolve, reject) => {
|
|
52
|
+
const timeout = setTimeout(resolve, 1000)
|
|
53
|
+
ctx.signal.addEventListener('abort', () => {
|
|
54
|
+
clearTimeout(timeout)
|
|
55
|
+
resolve(undefined)
|
|
56
|
+
}, { once: true })
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// After loop exits, signal.reason tells you why:
|
|
61
|
+
// - 'stream-completed': consumer finished reading (normal)
|
|
62
|
+
// - other: client disconnected or external abort
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// Placeholder — replace with real data fetching
|
|
67
|
+
async function fetchLatestData(channel: string, opts?: { signal?: AbortSignal }) {
|
|
68
|
+
return { value: Math.random() }
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Test — `{{Name}}.stream.test.ts`
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { describe, test, expect } from 'vitest'
|
|
76
|
+
import { ProcedureValidationError } from 'ts-procedures'
|
|
77
|
+
import { {{Name}} } from './{{Name}}.stream'
|
|
78
|
+
|
|
79
|
+
const mockContext = { userId: 'test-user' }
|
|
80
|
+
|
|
81
|
+
describe('{{Name}}', () => {
|
|
82
|
+
test('yields expected values', async () => {
|
|
83
|
+
const values = []
|
|
84
|
+
for await (const val of {{Name}}(mockContext, { channel: 'test' })) {
|
|
85
|
+
values.push(val)
|
|
86
|
+
if (values.length >= 3) break // Don't consume indefinitely
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
expect(values).toHaveLength(3)
|
|
90
|
+
expect(values[0]).toHaveProperty('id')
|
|
91
|
+
expect(values[0]).toHaveProperty('data')
|
|
92
|
+
expect(values[0]).toHaveProperty('timestamp')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('throws ProcedureValidationError for invalid params', async () => {
|
|
96
|
+
const gen = {{Name}}(mockContext, {} as any)
|
|
97
|
+
await expect(gen.next()).rejects.toThrow(ProcedureValidationError)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('respects signal abort', async () => {
|
|
101
|
+
const ac = new AbortController()
|
|
102
|
+
const values = []
|
|
103
|
+
|
|
104
|
+
setTimeout(() => ac.abort(), 100)
|
|
105
|
+
|
|
106
|
+
for await (const val of {{Name}}({ ...mockContext, signal: ac.signal }, { channel: 'test' })) {
|
|
107
|
+
values.push(val)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
expect(values.length).toBeGreaterThanOrEqual(0)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
```
|