@yrest/cli 0.11.2 → 0.12.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 CHANGED
@@ -10,7 +10,7 @@
10
10
  [![CI](https://github.com/aggiovato/yRest/actions/workflows/ci.yml/badge.svg)](https://github.com/aggiovato/yRest/actions)
11
11
  [![Node](https://img.shields.io/node/v/@yrest/cli)](https://www.npmjs.com/package/@yrest/cli)
12
12
  [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue)](https://www.typescriptlang.org/)
13
- [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.11.2)](https://socket.dev/npm/package/@yrest/cli)
13
+ [![Socket](https://badge.socket.dev/npm/package/@yrest/cli/0.12.0)](https://socket.dev/npm/package/@yrest/cli)
14
14
 
15
15
  YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a `db.yml` file.
16
16
 
@@ -75,6 +75,7 @@ A YAML-first alternative to json-server for frontend development.
75
75
  | OpenAPI 3.0 spec (`GET /_openapi`, `yrest openapi`) | ✅ | ❌ |
76
76
  | Field annotations (`_schema`) | ✅ | ❌ |
77
77
  | Compact relation DSL (`m2o:target@fk[card]+nested`) | ✅ | ❌ |
78
+ | SSE stream routes (`_method: SSE`) | ✅ | ❌ |
78
79
 
79
80
  ---
80
81
 
@@ -935,6 +936,76 @@ type Handler = (req: HandlerRequest) => HandlerResponse | Promise<HandlerRespons
935
936
 
936
937
  If a named handler is not found in the file, the server returns `501`. If the handler throws, it returns `500`. If a `response:` block is defined alongside `handler:`, it is used as fallback only when the handler itself throws.
937
938
 
939
+ ### SSE streams
940
+
941
+ Mock Server-Sent Events streams by adding `_method: SSE` and an `_sse` block to a `_routes` entry. Clients connect with a regular `GET` request and receive a continuous stream of events until the connection is closed.
942
+
943
+ ```yaml
944
+ _routes:
945
+ - _method: SSE
946
+ _path: /events/orders
947
+ _sse:
948
+ _interval: 1500 # ms between frames (default: 1000)
949
+ _loop: true # restart after last event (default: true)
950
+ _repeat: 3 # stop after N full cycles (omit = infinite)
951
+ _events:
952
+ - _event: update
953
+ _data:
954
+ orderId: 1
955
+ status: processing
956
+ - _event: update
957
+ _data:
958
+ orderId: 1
959
+ status: shipped
960
+ - _event: done
961
+ _data:
962
+ orderId: 1
963
+ status: delivered
964
+ ```
965
+
966
+ **`_sse` options:**
967
+
968
+ | Key | Default | Description |
969
+ | ----------- | ------- | ---------------------------------------------------------------------------------------------------- |
970
+ | `_interval` | `1000` | Milliseconds between frames |
971
+ | `_loop` | `true` | Restart the sequence after the last frame. Set to `false` to close after one cycle |
972
+ | `_repeat` | — | Stop after N complete cycles and close the stream. Omit for infinite |
973
+ | `_events` | — | Ordered list of frames. Each entry needs `_data` (required) and optionally `_event` (SSE event name) |
974
+
975
+ **Template variables** — resolved per frame at emit time, so each event gets a fresh value:
976
+
977
+ ```yaml
978
+ _events:
979
+ - _event: tick
980
+ _data:
981
+ ts: "{{now}}" # ISO timestamp, unique per frame
982
+ id: "{{uuid}}" # random UUID v4
983
+ channel: "{{params.channel}}" # from URL params
984
+ filter: "{{query.filter}}" # from query string
985
+ ```
986
+
987
+ **Consuming from the browser:**
988
+
989
+ ```js
990
+ const es = new EventSource("http://localhost:3070/events/orders");
991
+
992
+ es.addEventListener("update", (e) => console.log(JSON.parse(e.data)));
993
+ es.addEventListener("done", (e) => {
994
+ console.log(JSON.parse(e.data));
995
+ es.close();
996
+ });
997
+ ```
998
+
999
+ **Consuming from the terminal:**
1000
+
1001
+ ```bash
1002
+ curl -N http://localhost:3070/events/orders
1003
+ ```
1004
+
1005
+ The `-N` flag disables curl's output buffering, which is required to see SSE frames as they arrive.
1006
+
1007
+ A keep-alive comment (`: ping`) is sent every 15 s to prevent proxy timeouts. SSE routes are shown in `/_about` under their own **SSE streams** accordion with a sky-blue `SSE` badge.
1008
+
938
1009
  ---
939
1010
 
940
1011
  ## Server modes
@@ -1252,6 +1323,9 @@ const server = createYrestServer({
1252
1323
  | Explicit relation types (one2one, many2many) | ✅ |
1253
1324
  | Bidirectional many2many nested routes | ✅ |
1254
1325
  | Auto-embedding (`nested: true` in `_rel`) | ✅ |
1326
+ | SSE stream routes (`_method: SSE`) | ✅ |
1327
+ | WebSocket mock channels (`_method: WS`) | 🔜 |
1328
+ | TypeScript handlers + typed collections | 🔜 |
1255
1329
 
1256
1330
  ---
1257
1331
 
package/dist/cli/index.js CHANGED
@@ -664,6 +664,8 @@ function normaliseRoute(raw) {
664
664
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
665
665
  const v = raw;
666
666
  const method = v["_method"];
667
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
668
+ if ("_sse" in v) return null;
667
669
  const path = v["_path"];
668
670
  if (typeof method !== "string" || typeof path !== "string") return null;
669
671
  const route = { method, path };
@@ -703,6 +705,43 @@ function normaliseScenario(raw) {
703
705
  return { when, response };
704
706
  }
705
707
 
708
+ // src/storage/parseSseRoutes.ts
709
+ function parseSseRoutes(raw) {
710
+ if (!Array.isArray(raw)) return [];
711
+ return raw.map(normaliseSseRoute).filter((r) => r !== null);
712
+ }
713
+ function normaliseSseRoute(raw) {
714
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
715
+ const v = raw;
716
+ const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
717
+ const path = v["_path"];
718
+ const sse = v["_sse"];
719
+ if (method !== "SSE") return null;
720
+ if (typeof path !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
721
+ return null;
722
+ }
723
+ const s = sse;
724
+ const rawEvents = s["_events"];
725
+ if (!Array.isArray(rawEvents)) return null;
726
+ const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
727
+ if (!events.length) return null;
728
+ return {
729
+ path,
730
+ interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
731
+ loop: s["_loop"] !== false,
732
+ repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
733
+ events
734
+ };
735
+ }
736
+ function normaliseEvent(raw) {
737
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
738
+ const v = raw;
739
+ if (!("_data" in v)) return null;
740
+ const event = { data: v["_data"] };
741
+ if (typeof v["_event"] === "string") event.event = v["_event"];
742
+ return event;
743
+ }
744
+
706
745
  // src/storage/parseSchema.ts
707
746
  function parseSchema(raw) {
708
747
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -846,6 +885,7 @@ function createYrestStorage(filePath) {
846
885
  const raw = (0, import_yaml.parse)((0, import_node_fs2.readFileSync)(absPath, "utf8")) ?? {};
847
886
  const relations = parseRelations(raw["_rel"]);
848
887
  const routes = parseRoutes(raw["_routes"]);
888
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
849
889
  const schema = parseSchema(raw["_schema"]);
850
890
  const dataBlock = hasDataBlock(raw);
851
891
  const data = parseData(raw);
@@ -867,6 +907,9 @@ function createYrestStorage(filePath) {
867
907
  getRoutes() {
868
908
  return routes;
869
909
  },
910
+ getSseRoutes() {
911
+ return sseRoutes;
912
+ },
870
913
  getCollection(name) {
871
914
  return data[name];
872
915
  },
@@ -978,6 +1021,8 @@ var METHOD_COLOR = {
978
1021
  PUT: "#d29922",
979
1022
  PATCH: "#a371f7",
980
1023
  DELETE: "#f85149",
1024
+ SSE: "#0ea5e9",
1025
+ WS: "#8b5cf6",
981
1026
  fn: "#f0883e"
982
1027
  };
983
1028
  function escapeHtml(str) {
@@ -1169,6 +1214,30 @@ function handlersAccordion(handlers, routes, base) {
1169
1214
  </tbody></table>
1170
1215
  </details>`;
1171
1216
  }
1217
+ function sseRoutesAccordion(routes, base) {
1218
+ if (!routes.length) return "";
1219
+ const rows = routes.map((r) => {
1220
+ const tags = [
1221
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
1222
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
1223
+ ];
1224
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
1225
+ if (r.repeat !== void 0)
1226
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
1227
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
1228
+ return endpointRow("SSE", `${base}${r.path}`, desc);
1229
+ });
1230
+ return `
1231
+ <details class="resource-card nested-card">
1232
+ <summary>
1233
+ <span class="resource-name">SSE streams</span>
1234
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
1235
+ </summary>
1236
+ <table><tbody>
1237
+ ${rows.join("")}
1238
+ </tbody></table>
1239
+ </details>`;
1240
+ }
1172
1241
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
1173
1242
  const examples = [];
1174
1243
  const firstCol = collections[0];
@@ -1839,6 +1908,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1839
1908
  const collections = Object.keys(storage.getData());
1840
1909
  const relations = storage.getRelations();
1841
1910
  const customRoutes = storage.getRoutes();
1911
+ const sseRoutes = storage.getSseRoutes();
1842
1912
  const base = options.base;
1843
1913
  const host = `http://${options.host}:${options.port}`;
1844
1914
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -2022,6 +2092,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
2022
2092
  ${accordions}
2023
2093
  ${nestedRoutesAccordion(relations, base)}
2024
2094
  ${options.snapshot ? snapshotAccordion() : ""}
2095
+ ${sseRoutesAccordion(sseRoutes, base)}
2025
2096
  ${customRoutesAccordion(customRoutes, base, handlers)}
2026
2097
  ${handlersAccordion(handlers, customRoutes, base)}
2027
2098
  </div>
@@ -3152,6 +3223,79 @@ var SnapshotRouteCommand = class {
3152
3223
  }
3153
3224
  };
3154
3225
 
3226
+ // src/router/routes/sse.routes.ts
3227
+ var SSERouteCommand = class {
3228
+ constructor(storage, base) {
3229
+ this.storage = storage;
3230
+ this.base = base;
3231
+ }
3232
+ storage;
3233
+ base;
3234
+ register(server) {
3235
+ for (const route of this.storage.getSseRoutes()) {
3236
+ const url = `${this.base}${route.path}`;
3237
+ server.route({
3238
+ method: "GET",
3239
+ url,
3240
+ handler: async (req, reply) => {
3241
+ reply.hijack();
3242
+ const raw = reply.raw;
3243
+ raw.setHeader("Content-Type", "text/event-stream");
3244
+ raw.setHeader("Cache-Control", "no-cache");
3245
+ raw.setHeader("Connection", "keep-alive");
3246
+ raw.setHeader("Access-Control-Allow-Origin", "*");
3247
+ raw.flushHeaders();
3248
+ let alive = true;
3249
+ req.raw.on("close", () => {
3250
+ alive = false;
3251
+ });
3252
+ const keepAlive = setInterval(() => {
3253
+ if (alive) raw.write(": ping\n\n");
3254
+ }, 15e3);
3255
+ let frameIndex = 0;
3256
+ let cycleCount = 0;
3257
+ function sendNext() {
3258
+ if (!alive) {
3259
+ clearInterval(keepAlive);
3260
+ return;
3261
+ }
3262
+ const ev = route.events[frameIndex];
3263
+ const ctx = {
3264
+ params: req.params,
3265
+ query: req.query,
3266
+ body: null,
3267
+ headers: req.headers
3268
+ };
3269
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
3270
+ let chunk = "";
3271
+ if (ev.event) chunk += `event: ${ev.event}
3272
+ `;
3273
+ chunk += `data: ${JSON.stringify(data)}
3274
+
3275
+ `;
3276
+ raw.write(chunk);
3277
+ frameIndex++;
3278
+ if (frameIndex >= route.events.length) {
3279
+ frameIndex = 0;
3280
+ cycleCount++;
3281
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
3282
+ if (done) {
3283
+ clearInterval(keepAlive);
3284
+ raw.end();
3285
+ return;
3286
+ }
3287
+ }
3288
+ if (alive) {
3289
+ setTimeout(sendNext, route.interval);
3290
+ }
3291
+ }
3292
+ sendNext();
3293
+ }
3294
+ });
3295
+ }
3296
+ }
3297
+ };
3298
+
3155
3299
  // src/router/resource.router.ts
3156
3300
  function buildResourceRouteCommands(storage, options) {
3157
3301
  const commands = [];
@@ -3193,6 +3337,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
3193
3337
  new AboutRouteCommand(storage, options, handlers),
3194
3338
  new OpenApiRouteCommand(storage, options),
3195
3339
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3340
+ new SSERouteCommand(storage, options.base),
3196
3341
  new CustomRouteCommand(storage, options.base, handlers),
3197
3342
  ...buildResourceRouteCommands(storage, options)
3198
3343
  ];
@@ -637,6 +637,8 @@ function normaliseRoute(raw) {
637
637
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
638
638
  const v = raw;
639
639
  const method = v["_method"];
640
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
641
+ if ("_sse" in v) return null;
640
642
  const path = v["_path"];
641
643
  if (typeof method !== "string" || typeof path !== "string") return null;
642
644
  const route = { method, path };
@@ -676,6 +678,43 @@ function normaliseScenario(raw) {
676
678
  return { when, response };
677
679
  }
678
680
 
681
+ // src/storage/parseSseRoutes.ts
682
+ function parseSseRoutes(raw) {
683
+ if (!Array.isArray(raw)) return [];
684
+ return raw.map(normaliseSseRoute).filter((r) => r !== null);
685
+ }
686
+ function normaliseSseRoute(raw) {
687
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
688
+ const v = raw;
689
+ const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
690
+ const path = v["_path"];
691
+ const sse = v["_sse"];
692
+ if (method !== "SSE") return null;
693
+ if (typeof path !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
694
+ return null;
695
+ }
696
+ const s = sse;
697
+ const rawEvents = s["_events"];
698
+ if (!Array.isArray(rawEvents)) return null;
699
+ const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
700
+ if (!events.length) return null;
701
+ return {
702
+ path,
703
+ interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
704
+ loop: s["_loop"] !== false,
705
+ repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
706
+ events
707
+ };
708
+ }
709
+ function normaliseEvent(raw) {
710
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
711
+ const v = raw;
712
+ if (!("_data" in v)) return null;
713
+ const event = { data: v["_data"] };
714
+ if (typeof v["_event"] === "string") event.event = v["_event"];
715
+ return event;
716
+ }
717
+
679
718
  // src/storage/parseSchema.ts
680
719
  function parseSchema(raw) {
681
720
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -819,6 +858,7 @@ function createYrestStorage(filePath) {
819
858
  const raw = parse(readFileSync(absPath, "utf8")) ?? {};
820
859
  const relations = parseRelations(raw["_rel"]);
821
860
  const routes = parseRoutes(raw["_routes"]);
861
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
822
862
  const schema = parseSchema(raw["_schema"]);
823
863
  const dataBlock = hasDataBlock(raw);
824
864
  const data = parseData(raw);
@@ -840,6 +880,9 @@ function createYrestStorage(filePath) {
840
880
  getRoutes() {
841
881
  return routes;
842
882
  },
883
+ getSseRoutes() {
884
+ return sseRoutes;
885
+ },
843
886
  getCollection(name) {
844
887
  return data[name];
845
888
  },
@@ -951,6 +994,8 @@ var METHOD_COLOR = {
951
994
  PUT: "#d29922",
952
995
  PATCH: "#a371f7",
953
996
  DELETE: "#f85149",
997
+ SSE: "#0ea5e9",
998
+ WS: "#8b5cf6",
954
999
  fn: "#f0883e"
955
1000
  };
956
1001
  function escapeHtml(str) {
@@ -1142,6 +1187,30 @@ function handlersAccordion(handlers, routes, base) {
1142
1187
  </tbody></table>
1143
1188
  </details>`;
1144
1189
  }
1190
+ function sseRoutesAccordion(routes, base) {
1191
+ if (!routes.length) return "";
1192
+ const rows = routes.map((r) => {
1193
+ const tags = [
1194
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
1195
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
1196
+ ];
1197
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
1198
+ if (r.repeat !== void 0)
1199
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
1200
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
1201
+ return endpointRow("SSE", `${base}${r.path}`, desc);
1202
+ });
1203
+ return `
1204
+ <details class="resource-card nested-card">
1205
+ <summary>
1206
+ <span class="resource-name">SSE streams</span>
1207
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
1208
+ </summary>
1209
+ <table><tbody>
1210
+ ${rows.join("")}
1211
+ </tbody></table>
1212
+ </details>`;
1213
+ }
1145
1214
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
1146
1215
  const examples = [];
1147
1216
  const firstCol = collections[0];
@@ -1812,6 +1881,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1812
1881
  const collections = Object.keys(storage.getData());
1813
1882
  const relations = storage.getRelations();
1814
1883
  const customRoutes = storage.getRoutes();
1884
+ const sseRoutes = storage.getSseRoutes();
1815
1885
  const base = options.base;
1816
1886
  const host = `http://${options.host}:${options.port}`;
1817
1887
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -1995,6 +2065,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1995
2065
  ${accordions}
1996
2066
  ${nestedRoutesAccordion(relations, base)}
1997
2067
  ${options.snapshot ? snapshotAccordion() : ""}
2068
+ ${sseRoutesAccordion(sseRoutes, base)}
1998
2069
  ${customRoutesAccordion(customRoutes, base, handlers)}
1999
2070
  ${handlersAccordion(handlers, customRoutes, base)}
2000
2071
  </div>
@@ -3125,6 +3196,79 @@ var SnapshotRouteCommand = class {
3125
3196
  }
3126
3197
  };
3127
3198
 
3199
+ // src/router/routes/sse.routes.ts
3200
+ var SSERouteCommand = class {
3201
+ constructor(storage, base) {
3202
+ this.storage = storage;
3203
+ this.base = base;
3204
+ }
3205
+ storage;
3206
+ base;
3207
+ register(server) {
3208
+ for (const route of this.storage.getSseRoutes()) {
3209
+ const url = `${this.base}${route.path}`;
3210
+ server.route({
3211
+ method: "GET",
3212
+ url,
3213
+ handler: async (req, reply) => {
3214
+ reply.hijack();
3215
+ const raw = reply.raw;
3216
+ raw.setHeader("Content-Type", "text/event-stream");
3217
+ raw.setHeader("Cache-Control", "no-cache");
3218
+ raw.setHeader("Connection", "keep-alive");
3219
+ raw.setHeader("Access-Control-Allow-Origin", "*");
3220
+ raw.flushHeaders();
3221
+ let alive = true;
3222
+ req.raw.on("close", () => {
3223
+ alive = false;
3224
+ });
3225
+ const keepAlive = setInterval(() => {
3226
+ if (alive) raw.write(": ping\n\n");
3227
+ }, 15e3);
3228
+ let frameIndex = 0;
3229
+ let cycleCount = 0;
3230
+ function sendNext() {
3231
+ if (!alive) {
3232
+ clearInterval(keepAlive);
3233
+ return;
3234
+ }
3235
+ const ev = route.events[frameIndex];
3236
+ const ctx = {
3237
+ params: req.params,
3238
+ query: req.query,
3239
+ body: null,
3240
+ headers: req.headers
3241
+ };
3242
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
3243
+ let chunk = "";
3244
+ if (ev.event) chunk += `event: ${ev.event}
3245
+ `;
3246
+ chunk += `data: ${JSON.stringify(data)}
3247
+
3248
+ `;
3249
+ raw.write(chunk);
3250
+ frameIndex++;
3251
+ if (frameIndex >= route.events.length) {
3252
+ frameIndex = 0;
3253
+ cycleCount++;
3254
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
3255
+ if (done) {
3256
+ clearInterval(keepAlive);
3257
+ raw.end();
3258
+ return;
3259
+ }
3260
+ }
3261
+ if (alive) {
3262
+ setTimeout(sendNext, route.interval);
3263
+ }
3264
+ }
3265
+ sendNext();
3266
+ }
3267
+ });
3268
+ }
3269
+ }
3270
+ };
3271
+
3128
3272
  // src/router/resource.router.ts
3129
3273
  function buildResourceRouteCommands(storage, options) {
3130
3274
  const commands = [];
@@ -3166,6 +3310,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
3166
3310
  new AboutRouteCommand(storage, options, handlers),
3167
3311
  new OpenApiRouteCommand(storage, options),
3168
3312
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3313
+ new SSERouteCommand(storage, options.base),
3169
3314
  new CustomRouteCommand(storage, options.base, handlers),
3170
3315
  ...buildResourceRouteCommands(storage, options)
3171
3316
  ];
package/dist/index.d.mts CHANGED
@@ -284,6 +284,42 @@ type CustomRoute = {
284
284
  /** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
285
285
  errorBody?: unknown;
286
286
  };
287
+ /** A single event in an SSE stream declared under `_sse._events`. */
288
+ type SseEvent = {
289
+ /** SSE event name emitted as `event: <name>`. Omit for unnamed events. */
290
+ event?: string;
291
+ /** Payload serialised as JSON in the `data:` line. Supports `{{}}` templates. */
292
+ data: unknown;
293
+ };
294
+ /**
295
+ * A Server-Sent Events stream route declared under `_routes` with an `_sse` block.
296
+ *
297
+ * @example
298
+ * ```yaml
299
+ * _routes:
300
+ * - _method: GET
301
+ * _path: /events/orders
302
+ * _sse:
303
+ * _interval: 1500
304
+ * _loop: true
305
+ * _repeat: 5
306
+ * _events:
307
+ * - _event: update
308
+ * _data: { orderId: 1, status: processing }
309
+ * ```
310
+ */
311
+ type SseRoute = {
312
+ /** URL path. May include Fastify path params (e.g. `/events/:id`). */
313
+ path: string;
314
+ /** Milliseconds between frames. Default: `1000`. */
315
+ interval: number;
316
+ /** Restart the sequence after the last event. Default: `true`. */
317
+ loop: boolean;
318
+ /** Stop after this many full cycles. Omit for infinite. */
319
+ repeat?: number;
320
+ /** Ordered list of events to emit. */
321
+ events: SseEvent[];
322
+ };
287
323
  /**
288
324
  * In-memory store backed by a YAML file.
289
325
  *
@@ -336,6 +372,11 @@ interface YrestStorage {
336
372
  * Returns an empty array if no `_routes` block is present.
337
373
  */
338
374
  getRoutes(): CustomRoute[];
375
+ /**
376
+ * Returns the SSE stream routes declared under `_routes` (entries with an `_sse` block).
377
+ * Returns an empty array if none are present.
378
+ */
379
+ getSseRoutes(): SseRoute[];
339
380
  /**
340
381
  * Returns the saved snapshot: a frozen copy of `data` and `relations` taken
341
382
  * at the last call to {@link saveSnapshot} (or at construction time).
package/dist/index.d.ts CHANGED
@@ -284,6 +284,42 @@ type CustomRoute = {
284
284
  /** Optional body to return alongside `error:`. Defaults to `{ error: "Forced error <status>" }`. */
285
285
  errorBody?: unknown;
286
286
  };
287
+ /** A single event in an SSE stream declared under `_sse._events`. */
288
+ type SseEvent = {
289
+ /** SSE event name emitted as `event: <name>`. Omit for unnamed events. */
290
+ event?: string;
291
+ /** Payload serialised as JSON in the `data:` line. Supports `{{}}` templates. */
292
+ data: unknown;
293
+ };
294
+ /**
295
+ * A Server-Sent Events stream route declared under `_routes` with an `_sse` block.
296
+ *
297
+ * @example
298
+ * ```yaml
299
+ * _routes:
300
+ * - _method: GET
301
+ * _path: /events/orders
302
+ * _sse:
303
+ * _interval: 1500
304
+ * _loop: true
305
+ * _repeat: 5
306
+ * _events:
307
+ * - _event: update
308
+ * _data: { orderId: 1, status: processing }
309
+ * ```
310
+ */
311
+ type SseRoute = {
312
+ /** URL path. May include Fastify path params (e.g. `/events/:id`). */
313
+ path: string;
314
+ /** Milliseconds between frames. Default: `1000`. */
315
+ interval: number;
316
+ /** Restart the sequence after the last event. Default: `true`. */
317
+ loop: boolean;
318
+ /** Stop after this many full cycles. Omit for infinite. */
319
+ repeat?: number;
320
+ /** Ordered list of events to emit. */
321
+ events: SseEvent[];
322
+ };
287
323
  /**
288
324
  * In-memory store backed by a YAML file.
289
325
  *
@@ -336,6 +372,11 @@ interface YrestStorage {
336
372
  * Returns an empty array if no `_routes` block is present.
337
373
  */
338
374
  getRoutes(): CustomRoute[];
375
+ /**
376
+ * Returns the SSE stream routes declared under `_routes` (entries with an `_sse` block).
377
+ * Returns an empty array if none are present.
378
+ */
379
+ getSseRoutes(): SseRoute[];
339
380
  /**
340
381
  * Returns the saved snapshot: a frozen copy of `data` and `relations` taken
341
382
  * at the last call to {@link saveSnapshot} (or at construction time).
package/dist/index.js CHANGED
@@ -153,6 +153,49 @@ var init_parseRelations = __esm({
153
153
  }
154
154
  });
155
155
 
156
+ // src/storage/parseSseRoutes.ts
157
+ function parseSseRoutes(raw) {
158
+ if (!Array.isArray(raw)) return [];
159
+ return raw.map(normaliseSseRoute).filter((r) => r !== null);
160
+ }
161
+ function normaliseSseRoute(raw) {
162
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
163
+ const v = raw;
164
+ const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
165
+ const path = v["_path"];
166
+ const sse = v["_sse"];
167
+ if (method !== "SSE") return null;
168
+ if (typeof path !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
169
+ return null;
170
+ }
171
+ const s = sse;
172
+ const rawEvents = s["_events"];
173
+ if (!Array.isArray(rawEvents)) return null;
174
+ const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
175
+ if (!events.length) return null;
176
+ return {
177
+ path,
178
+ interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
179
+ loop: s["_loop"] !== false,
180
+ repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
181
+ events
182
+ };
183
+ }
184
+ function normaliseEvent(raw) {
185
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
186
+ const v = raw;
187
+ if (!("_data" in v)) return null;
188
+ const event = { data: v["_data"] };
189
+ if (typeof v["_event"] === "string") event.event = v["_event"];
190
+ return event;
191
+ }
192
+ var init_parseSseRoutes = __esm({
193
+ "src/storage/parseSseRoutes.ts"() {
194
+ "use strict";
195
+ init_cjs_shims();
196
+ }
197
+ });
198
+
156
199
  // src/storage/parseSchema.ts
157
200
  function parseSchema(raw) {
158
201
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -241,6 +284,8 @@ function normaliseRoute(raw) {
241
284
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
242
285
  const v = raw;
243
286
  const method = v["_method"];
287
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
288
+ if ("_sse" in v) return null;
244
289
  const path = v["_path"];
245
290
  if (typeof method !== "string" || typeof path !== "string") return null;
246
291
  const route = { method, path };
@@ -374,6 +419,7 @@ function createYrestStorage(filePath) {
374
419
  const raw = (0, import_yaml3.parse)((0, import_node_fs3.readFileSync)(absPath, "utf8")) ?? {};
375
420
  const relations = parseRelations(raw["_rel"]);
376
421
  const routes = parseRoutes(raw["_routes"]);
422
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
377
423
  const schema = parseSchema(raw["_schema"]);
378
424
  const dataBlock = hasDataBlock(raw);
379
425
  const data = parseData(raw);
@@ -395,6 +441,9 @@ function createYrestStorage(filePath) {
395
441
  getRoutes() {
396
442
  return routes;
397
443
  },
444
+ getSseRoutes() {
445
+ return sseRoutes;
446
+ },
398
447
  getCollection(name) {
399
448
  return data[name];
400
449
  },
@@ -454,6 +503,7 @@ var init_yrestStorage = __esm({
454
503
  import_yaml3 = require("yaml");
455
504
  init_parseRelations();
456
505
  init_parseRoutes();
506
+ init_parseSseRoutes();
457
507
  init_parseSchema();
458
508
  init_parseData();
459
509
  init_deepCopy();
@@ -519,6 +569,7 @@ function dedent(str) {
519
569
  init_cjs_shims();
520
570
  var import_node_path3 = require("path");
521
571
  init_parseRelations();
572
+ init_parseSseRoutes();
522
573
  init_parseSchema();
523
574
 
524
575
  // src/utils/handlers.ts
@@ -630,6 +681,8 @@ var METHOD_COLOR = {
630
681
  PUT: "#d29922",
631
682
  PATCH: "#a371f7",
632
683
  DELETE: "#f85149",
684
+ SSE: "#0ea5e9",
685
+ WS: "#8b5cf6",
633
686
  fn: "#f0883e"
634
687
  };
635
688
  function escapeHtml(str) {
@@ -821,6 +874,30 @@ function handlersAccordion(handlers, routes, base) {
821
874
  </tbody></table>
822
875
  </details>`;
823
876
  }
877
+ function sseRoutesAccordion(routes, base) {
878
+ if (!routes.length) return "";
879
+ const rows = routes.map((r) => {
880
+ const tags = [
881
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
882
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
883
+ ];
884
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
885
+ if (r.repeat !== void 0)
886
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
887
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
888
+ return endpointRow("SSE", `${base}${r.path}`, desc);
889
+ });
890
+ return `
891
+ <details class="resource-card nested-card">
892
+ <summary>
893
+ <span class="resource-name">SSE streams</span>
894
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
895
+ </summary>
896
+ <table><tbody>
897
+ ${rows.join("")}
898
+ </tbody></table>
899
+ </details>`;
900
+ }
824
901
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
825
902
  const examples = [];
826
903
  const firstCol = collections[0];
@@ -1493,6 +1570,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1493
1570
  const collections = Object.keys(storage.getData());
1494
1571
  const relations = storage.getRelations();
1495
1572
  const customRoutes = storage.getRoutes();
1573
+ const sseRoutes = storage.getSseRoutes();
1496
1574
  const base = options.base;
1497
1575
  const host = `http://${options.host}:${options.port}`;
1498
1576
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -1676,6 +1754,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1676
1754
  ${accordions}
1677
1755
  ${nestedRoutesAccordion(relations, base)}
1678
1756
  ${options.snapshot ? snapshotAccordion() : ""}
1757
+ ${sseRoutesAccordion(sseRoutes, base)}
1679
1758
  ${customRoutesAccordion(customRoutes, base, handlers)}
1680
1759
  ${handlersAccordion(handlers, customRoutes, base)}
1681
1760
  </div>
@@ -2828,6 +2907,80 @@ var SnapshotRouteCommand = class {
2828
2907
  }
2829
2908
  };
2830
2909
 
2910
+ // src/router/routes/sse.routes.ts
2911
+ init_cjs_shims();
2912
+ var SSERouteCommand = class {
2913
+ constructor(storage, base) {
2914
+ this.storage = storage;
2915
+ this.base = base;
2916
+ }
2917
+ storage;
2918
+ base;
2919
+ register(server) {
2920
+ for (const route of this.storage.getSseRoutes()) {
2921
+ const url = `${this.base}${route.path}`;
2922
+ server.route({
2923
+ method: "GET",
2924
+ url,
2925
+ handler: async (req, reply) => {
2926
+ reply.hijack();
2927
+ const raw = reply.raw;
2928
+ raw.setHeader("Content-Type", "text/event-stream");
2929
+ raw.setHeader("Cache-Control", "no-cache");
2930
+ raw.setHeader("Connection", "keep-alive");
2931
+ raw.setHeader("Access-Control-Allow-Origin", "*");
2932
+ raw.flushHeaders();
2933
+ let alive = true;
2934
+ req.raw.on("close", () => {
2935
+ alive = false;
2936
+ });
2937
+ const keepAlive = setInterval(() => {
2938
+ if (alive) raw.write(": ping\n\n");
2939
+ }, 15e3);
2940
+ let frameIndex = 0;
2941
+ let cycleCount = 0;
2942
+ function sendNext() {
2943
+ if (!alive) {
2944
+ clearInterval(keepAlive);
2945
+ return;
2946
+ }
2947
+ const ev = route.events[frameIndex];
2948
+ const ctx = {
2949
+ params: req.params,
2950
+ query: req.query,
2951
+ body: null,
2952
+ headers: req.headers
2953
+ };
2954
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
2955
+ let chunk = "";
2956
+ if (ev.event) chunk += `event: ${ev.event}
2957
+ `;
2958
+ chunk += `data: ${JSON.stringify(data)}
2959
+
2960
+ `;
2961
+ raw.write(chunk);
2962
+ frameIndex++;
2963
+ if (frameIndex >= route.events.length) {
2964
+ frameIndex = 0;
2965
+ cycleCount++;
2966
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
2967
+ if (done) {
2968
+ clearInterval(keepAlive);
2969
+ raw.end();
2970
+ return;
2971
+ }
2972
+ }
2973
+ if (alive) {
2974
+ setTimeout(sendNext, route.interval);
2975
+ }
2976
+ }
2977
+ sendNext();
2978
+ }
2979
+ });
2980
+ }
2981
+ }
2982
+ };
2983
+
2831
2984
  // src/router/resource.router.ts
2832
2985
  function buildResourceRouteCommands(storage, options) {
2833
2986
  const commands = [];
@@ -2869,6 +3022,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
2869
3022
  new AboutRouteCommand(storage, options, handlers),
2870
3023
  new OpenApiRouteCommand(storage, options),
2871
3024
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
3025
+ new SSERouteCommand(storage, options.base),
2872
3026
  new CustomRouteCommand(storage, options.base, handlers),
2873
3027
  ...buildResourceRouteCommands(storage, options)
2874
3028
  ];
@@ -2950,6 +3104,7 @@ function createInMemoryStorage(data) {
2950
3104
  const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2951
3105
  const relations = parseRelations(raw["_rel"]);
2952
3106
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
3107
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
2953
3108
  const schema = parseSchema(raw["_schema"]);
2954
3109
  const collections = Object.fromEntries(
2955
3110
  Object.entries(raw).filter(([k]) => !RESERVED2.has(k))
@@ -2964,6 +3119,7 @@ function createInMemoryStorage(data) {
2964
3119
  getRelations: () => relations,
2965
3120
  getSchema: () => schema,
2966
3121
  getRoutes: () => routes,
3122
+ getSseRoutes: () => sseRoutes,
2967
3123
  getCollection: (name) => collections[name],
2968
3124
  setCollection: (name, items) => {
2969
3125
  collections[name] = items;
package/dist/index.mjs CHANGED
@@ -130,6 +130,49 @@ var init_parseRelations = __esm({
130
130
  }
131
131
  });
132
132
 
133
+ // src/storage/parseSseRoutes.ts
134
+ function parseSseRoutes(raw) {
135
+ if (!Array.isArray(raw)) return [];
136
+ return raw.map(normaliseSseRoute).filter((r) => r !== null);
137
+ }
138
+ function normaliseSseRoute(raw) {
139
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
140
+ const v = raw;
141
+ const method = typeof v["_method"] === "string" ? v["_method"].toUpperCase() : null;
142
+ const path2 = v["_path"];
143
+ const sse = v["_sse"];
144
+ if (method !== "SSE") return null;
145
+ if (typeof path2 !== "string" || !sse || typeof sse !== "object" || Array.isArray(sse)) {
146
+ return null;
147
+ }
148
+ const s = sse;
149
+ const rawEvents = s["_events"];
150
+ if (!Array.isArray(rawEvents)) return null;
151
+ const events = rawEvents.map(normaliseEvent).filter((e) => e !== null);
152
+ if (!events.length) return null;
153
+ return {
154
+ path: path2,
155
+ interval: typeof s["_interval"] === "number" ? s["_interval"] : 1e3,
156
+ loop: s["_loop"] !== false,
157
+ repeat: typeof s["_repeat"] === "number" ? s["_repeat"] : void 0,
158
+ events
159
+ };
160
+ }
161
+ function normaliseEvent(raw) {
162
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
163
+ const v = raw;
164
+ if (!("_data" in v)) return null;
165
+ const event = { data: v["_data"] };
166
+ if (typeof v["_event"] === "string") event.event = v["_event"];
167
+ return event;
168
+ }
169
+ var init_parseSseRoutes = __esm({
170
+ "src/storage/parseSseRoutes.ts"() {
171
+ "use strict";
172
+ init_esm_shims();
173
+ }
174
+ });
175
+
133
176
  // src/storage/parseSchema.ts
134
177
  function parseSchema(raw) {
135
178
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
@@ -218,6 +261,8 @@ function normaliseRoute(raw) {
218
261
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
219
262
  const v = raw;
220
263
  const method = v["_method"];
264
+ if (typeof method === "string" && method.toUpperCase() === "SSE") return null;
265
+ if ("_sse" in v) return null;
221
266
  const path2 = v["_path"];
222
267
  if (typeof method !== "string" || typeof path2 !== "string") return null;
223
268
  const route = { method, path: path2 };
@@ -355,6 +400,7 @@ function createYrestStorage(filePath) {
355
400
  const raw = parse2(readFileSync2(absPath, "utf8")) ?? {};
356
401
  const relations = parseRelations(raw["_rel"]);
357
402
  const routes = parseRoutes(raw["_routes"]);
403
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
358
404
  const schema = parseSchema(raw["_schema"]);
359
405
  const dataBlock = hasDataBlock(raw);
360
406
  const data = parseData(raw);
@@ -376,6 +422,9 @@ function createYrestStorage(filePath) {
376
422
  getRoutes() {
377
423
  return routes;
378
424
  },
425
+ getSseRoutes() {
426
+ return sseRoutes;
427
+ },
379
428
  getCollection(name) {
380
429
  return data[name];
381
430
  },
@@ -430,6 +479,7 @@ var init_yrestStorage = __esm({
430
479
  init_esm_shims();
431
480
  init_parseRelations();
432
481
  init_parseRoutes();
482
+ init_parseSseRoutes();
433
483
  init_parseSchema();
434
484
  init_parseData();
435
485
  init_deepCopy();
@@ -484,6 +534,7 @@ function dedent(str) {
484
534
  // src/api/yrestServer.ts
485
535
  init_esm_shims();
486
536
  init_parseRelations();
537
+ init_parseSseRoutes();
487
538
  init_parseSchema();
488
539
  import { resolve as resolve2 } from "path";
489
540
 
@@ -596,6 +647,8 @@ var METHOD_COLOR = {
596
647
  PUT: "#d29922",
597
648
  PATCH: "#a371f7",
598
649
  DELETE: "#f85149",
650
+ SSE: "#0ea5e9",
651
+ WS: "#8b5cf6",
599
652
  fn: "#f0883e"
600
653
  };
601
654
  function escapeHtml(str) {
@@ -787,6 +840,30 @@ function handlersAccordion(handlers, routes, base) {
787
840
  </tbody></table>
788
841
  </details>`;
789
842
  }
843
+ function sseRoutesAccordion(routes, base) {
844
+ if (!routes.length) return "";
845
+ const rows = routes.map((r) => {
846
+ const tags = [
847
+ `<span style="color:#0ea5e9;font-size:11px">interval\xB7${r.interval}ms</span>`,
848
+ `<span style="color:var(--text-muted);font-size:11px">${r.events.length} event${r.events.length !== 1 ? "s" : ""}</span>`
849
+ ];
850
+ if (!r.loop) tags.push(`<span style="color:var(--text-muted);font-size:11px">no\xB7loop</span>`);
851
+ if (r.repeat !== void 0)
852
+ tags.push(`<span style="color:var(--text-muted);font-size:11px">repeat\xB7${r.repeat}\xD7</span>`);
853
+ const desc = `SSE stream&ensp;${tags.join("&ensp;")}`;
854
+ return endpointRow("SSE", `${base}${r.path}`, desc);
855
+ });
856
+ return `
857
+ <details class="resource-card nested-card">
858
+ <summary>
859
+ <span class="resource-name">SSE streams</span>
860
+ <span class="route-count">${routes.length} stream${routes.length !== 1 ? "s" : ""}</span>
861
+ </summary>
862
+ <table><tbody>
863
+ ${rows.join("")}
864
+ </tbody></table>
865
+ </details>`;
866
+ }
790
867
  function examplesBlock(collections, relations, base, host, options, firstCustomRoute) {
791
868
  const examples = [];
792
869
  const firstCol = collections[0];
@@ -1459,6 +1536,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1459
1536
  const collections = Object.keys(storage.getData());
1460
1537
  const relations = storage.getRelations();
1461
1538
  const customRoutes = storage.getRoutes();
1539
+ const sseRoutes = storage.getSseRoutes();
1462
1540
  const base = options.base;
1463
1541
  const host = `http://${options.host}:${options.port}`;
1464
1542
  const swaggerUrl = `https://petstore.swagger.io/?url=${encodeURIComponent(`${host}/_openapi.json`)}`;
@@ -1642,6 +1720,7 @@ function generateAboutHtml(storage, options, handlers = /* @__PURE__ */ new Map(
1642
1720
  ${accordions}
1643
1721
  ${nestedRoutesAccordion(relations, base)}
1644
1722
  ${options.snapshot ? snapshotAccordion() : ""}
1723
+ ${sseRoutesAccordion(sseRoutes, base)}
1645
1724
  ${customRoutesAccordion(customRoutes, base, handlers)}
1646
1725
  ${handlersAccordion(handlers, customRoutes, base)}
1647
1726
  </div>
@@ -2794,6 +2873,80 @@ var SnapshotRouteCommand = class {
2794
2873
  }
2795
2874
  };
2796
2875
 
2876
+ // src/router/routes/sse.routes.ts
2877
+ init_esm_shims();
2878
+ var SSERouteCommand = class {
2879
+ constructor(storage, base) {
2880
+ this.storage = storage;
2881
+ this.base = base;
2882
+ }
2883
+ storage;
2884
+ base;
2885
+ register(server) {
2886
+ for (const route of this.storage.getSseRoutes()) {
2887
+ const url = `${this.base}${route.path}`;
2888
+ server.route({
2889
+ method: "GET",
2890
+ url,
2891
+ handler: async (req, reply) => {
2892
+ reply.hijack();
2893
+ const raw = reply.raw;
2894
+ raw.setHeader("Content-Type", "text/event-stream");
2895
+ raw.setHeader("Cache-Control", "no-cache");
2896
+ raw.setHeader("Connection", "keep-alive");
2897
+ raw.setHeader("Access-Control-Allow-Origin", "*");
2898
+ raw.flushHeaders();
2899
+ let alive = true;
2900
+ req.raw.on("close", () => {
2901
+ alive = false;
2902
+ });
2903
+ const keepAlive = setInterval(() => {
2904
+ if (alive) raw.write(": ping\n\n");
2905
+ }, 15e3);
2906
+ let frameIndex = 0;
2907
+ let cycleCount = 0;
2908
+ function sendNext() {
2909
+ if (!alive) {
2910
+ clearInterval(keepAlive);
2911
+ return;
2912
+ }
2913
+ const ev = route.events[frameIndex];
2914
+ const ctx = {
2915
+ params: req.params,
2916
+ query: req.query,
2917
+ body: null,
2918
+ headers: req.headers
2919
+ };
2920
+ const data = hasTemplates(ev.data) ? interpolate(ev.data, ctx) : ev.data;
2921
+ let chunk = "";
2922
+ if (ev.event) chunk += `event: ${ev.event}
2923
+ `;
2924
+ chunk += `data: ${JSON.stringify(data)}
2925
+
2926
+ `;
2927
+ raw.write(chunk);
2928
+ frameIndex++;
2929
+ if (frameIndex >= route.events.length) {
2930
+ frameIndex = 0;
2931
+ cycleCount++;
2932
+ const done = !route.loop || route.repeat !== void 0 && cycleCount >= route.repeat;
2933
+ if (done) {
2934
+ clearInterval(keepAlive);
2935
+ raw.end();
2936
+ return;
2937
+ }
2938
+ }
2939
+ if (alive) {
2940
+ setTimeout(sendNext, route.interval);
2941
+ }
2942
+ }
2943
+ sendNext();
2944
+ }
2945
+ });
2946
+ }
2947
+ }
2948
+ };
2949
+
2797
2950
  // src/router/resource.router.ts
2798
2951
  function buildResourceRouteCommands(storage, options) {
2799
2952
  const commands = [];
@@ -2835,6 +2988,7 @@ async function createServer(storage, options, handlers = /* @__PURE__ */ new Map
2835
2988
  new AboutRouteCommand(storage, options, handlers),
2836
2989
  new OpenApiRouteCommand(storage, options),
2837
2990
  ...options.snapshot ? [new SnapshotRouteCommand(storage)] : [],
2991
+ new SSERouteCommand(storage, options.base),
2838
2992
  new CustomRouteCommand(storage, options.base, handlers),
2839
2993
  ...buildResourceRouteCommands(storage, options)
2840
2994
  ];
@@ -2916,6 +3070,7 @@ function createInMemoryStorage(data) {
2916
3070
  const RESERVED2 = /* @__PURE__ */ new Set(["_rel", "_routes", "_schema"]);
2917
3071
  const relations = parseRelations(raw["_rel"]);
2918
3072
  const routes = Array.isArray(raw["_routes"]) ? raw["_routes"] : [];
3073
+ const sseRoutes = parseSseRoutes(raw["_routes"]);
2919
3074
  const schema = parseSchema(raw["_schema"]);
2920
3075
  const collections = Object.fromEntries(
2921
3076
  Object.entries(raw).filter(([k]) => !RESERVED2.has(k))
@@ -2930,6 +3085,7 @@ function createInMemoryStorage(data) {
2930
3085
  getRelations: () => relations,
2931
3086
  getSchema: () => schema,
2932
3087
  getRoutes: () => routes,
3088
+ getSseRoutes: () => sseRoutes,
2933
3089
  getCollection: (name) => collections[name],
2934
3090
  setCollection: (name, items) => {
2935
3091
  collections[name] = items;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yrest/cli",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "YAML-powered json-server alternative. Zero-config REST API mock server with full CRUD, relations, filters and snapshots from a db.yml file.",
5
5
  "keywords": [
6
6
  "yrest",