К основному содержанию
скилл CX Codex

jsonlines — скилл для Claude Code: поток JSON Lines

Скилл помогает настроить потоковую передачу JSON Lines в Vovk.ts: сервер отправляет данные по мере готовности, а клиент читает их как асинхронный поток.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add finom/vovk --skill jsonlines -a claude-code
Открыть на GitHub

Что делает jsonlines

Скилл разбирает обработчики-генераторы function* и async function*, проверку каждой порции данных через iteration и ручное управление потоком с помощью JSONLinesResponder. Для проверки каждой отправленной порции есть параметр validateEachIteration; такая проверка добавляет затраты на каждый элемент. В примерах используются vovk и zod.

Клиентский RPC-метод возвращает асинхронный итератор: его можно читать по элементам, собрать всё через asPromise(), наблюдать за элементами через onIterate() или остановить через abortController. using закрывает поток при выходе из области видимости. Для ответов с несколькими типами данных описан progressive(), но этот API экспериментальный и может измениться. Скилл не охватывает настройку RPC-клиента, основы процедур и WebSocket-соединения.

Какие задачи решает

  • отправлять результаты генератора по мере готовности
  • проверять структуру каждой порции JSON Lines
  • управлять потоком вручную через JSONLinesResponder
  • читать поток RPC по элементам или собрать его целиком
  • остановить чтение потока со стороны клиента

Примеры запросов агенту

  • Настрой обработчик Vovk.ts, который отправляет сообщения по мере готовности через JSON Lines.
  • Покажи, как проверить каждую порцию, которую выдаёт генератор.
  • Объясни, как прочитать поток RPC и остановить его при нужном условии.

Как установить jsonlines

1Через Skills CLI (Vercel)

npx skills add finom/vovk --skill jsonlines -a claude-code

2Вручную: положить файлы

~/.claude/skills/jsonlines/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/jsonlines/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о jsonlines

Как установить jsonlines в Claude Code?

Выполните npx skills add finom/vovk --skill jsonlines -a claude-code. Файл скилла находится по пути ~/.claude/skills/jsonlines/SKILL.md.

Что делает jsonlines?

Помогает создавать обработчики потоковой передачи JSON Lines в Vovk.ts и читать такие потоки через RPC-клиент. Описывает генераторы, JSONLinesResponder и способы управления чтением.

Как клиенту остановить поток JSON Lines?

Для завершения без ошибки используйте abortSilently(). Вызов abortController.abort() прерывает чтение с ошибкой AbortError.

Исходный текст

SKILL.md · MIT
---
name: jsonlines
description: Vovk.ts JSON Lines streaming — generator handlers (`function*`, `async function*`), the `iteration` validation option, `validateEachIteration`, the `JSONLinesResponder` manual API, progressive responses via `progressive()`, consuming streams from the RPC client (async iteration, `using`, `asPromise`, `onIterate`, `abortController`), and `VovkIteration` / `VovkYieldType` inference. Use whenever the user asks to stream data ("stream tokens", "SSE-like streaming", "server-sent events", "progressive response", "poll in a loop", "yield results as they arrive", "chat completion streaming", "JSON Lines", "JSONL", "async generator endpoint", "incremental response", "push updates to the client"). Does NOT cover procedure basics → hand off to `procedure` skill. Does NOT cover RPC client setup / `vovk generate` → hand off to `rpc` skill. Does NOT cover WebSockets / bidirectional realtime — that's outside Vovk's scope.
---

# Vovk.ts JSON Lines streaming

JSON Lines is Vovk's streaming model: server yields one JSON object per line, client consumes as async iterable. Works for chat completions, progress updates, long-running jobs, anything where data arrives over time.

Two authoring styles:

- **Generator handlers** — `function*` / `async function*` with `yield`. Common case.
- **`JSONLinesResponder`** — low-level manual `send` / `close` / `throw`. For orchestration, fan-in, or when stream shape doesn't fit a generator.

## Generator handler

Simplest streaming handler: `async function*` yielding objects. Each yield → one JSON line on wire.

```ts
import { post, procedure, operation } from 'vovk';
import { z } from 'zod';

export default class StreamController {
  @operation({ summary: 'Stream messages' })
  @post('completions')
  static getJSONLines = procedure({
    iteration: z.object({ message: z.string() }),
  }).handle(async function* () {
    yield { message: 'Hello,' };
    yield { message: ' World' };
  });
}
```

**`iteration`**: validation schema for each yielded object. Same role `output` plays for non-streaming procedures.

### Delegating to a service with `yield*`

Keep controller thin, let service do work:

```ts
static getJSONLines = procedure({
  iteration: z.object({ message: z.string() }),
}).handle(function* () {
  yield* StreamService.getJSONLines();
});
```

### Validation cadence

Default: **only first yielded item validated** — cheap, catches shape bugs early. Opt into every-iteration validation when shape varies:

```ts
procedure({
  iteration: z.union([
    z.strictObject({ users: z.array(UserSchema) }),
    z.strictObject({ tasks: z.array(TaskSchema) }),
  ]),
  validateEachIteration: true,
})
```

Use sparingly — every-item validation adds per-yield cost.

## `JSONLinesResponder` — manual control

For cases generator can't express: fan-in from multiple concurrent sources, conditional close, error signaling outside generator protocol. Still a `procedure` — handler is plain `async` function building responder, kicking off producer, returning responder. With `iteration` defined, Vovk wires `responder.onBeforeSend` automatically → every `send()` runs through same schema (and same `validateEachIteration` rules) as generator handler.

```ts
import { JSONLinesResponder, procedure, get } from 'vovk';
import { z } from 'zod';

class StreamController {
  @get('tokens')
  static streamTokens = procedure({
    iteration: z.object({ message: z.string() }),
  }).handle(async (req) => {
    const responder = new JSONLinesResponder<{ message: string }>(req);
    void StreamService.streamTokens(responder);
    return responder;
  });
}
```

Handler returns synchronously; service runs as floating promise (`void ...`), feeds responder over time. Don't `await` service — would block response.

**Constructor**:

```ts
new JSONLinesResponder<T>(
  request?: Request | null,              // checks Accept: application/jsonl
  getResponse?: (responder) => Response, // custom Response builder
);
```

**Methods**:

| Method | Purpose |
|---|---|
| `send(item: T): Promise<void>` | Serialize + send one JSON line. Validates per `validateEachIteration`. |
| `close(): void` | End stream. |
| `throw(err: Error): void` | Send error frame + close. |

**Properties**: `response`, `headers`, `readableStream`.

### Service-owned streaming

Service accepts responder + orchestrates:

```ts
export default class StreamService {
  static async streamTokens(responder: JSONLinesResponder<Token>) {
    const tokens = [{ message: 'Hello,' }, { message: ' World' }];
    for (const t of tokens) await responder.send(t);
    responder.close();
  }
}
```

Controller returns responder immediately; service populates via floating promise (`void service.run(responder)`).

## Client consumption

Generated RPC method returns async iterable.

### Basic iteration

```ts
import { StreamRPC } from '@/client';

using stream = await StreamRPC.getJSONLines();

for await (const { message } of stream) {
  console.log(message);
}
```

`using` triggers `Symbol.asyncDispose` → calls `abortSilently('Stream async disposed')` on scope exit (error, break, function return). Safer than plain `const`.

### Stream object API

Returned value is `VovkStreamAsyncIterable<T>` (from `vovk`) — async iterable plus:

```ts
type VovkStreamAsyncIterable<T> = AsyncIterable<T> & {
  status: number;                                       // HTTP status
  asPromise(): Promise<T[]>;                            // collect all items
  onIterate(cb: (item: T, i: number) => void): () => void; // returns unsubscribe
  abortController: AbortController;                     // programmatic abort — throws AbortError on the reader
  abortSilently(reason?: unknown): void;                // close without throwing
  [Symbol.dispose](): void;
  [Symbol.asyncDispose](): Promise<void>;
};
```

`abortController.abort()` throws `AbortError` on consumer (catchable via `error.cause`); `abortSilently()` tears stream down without throwing. Pick deliberately.

### Common patterns

**Collect all at once:**

```ts
const items = await (await StreamRPC.getJSONLines()).asPromise();
```

**Observe without consuming:**

```ts
using stream = await StreamRPC.getJSONLines();
stream.onIterate((item) => console.log(item));
await stream.asPromise();
```

**Abort mid-stream:**

```ts
using stream = await StreamRPC.getJSONLines();
for await (const item of stream) {
  if (someCondition(item)) stream.abortSilently();
}
```

## Progressive responses

Multi-shape stream where different lines populate different promises. Use when single request produces several distinct payloads (users, tasks, summary) in parallel. **Experimental** — API may change.

**Client:**

```ts
import { progressive } from 'vovk';

const { users, tasks } = progressive(
  ProgressiveRPC.streamProgressiveResponse,
  { params: { id: '123' } },
);

users.then(console.log);
tasks.then(console.log);
```

`progressive()` returns proxy — each property access creates promise resolving when matching JSON line arrives.

**Server** — controller declares iteration union + builds responder; service orchestrates fan-in. Every line has own shape → turn on `validateEachIteration`:

```ts
// progressive-controller.ts
@get('/')
static streamProgressiveResponse = procedure({
  iteration: z.union([
    z.strictObject({ users: z.array(UserSchema) }),
    z.strictObject({ tasks: z.array(TaskSchema) }),
  ]),
  validateEachIteration: true,
}).handle(async (req) => {
  const responder = new JSONLinesResponder<Iter>(req);
  void ProgressiveService.streamProgressiveResponse(responder);
  return responder;
});

// progressive-service.ts — no req, just the sink
static streamProgressiveResponse(responder: JSONLinesResponder<Iter>) {
  return Promise.all([
    ProgressiveService.getUsers().then((users) => responder.send({ users })),
    ProgressiveService.getTasks().then((tasks) => responder.send({ tasks })),
  ])
    .then(() => responder.close())
    .catch((e) => responder.throw(e));
}
```

## Type inference

```ts
import type { VovkIteration, VovkYieldType } from 'vovk';

// From iteration schema:
type Item = VovkIteration<typeof StreamRPC.getJSONLines>;

// From generator return type (when no iteration schema):
type Yield = VovkYieldType<typeof StreamController.streamItems>;
```

`VovkIteration` when `iteration` schema exists (preferred). `VovkYieldType` infers from handler's actual yield type when no schema declared.

## Content negotiation

- `Accept: application/jsonl` → response `Content-Type: application/jsonl`.
- Otherwise → `Content-Type: text/plain` (browser-viewable for debugging).

JSON Lines responses **bypass compression** (Gzip/Brotli) — size unknown, streaming matters more. For large streams, factor bandwidth into design.

## Real-world example: OpenAI chat completion

Controller does thin pre-calc (validated body → plain values), service owns OpenAI call + `yield*`s its stream:

```ts
// chat-controller.ts
@post('chat')
static createChatCompletion = procedure({
  body: z.object({ messages: z.array(MessageSchema) }),
  iteration: ChatChunkSchema,
}).handle(async function* ({ vovk }) {
  const { messages } = await vovk.body();
  yield* ChatService.streamCompletion({ messages });
});

// chat-service.ts — no req, no VovkRequest
export default class ChatService {
  static async *streamCompletion({ messages }: { messages: Message[] }) {
    const openai = new OpenAI();
    yield* await openai.chat.completions.create({ messages, model: 'gpt-5.5', stream: true });
  }
}
```

```ts
// Client
using completion = await ChatRPC.createChatCompletion({ body: { messages } });
for await (const part of completion) {
  process.stdout.write(part.choices[0]?.delta?.content ?? '');
}
```

## Flows

### "Stream GPT tokens to the browser"

Generator handler yielding OpenAI chunks → client `for await`. See example above.

### "Long-running job with progress + final result"

Server yields progress shapes, then terminal `done` shape carrying result; client iterates until terminal one. What people mean by "poll until done" — server pushes, client reads until terminal line:

```ts
// Server
@post('import')
static runImport = procedure({
  iteration: z.discriminatedUnion('status', [
    z.object({ status: z.literal('progress'), percent: z.number() }),
    z.object({ status: z.literal('done'), result: z.string() }),
  ]),
  validateEachIteration: true,
}).handle(async function* () {
  for (let i = 10; i < 100; i += 10) {
    await importStep(i);
    yield { status: 'progress', percent: i } as const;
  }
  yield { status: 'done', result: await finalize() } as const;
});

// Client
using stream = await ImportRPC.runImport();
for await (const update of stream) {
  if (update.status === 'done') return update.result;
  renderBar(update.percent);
}
```

### "Infinite polling / reconnect ticker"

For connections outlasting what proxies / load-balancers tolerate, or when you want bounded per-connection cost: server streams short run, closes at natural boundary; client wraps call in `while(true)`, resumes with last cursor. JSON Lines is primitive, `while` on both sides does the work.

```ts
// Server — yield until a break point, then let the stream end
static streamPoll = procedure({
  query: z.object({ i: z.string().regex(/^\d{1,6}$/) }), // from 2^53 on, ++i stays put and the stream never ends
  iteration: z.object({ i: z.number() }),
}).handle(async function* (req) {
  let i = Number(req.vovk.query().i);
  while (true) {
    yield { i: ++i };
    await new Promise((r) => setTimeout(r, 1000));
    if (!(i % 10)) break; // natural reconnect point
  }
});

// Client — reconnect forever, passing the last cursor back in
let i = 0;
while (true) {
  using iterable = await PollRPC.streamPoll({ query: { i: i.toString() } });
  for await ({ i } of iterable) render(i);
}
```

### "Dashboard: users and tasks load in parallel, render each as it arrives"

`progressive()` pattern — server fires both fetches concurrently, sends each result when ready; client awaits each property independently + renders piecewise.

### "Cancel the stream when the user navigates away"

```ts
using stream = await StreamRPC.getJSONLines();
controller.signal.addEventListener('abort', () => stream.abortSilently());
```

## Gotchas

- **Only first iteration validated by default**. Turn on `validateEachIteration: true` for variable-shape streams, or accept later items bypass schema enforcement.
- **`using` matters**. Without it, `abortSilently` not called on early exit → server keeps pushing into dropped connection.
- **`asPromise()` vs `for await`**: `asPromise` simpler but buffers everything. Use `for await` (or `onIterate`) to process items as they stream.
- **No compression**. JSON Lines skips Gzip/Brotli. Design accordingly for large payloads.
- **`yield*` with service generator**: types must line up. If service's yield type doesn't match `iteration` schema, first-item validation catches it.
- **`JSONLinesResponder` requires manual `close`**. Forgetting it leaves client hanging.
- **Error mid-stream**: use `responder.throw(err)` or throw from within generator. Bare unhandled rejections hang connection.
- **`VovkYieldType` brittle for self-referential service methods** — explicit return types on service method help.

© finom · MIT · finom/vovk

Похожие скиллы

Все скиллы по теме «Бэкенд и API» →
mcp-builder официальный anthropics Скилл проводит через создание MCP-сервера на TypeScript или Python: от изучения протокола до тестов и оценки качества. 130 949установок claude-api официальный anthropics Справочник по Anthropic SDK и API Claude для генерации кода без устаревших параметров и выдуманных методов. 74 198установок lark-im larksuite Скилл подключает Lark IM к агенту: он отправляет сообщения, ищет историю, управляет чатами и файлами. Подойдёт для задач вокруг командных коммуникаций. CX Codex +1 472 900установок lark-event larksuite Слушает события Lark/Feishu и отдаёт их потоком NDJSON. Подходит для ботов, обработчиков сообщений и долгих подписчиков. CX Codex +1 466 800установок caveman-discover JuliusBrussee Находит точки вызова LLM в репозитории и размечает их воркфлоу для Caveman Cloud, чтобы расходы на токены не сливались в одну общую кучу. CX Codex +1 104 400установок migration JuliusBrussee Скилл задаёт правила безопасных миграций схем, данных и API с обязательным откатом. Пригодится при изменениях, где нельзя терять данные или ломать совместимость. CX Codex +1 103 900установок

Из того же набора10

segment Делит бэкенд на сегменты Vovk.ts: отдельные префиксы API, свои настройки runtime и статические эндпоинты. Пригодится, когда бэкенд разрастается. CX Codex +1 ★ 57звёзд репо rpc Скилл помогает генерировать и настраивать типизированные RPC-клиенты Vovk.ts. Пригодится, когда нужно вызывать API из клиентского кода или разобраться, почему клиент устарел. CX Codex +1 ★ 57звёзд репо python Скилл генерирует типизированный Python-пакет из схем Vovk.ts: RPC-функции, TypedDict, стриминг через JSON Lines. Нужен, когда API уже есть, а Python-клиент писать руками не хочется. CX Codex +1 ★ 57звёзд репо openapi Скилл объясняет, как Vovk.ts собирает спецификацию OpenAPI из процедур и схем валидации. Пригодится, чтобы описать API и показать его документацию через Scalar. CX Codex +1 ★ 57звёзд репо multitenant Скилл для Vovk.ts: разводит несколько тенантов по поддоменам в одном Next.js-приложении — прокси, сегменты, страницы. CX Codex +1 ★ 57звёзд репо init Разворачивает бэкенд в Next.js через Vovk.ts: проверяет версии, запускает vovk-cli и доводит проект до рабочего состояния. CX Codex +1 ★ 57звёзд репо config Скилл объясняет настройки Vovk.ts и tsconfig.json. Пригодится, когда нужно изменить путь генерации, параметры клиента или настройки TypeScript. CX Codex +1 ★ 57звёзд репо bundle Упаковывает сгенерированный TypeScript-клиент Vovk.ts в npm-пакет без зависимостей: готовые JS, типы, package.json и README. Нужен, когда клиент пора выкладывать в реестр. CX Codex +1 ★ 57звёзд репо

Спонсоры проекта

Компании, которые помогают держать аналитику открытой