@objectstack/metadata 4.1.1 → 4.2.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/dist/node.js CHANGED
@@ -1,9 +1,162 @@
1
1
  var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
2
6
  var __export = (target, all) => {
3
7
  for (var name in all)
4
8
  __defProp(target, name, { get: all[name], enumerable: true });
5
9
  };
6
10
 
11
+ // src/routes/hmr-routes.ts
12
+ var hmr_routes_exports = {};
13
+ __export(hmr_routes_exports, {
14
+ registerMetadataHmrRoutes: () => registerMetadataHmrRoutes
15
+ });
16
+ function registerMetadataHmrRoutes(app, manager, options = {}) {
17
+ const routePath = options.path ?? "/api/v1/dev/metadata-events";
18
+ const listeners = /* @__PURE__ */ new Set();
19
+ const broadcast = (evt) => {
20
+ for (const l of listeners) {
21
+ try {
22
+ l(evt);
23
+ } catch {
24
+ }
25
+ }
26
+ };
27
+ let fsHookInstalled = false;
28
+ const installFsHooks = async () => {
29
+ if (fsHookInstalled) return;
30
+ const mgr = manager;
31
+ if (typeof mgr.subscribe !== "function") {
32
+ fsHookInstalled = true;
33
+ return;
34
+ }
35
+ const types = await manager.getRegisteredTypes();
36
+ for (const type of types) {
37
+ mgr.subscribe(type, (evt) => {
38
+ const ts = typeof evt.timestamp === "string" ? Date.parse(evt.timestamp) : evt.timestamp ?? Date.now();
39
+ broadcast({
40
+ kind: "metadata-change",
41
+ type: evt.type ?? "changed",
42
+ metadataType: evt.metadataType ?? type,
43
+ name: evt.name ?? "",
44
+ path: evt.path,
45
+ timestamp: Number.isFinite(ts) ? ts : Date.now()
46
+ });
47
+ });
48
+ }
49
+ fsHookInstalled = true;
50
+ };
51
+ installFsHooks().catch(() => {
52
+ });
53
+ let onPostReload = null;
54
+ app.get(routePath, async (c) => {
55
+ await installFsHooks().catch(() => {
56
+ });
57
+ const types = await manager.getRegisteredTypes().catch(() => []);
58
+ const stream = new ReadableStream({
59
+ async start(controller) {
60
+ const enc = new TextEncoder();
61
+ let closed = false;
62
+ const safeEnqueue = (chunk) => {
63
+ if (closed) return;
64
+ try {
65
+ controller.enqueue(enc.encode(chunk));
66
+ } catch {
67
+ closed = true;
68
+ }
69
+ };
70
+ const listener = (evt) => {
71
+ if (closed) return;
72
+ const eventName = evt.kind === "reload" ? "reload" : "metadata-change";
73
+ safeEnqueue(`event: ${eventName}
74
+ data: ${JSON.stringify(evt)}
75
+
76
+ `);
77
+ };
78
+ listeners.add(listener);
79
+ safeEnqueue(`event: ready
80
+ data: ${JSON.stringify({ types, timestamp: Date.now() })}
81
+
82
+ `);
83
+ const heartbeat = setInterval(() => {
84
+ safeEnqueue(`: ping ${Date.now()}
85
+
86
+ `);
87
+ }, 15e3);
88
+ const cleanup = () => {
89
+ if (closed) return;
90
+ closed = true;
91
+ clearInterval(heartbeat);
92
+ listeners.delete(listener);
93
+ try {
94
+ controller.close();
95
+ } catch {
96
+ }
97
+ };
98
+ const signal = c.req?.raw?.signal;
99
+ if (signal) {
100
+ if (signal.aborted) cleanup();
101
+ else signal.addEventListener("abort", cleanup, { once: true });
102
+ }
103
+ }
104
+ });
105
+ return new Response(stream, {
106
+ status: 200,
107
+ headers: {
108
+ "Content-Type": "text/event-stream; charset=utf-8",
109
+ "Cache-Control": "no-cache, no-transform",
110
+ "Connection": "keep-alive",
111
+ "X-Accel-Buffering": "no"
112
+ }
113
+ });
114
+ });
115
+ app.post(routePath, async (c) => {
116
+ let body = {};
117
+ try {
118
+ const ct = c.req?.header?.("content-type") ?? "";
119
+ if (typeof c.req?.json === "function" && ct.includes("json")) {
120
+ body = await c.req.json();
121
+ }
122
+ } catch {
123
+ }
124
+ try {
125
+ if (onPostReload) await onPostReload(body);
126
+ } catch (e) {
127
+ return new Response(
128
+ JSON.stringify({ ok: false, error: e?.message ?? "reload handler failed" }),
129
+ { status: 500, headers: { "Content-Type": "application/json" } }
130
+ );
131
+ }
132
+ const reason = body.reason ?? "manual-trigger";
133
+ broadcast({
134
+ kind: "reload",
135
+ reason,
136
+ changed: body.changed,
137
+ timestamp: Date.now()
138
+ });
139
+ return new Response(
140
+ JSON.stringify({ ok: true, listeners: listeners.size, reason }),
141
+ { status: 200, headers: { "Content-Type": "application/json" } }
142
+ );
143
+ });
144
+ return {
145
+ broadcastReload(reason, changed) {
146
+ broadcast({ kind: "reload", reason, changed, timestamp: Date.now() });
147
+ },
148
+ setOnPostReload(fn) {
149
+ onPostReload = fn;
150
+ },
151
+ listenerCount: () => listeners.size
152
+ };
153
+ }
154
+ var init_hmr_routes = __esm({
155
+ "src/routes/hmr-routes.ts"() {
156
+ "use strict";
157
+ }
158
+ });
159
+
7
160
  // src/metadata-manager.ts
8
161
  import { createLogger } from "@objectstack/core";
9
162
 
@@ -1773,6 +1926,21 @@ var _MetadataManager = class _MetadataManager {
1773
1926
  unsubscribe: () => this.removeWatchCallback(type, wrappedCallback)
1774
1927
  };
1775
1928
  }
1929
+ /**
1930
+ * Subscribe to raw metadata watch events for a given type.
1931
+ *
1932
+ * Unlike `watchService` (which maps to the IMetadataService contract and
1933
+ * drops fields like `path`/`timestamp`), this returns the raw
1934
+ * `MetadataWatchEvent` produced by the underlying watcher — useful for
1935
+ * developer-facing tooling such as the HMR SSE endpoint that wants the
1936
+ * source file path and original timestamp.
1937
+ *
1938
+ * @returns An unsubscribe function.
1939
+ */
1940
+ subscribe(type, callback) {
1941
+ this.addWatchCallback(type, callback);
1942
+ return () => this.removeWatchCallback(type, callback);
1943
+ }
1776
1944
  // ==========================================
1777
1945
  // Import / Export
1778
1946
  // ==========================================
@@ -2791,6 +2959,33 @@ var MetadataPlugin = class {
2791
2959
  error: e.message
2792
2960
  });
2793
2961
  }
2962
+ try {
2963
+ const httpServer = ctx.getService("http-server") ?? ctx.getService("http.server");
2964
+ if (httpServer && typeof httpServer.getRawApp === "function") {
2965
+ const { registerMetadataHmrRoutes: registerMetadataHmrRoutes2 } = await Promise.resolve().then(() => (init_hmr_routes(), hmr_routes_exports));
2966
+ const hub = registerMetadataHmrRoutes2(httpServer.getRawApp(), this.manager);
2967
+ hub.setOnPostReload(async (body = {}) => {
2968
+ const src2 = this.options.artifactSource;
2969
+ if (src2?.mode === "local-file") {
2970
+ try {
2971
+ await this._loadFromLocalFile(ctx, src2.path, src2.fetchTimeoutMs);
2972
+ ctx.logger.info("[MetadataPlugin] artifact reloaded via HMR POST", {
2973
+ path: src2.path,
2974
+ reason: body?.reason
2975
+ });
2976
+ } catch (e) {
2977
+ ctx.logger.warn("[MetadataPlugin] artifact reload failed", { error: e?.message });
2978
+ throw e;
2979
+ }
2980
+ }
2981
+ });
2982
+ console.log("[MetadataPlugin] HMR endpoint registered at /api/v1/dev/metadata-events");
2983
+ } else {
2984
+ console.log("[MetadataPlugin] HTTP server with getRawApp() not available \u2014 skipping HMR endpoint");
2985
+ }
2986
+ } catch (e) {
2987
+ console.warn("[MetadataPlugin] Failed to register HMR endpoint", e?.message);
2988
+ }
2794
2989
  };
2795
2990
  this.options = {
2796
2991
  watch: true,