@durable-streams/cli 0.1.4 → 0.1.5

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 CHANGED
@@ -100,6 +100,18 @@ durable-stream-dev write <stream_id> '{"key": "value"}' --content-type applicati
100
100
  durable-stream-dev write <stream_id> '{"key": "value"}' --json
101
101
  ```
102
102
 
103
+ ##### JSON Mode Array Flattening
104
+
105
+ In JSON mode (`--json` or `--content-type application/json`), top-level arrays are flattened into individual messages:
106
+
107
+ | Input | Messages stored |
108
+ | ------------ | ---------------------- |
109
+ | `{}` | 1 message: `{}` |
110
+ | `[{}, {}]` | 2 messages: `{}`, `{}` |
111
+ | `[[{}, {}]]` | 1 message: `[{}, {}]` |
112
+
113
+ This matches the protocol's batch semantics.
114
+
103
115
  #### Read from a stream
104
116
 
105
117
  ```bash
package/dist/index.cjs CHANGED
@@ -28,6 +28,28 @@ const node_process = __toESM(require("node:process"));
28
28
  const node_url = __toESM(require("node:url"));
29
29
  const __durable_streams_client = __toESM(require("@durable-streams/client"));
30
30
 
31
+ //#region src/jsonUtils.ts
32
+ /**
33
+ * Check if content-type indicates JSON mode.
34
+ * Handles cases like "application/json; charset=utf-8".
35
+ */
36
+ function isJsonContentType(contentType) {
37
+ return contentType.split(`;`)[0].trim().toLowerCase() === `application/json`;
38
+ }
39
+ /**
40
+ * One-level array flattening for JSON batch semantics.
41
+ * - Single object: `{}` → yields once
42
+ * - Array: `[{}, {}]` → yields each element (flattened)
43
+ * - Nested array: `[[{}, {}]]` → yields `[{}, {}]` (outer array flattened, inner preserved)
44
+ *
45
+ * This matches the protocol's batch semantics where servers flatten exactly one level.
46
+ */
47
+ function* flattenJsonForAppend(parsed) {
48
+ if (Array.isArray(parsed)) for (const item of parsed) yield item;
49
+ else yield parsed;
50
+ }
51
+
52
+ //#endregion
31
53
  //#region src/parseWriteArgs.ts
32
54
  /**
33
55
  * Parse write command arguments, extracting content-type flags and content.
@@ -37,21 +59,33 @@ const __durable_streams_client = __toESM(require("@durable-streams/client"));
37
59
  */
38
60
  function parseWriteArgs(args) {
39
61
  let contentType = `application/octet-stream`;
62
+ let batchJson = false;
40
63
  const contentParts = [];
41
64
  for (let i = 0; i < args.length; i++) {
42
65
  const arg = args[i];
43
- if (arg === `--json`) contentType = `application/json`;
44
- else if (arg === `--content-type`) {
66
+ if (arg === `--json`) {
67
+ contentType = `application/json`;
68
+ continue;
69
+ }
70
+ if (arg === `--batch-json`) {
71
+ batchJson = true;
72
+ contentType = `application/json`;
73
+ continue;
74
+ }
75
+ if (arg === `--content-type`) {
45
76
  const nextArg = args[i + 1];
46
77
  if (!nextArg || nextArg.startsWith(`--`)) throw new Error(`--content-type requires a value`);
47
78
  contentType = nextArg;
48
79
  i++;
49
- } else if (arg.startsWith(`--`)) throw new Error(`unknown flag: ${arg}`);
50
- else contentParts.push(arg);
80
+ continue;
81
+ }
82
+ if (arg.startsWith(`--`)) throw new Error(`unknown flag: ${arg}`);
83
+ contentParts.push(arg);
51
84
  }
52
85
  return {
53
86
  contentType,
54
- content: contentParts.join(` `)
87
+ content: contentParts.join(` `),
88
+ batchJson
55
89
  };
56
90
  }
57
91
 
@@ -69,7 +103,8 @@ Usage:
69
103
 
70
104
  Write Options:
71
105
  --content-type <type> Content-Type for the message (default: application/octet-stream)
72
- --json Shorthand for --content-type application/json
106
+ --json Write as JSON (input stored as single message)
107
+ --batch-json Write as JSON array of messages (each array element stored separately)
73
108
 
74
109
  Environment Variables:
75
110
  STREAM_URL Base URL of the stream server (default: http://localhost:4437)
@@ -88,8 +123,20 @@ async function createStream(streamId) {
88
123
  process.exit(1);
89
124
  }
90
125
  }
91
- async function writeStream(streamId, contentType, content) {
126
+ /**
127
+ * Append JSON data to a stream with one-level array flattening.
128
+ */
129
+ async function appendJson(stream, parsed) {
130
+ let count = 0;
131
+ for (const item of flattenJsonForAppend(parsed)) {
132
+ await stream.append(item);
133
+ count++;
134
+ }
135
+ return count;
136
+ }
137
+ async function writeStream(streamId, contentType, batchJson, content) {
92
138
  const url = `${STREAM_URL}/v1/stream/${streamId}`;
139
+ const isJson = isJsonContentType(contentType);
93
140
  try {
94
141
  const stream = new __durable_streams_client.DurableStream({
95
142
  url,
@@ -97,8 +144,19 @@ async function writeStream(streamId, contentType, content) {
97
144
  });
98
145
  if (content) {
99
146
  const processedContent = content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
100
- await stream.append(processedContent);
101
- console.log(`Wrote ${processedContent.length} bytes to ${streamId}`);
147
+ if (isJson) {
148
+ const parsed = JSON.parse(processedContent);
149
+ if (batchJson) {
150
+ const count = await appendJson(stream, parsed);
151
+ console.log(`Wrote ${count} message(s) to ${streamId}`);
152
+ } else {
153
+ await stream.append(parsed);
154
+ console.log(`Wrote 1 message to ${streamId}`);
155
+ }
156
+ } else {
157
+ await stream.append(processedContent);
158
+ console.log(`Wrote ${processedContent.length} bytes to ${streamId}`);
159
+ }
102
160
  } else {
103
161
  const chunks = [];
104
162
  node_process.stdin.on(`data`, (chunk) => {
@@ -109,8 +167,19 @@ async function writeStream(streamId, contentType, content) {
109
167
  node_process.stdin.on(`error`, reject);
110
168
  });
111
169
  const data = Buffer.concat(chunks);
112
- await stream.append(data);
113
- console.log(`Wrote ${data.length} bytes to ${streamId}`);
170
+ if (isJson) {
171
+ const parsed = JSON.parse(data.toString(`utf8`));
172
+ if (batchJson) {
173
+ const count = await appendJson(stream, parsed);
174
+ console.log(`Wrote ${count} message(s) to ${streamId}`);
175
+ } else {
176
+ await stream.append(parsed);
177
+ console.log(`Wrote 1 message to ${streamId}`);
178
+ }
179
+ } else {
180
+ await stream.append(data);
181
+ console.log(`Wrote ${data.length} bytes to ${streamId}`);
182
+ }
114
183
  }
115
184
  } catch (error) {
116
185
  if (error instanceof Error) node_process.stderr.write(`Error writing to stream: ${error.message}\n`);
@@ -170,8 +239,8 @@ async function main() {
170
239
  if (error instanceof Error) node_process.stderr.write(`Error: ${error.message}\n`);
171
240
  process.exit(1);
172
241
  }
173
- if (!node_process.stdin.isTTY) await writeStream(streamId, parsed.contentType);
174
- else if (parsed.content) await writeStream(streamId, parsed.contentType, parsed.content);
242
+ if (!node_process.stdin.isTTY) await writeStream(streamId, parsed.contentType, parsed.batchJson);
243
+ else if (parsed.content) await writeStream(streamId, parsed.contentType, parsed.batchJson, parsed.content);
175
244
  else {
176
245
  node_process.stderr.write(`Error: content required (provide as argument or pipe to stdin)\n`);
177
246
  printUsage();
@@ -215,4 +284,6 @@ if (isMainModule()) main().catch((error) => {
215
284
  });
216
285
 
217
286
  //#endregion
287
+ exports.flattenJsonForAppend = flattenJsonForAppend
288
+ exports.isJsonContentType = isJsonContentType
218
289
  exports.parseWriteArgs = parseWriteArgs
package/dist/index.d.cts CHANGED
@@ -1,7 +1,25 @@
1
+ //#region src/jsonUtils.d.ts
2
+ /**
3
+ * Check if content-type indicates JSON mode.
4
+ * Handles cases like "application/json; charset=utf-8".
5
+ */
6
+ declare function isJsonContentType(contentType: string): boolean;
7
+ /**
8
+ * One-level array flattening for JSON batch semantics.
9
+ * - Single object: `{}` → yields once
10
+ * - Array: `[{}, {}]` → yields each element (flattened)
11
+ * - Nested array: `[[{}, {}]]` → yields `[{}, {}]` (outer array flattened, inner preserved)
12
+ *
13
+ * This matches the protocol's batch semantics where servers flatten exactly one level.
14
+ */
15
+ declare function flattenJsonForAppend(parsed: unknown): Generator<unknown>;
16
+
17
+ //#endregion
1
18
  //#region src/parseWriteArgs.d.ts
2
19
  interface ParsedWriteArgs {
3
20
  contentType: string;
4
21
  content: string;
22
+ batchJson: boolean;
5
23
  }
6
24
  /**
7
25
  * Parse write command arguments, extracting content-type flags and content.
@@ -12,4 +30,4 @@ interface ParsedWriteArgs {
12
30
  declare function parseWriteArgs(args: Array<string>): ParsedWriteArgs;
13
31
 
14
32
  //#endregion
15
- export { ParsedWriteArgs, parseWriteArgs };
33
+ export { ParsedWriteArgs, flattenJsonForAppend, isJsonContentType, parseWriteArgs };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,25 @@
1
+ //#region src/jsonUtils.d.ts
2
+ /**
3
+ * Check if content-type indicates JSON mode.
4
+ * Handles cases like "application/json; charset=utf-8".
5
+ */
6
+ declare function isJsonContentType(contentType: string): boolean;
7
+ /**
8
+ * One-level array flattening for JSON batch semantics.
9
+ * - Single object: `{}` → yields once
10
+ * - Array: `[{}, {}]` → yields each element (flattened)
11
+ * - Nested array: `[[{}, {}]]` → yields `[{}, {}]` (outer array flattened, inner preserved)
12
+ *
13
+ * This matches the protocol's batch semantics where servers flatten exactly one level.
14
+ */
15
+ declare function flattenJsonForAppend(parsed: unknown): Generator<unknown>;
16
+
17
+ //#endregion
1
18
  //#region src/parseWriteArgs.d.ts
2
19
  interface ParsedWriteArgs {
3
20
  contentType: string;
4
21
  content: string;
22
+ batchJson: boolean;
5
23
  }
6
24
  /**
7
25
  * Parse write command arguments, extracting content-type flags and content.
@@ -12,4 +30,4 @@ interface ParsedWriteArgs {
12
30
  declare function parseWriteArgs(args: Array<string>): ParsedWriteArgs;
13
31
 
14
32
  //#endregion
15
- export { ParsedWriteArgs, parseWriteArgs };
33
+ export { ParsedWriteArgs, flattenJsonForAppend, isJsonContentType, parseWriteArgs };
package/dist/index.js CHANGED
@@ -4,6 +4,28 @@ import { stderr, stdin, stdout } from "node:process";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { DurableStream } from "@durable-streams/client";
6
6
 
7
+ //#region src/jsonUtils.ts
8
+ /**
9
+ * Check if content-type indicates JSON mode.
10
+ * Handles cases like "application/json; charset=utf-8".
11
+ */
12
+ function isJsonContentType(contentType) {
13
+ return contentType.split(`;`)[0].trim().toLowerCase() === `application/json`;
14
+ }
15
+ /**
16
+ * One-level array flattening for JSON batch semantics.
17
+ * - Single object: `{}` → yields once
18
+ * - Array: `[{}, {}]` → yields each element (flattened)
19
+ * - Nested array: `[[{}, {}]]` → yields `[{}, {}]` (outer array flattened, inner preserved)
20
+ *
21
+ * This matches the protocol's batch semantics where servers flatten exactly one level.
22
+ */
23
+ function* flattenJsonForAppend(parsed) {
24
+ if (Array.isArray(parsed)) for (const item of parsed) yield item;
25
+ else yield parsed;
26
+ }
27
+
28
+ //#endregion
7
29
  //#region src/parseWriteArgs.ts
8
30
  /**
9
31
  * Parse write command arguments, extracting content-type flags and content.
@@ -13,21 +35,33 @@ import { DurableStream } from "@durable-streams/client";
13
35
  */
14
36
  function parseWriteArgs(args) {
15
37
  let contentType = `application/octet-stream`;
38
+ let batchJson = false;
16
39
  const contentParts = [];
17
40
  for (let i = 0; i < args.length; i++) {
18
41
  const arg = args[i];
19
- if (arg === `--json`) contentType = `application/json`;
20
- else if (arg === `--content-type`) {
42
+ if (arg === `--json`) {
43
+ contentType = `application/json`;
44
+ continue;
45
+ }
46
+ if (arg === `--batch-json`) {
47
+ batchJson = true;
48
+ contentType = `application/json`;
49
+ continue;
50
+ }
51
+ if (arg === `--content-type`) {
21
52
  const nextArg = args[i + 1];
22
53
  if (!nextArg || nextArg.startsWith(`--`)) throw new Error(`--content-type requires a value`);
23
54
  contentType = nextArg;
24
55
  i++;
25
- } else if (arg.startsWith(`--`)) throw new Error(`unknown flag: ${arg}`);
26
- else contentParts.push(arg);
56
+ continue;
57
+ }
58
+ if (arg.startsWith(`--`)) throw new Error(`unknown flag: ${arg}`);
59
+ contentParts.push(arg);
27
60
  }
28
61
  return {
29
62
  contentType,
30
- content: contentParts.join(` `)
63
+ content: contentParts.join(` `),
64
+ batchJson
31
65
  };
32
66
  }
33
67
 
@@ -45,7 +79,8 @@ Usage:
45
79
 
46
80
  Write Options:
47
81
  --content-type <type> Content-Type for the message (default: application/octet-stream)
48
- --json Shorthand for --content-type application/json
82
+ --json Write as JSON (input stored as single message)
83
+ --batch-json Write as JSON array of messages (each array element stored separately)
49
84
 
50
85
  Environment Variables:
51
86
  STREAM_URL Base URL of the stream server (default: http://localhost:4437)
@@ -64,8 +99,20 @@ async function createStream(streamId) {
64
99
  process.exit(1);
65
100
  }
66
101
  }
67
- async function writeStream(streamId, contentType, content) {
102
+ /**
103
+ * Append JSON data to a stream with one-level array flattening.
104
+ */
105
+ async function appendJson(stream, parsed) {
106
+ let count = 0;
107
+ for (const item of flattenJsonForAppend(parsed)) {
108
+ await stream.append(item);
109
+ count++;
110
+ }
111
+ return count;
112
+ }
113
+ async function writeStream(streamId, contentType, batchJson, content) {
68
114
  const url = `${STREAM_URL}/v1/stream/${streamId}`;
115
+ const isJson = isJsonContentType(contentType);
69
116
  try {
70
117
  const stream = new DurableStream({
71
118
  url,
@@ -73,8 +120,19 @@ async function writeStream(streamId, contentType, content) {
73
120
  });
74
121
  if (content) {
75
122
  const processedContent = content.replace(/\\n/g, `\n`).replace(/\\t/g, `\t`).replace(/\\r/g, `\r`).replace(/\\\\/g, `\\`);
76
- await stream.append(processedContent);
77
- console.log(`Wrote ${processedContent.length} bytes to ${streamId}`);
123
+ if (isJson) {
124
+ const parsed = JSON.parse(processedContent);
125
+ if (batchJson) {
126
+ const count = await appendJson(stream, parsed);
127
+ console.log(`Wrote ${count} message(s) to ${streamId}`);
128
+ } else {
129
+ await stream.append(parsed);
130
+ console.log(`Wrote 1 message to ${streamId}`);
131
+ }
132
+ } else {
133
+ await stream.append(processedContent);
134
+ console.log(`Wrote ${processedContent.length} bytes to ${streamId}`);
135
+ }
78
136
  } else {
79
137
  const chunks = [];
80
138
  stdin.on(`data`, (chunk) => {
@@ -85,8 +143,19 @@ async function writeStream(streamId, contentType, content) {
85
143
  stdin.on(`error`, reject);
86
144
  });
87
145
  const data = Buffer.concat(chunks);
88
- await stream.append(data);
89
- console.log(`Wrote ${data.length} bytes to ${streamId}`);
146
+ if (isJson) {
147
+ const parsed = JSON.parse(data.toString(`utf8`));
148
+ if (batchJson) {
149
+ const count = await appendJson(stream, parsed);
150
+ console.log(`Wrote ${count} message(s) to ${streamId}`);
151
+ } else {
152
+ await stream.append(parsed);
153
+ console.log(`Wrote 1 message to ${streamId}`);
154
+ }
155
+ } else {
156
+ await stream.append(data);
157
+ console.log(`Wrote ${data.length} bytes to ${streamId}`);
158
+ }
90
159
  }
91
160
  } catch (error) {
92
161
  if (error instanceof Error) stderr.write(`Error writing to stream: ${error.message}\n`);
@@ -146,8 +215,8 @@ async function main() {
146
215
  if (error instanceof Error) stderr.write(`Error: ${error.message}\n`);
147
216
  process.exit(1);
148
217
  }
149
- if (!stdin.isTTY) await writeStream(streamId, parsed.contentType);
150
- else if (parsed.content) await writeStream(streamId, parsed.contentType, parsed.content);
218
+ if (!stdin.isTTY) await writeStream(streamId, parsed.contentType, parsed.batchJson);
219
+ else if (parsed.content) await writeStream(streamId, parsed.contentType, parsed.batchJson, parsed.content);
151
220
  else {
152
221
  stderr.write(`Error: content required (provide as argument or pipe to stdin)\n`);
153
222
  printUsage();
@@ -191,4 +260,4 @@ if (isMainModule()) main().catch((error) => {
191
260
  });
192
261
 
193
262
  //#endregion
194
- export { parseWriteArgs };
263
+ export { flattenJsonForAppend, isJsonContentType, parseWriteArgs };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@durable-streams/cli",
3
3
  "description": "CLI tool for working with Durable Streams",
4
- "version": "0.1.4",
4
+ "version": "0.1.5",
5
5
  "author": "Durable Stream contributors",
6
6
  "bin": {
7
7
  "cli": "./dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "url": "https://github.com/durable-streams/durable-streams/issues"
13
13
  },
14
14
  "dependencies": {
15
- "@durable-streams/client": "0.1.3"
15
+ "@durable-streams/client": "0.1.4"
16
16
  },
17
17
  "devDependencies": {
18
18
  "@types/node": "^22.15.21",
@@ -20,7 +20,7 @@
20
20
  "tsx": "^4.19.2",
21
21
  "typescript": "^5.5.2",
22
22
  "vitest": "^3.1.3",
23
- "@durable-streams/server": "0.1.4"
23
+ "@durable-streams/server": "0.1.5"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=18.0.0"