@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 +75 -1
- package/dist/cli/index.js +145 -0
- package/dist/cli/index.mjs +145 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +156 -0
- package/dist/index.mjs +156 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://github.com/aggiovato/yRest/actions)
|
|
11
11
|
[](https://www.npmjs.com/package/@yrest/cli)
|
|
12
12
|
[](https://www.typescriptlang.org/)
|
|
13
|
-
[](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 ${tags.join(" ")}`;
|
|
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
|
];
|
package/dist/cli/index.mjs
CHANGED
|
@@ -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 ${tags.join(" ")}`;
|
|
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 ${tags.join(" ")}`;
|
|
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 ${tags.join(" ")}`;
|
|
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