@omriashke/dynamico-core 0.1.9 → 0.1.11

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.
Files changed (76) hide show
  1. package/dist/bookPreview.d.ts +3 -0
  2. package/dist/bookPreview.d.ts.map +1 -1
  3. package/dist/bookPreview.js +5 -0
  4. package/dist/bookPreview.js.map +1 -1
  5. package/dist/constants.d.ts +6 -0
  6. package/dist/constants.d.ts.map +1 -0
  7. package/dist/constants.js +8 -0
  8. package/dist/constants.js.map +1 -0
  9. package/dist/esbuildFlatten.d.ts +15 -0
  10. package/dist/esbuildFlatten.d.ts.map +1 -0
  11. package/dist/esbuildFlatten.js +39 -0
  12. package/dist/esbuildFlatten.js.map +1 -0
  13. package/dist/index.d.ts +7 -3
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +7 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/loader.d.ts +2 -0
  18. package/dist/loader.d.ts.map +1 -1
  19. package/dist/loader.js +51 -14
  20. package/dist/loader.js.map +1 -1
  21. package/dist/node/bookConfig.d.ts +13 -0
  22. package/dist/node/bookConfig.d.ts.map +1 -0
  23. package/dist/node/bookConfig.js +54 -0
  24. package/dist/node/bookConfig.js.map +1 -0
  25. package/dist/node/index.d.ts +2 -0
  26. package/dist/node/index.d.ts.map +1 -0
  27. package/dist/node/index.js +2 -0
  28. package/dist/node/index.js.map +1 -0
  29. package/dist/packageScope.d.ts.map +1 -1
  30. package/dist/packageScope.js +15 -7
  31. package/dist/packageScope.js.map +1 -1
  32. package/dist/propsSchema.d.ts +2 -0
  33. package/dist/propsSchema.d.ts.map +1 -1
  34. package/dist/propsSchema.js +36 -0
  35. package/dist/propsSchema.js.map +1 -1
  36. package/dist/react/createRuntime.d.ts.map +1 -1
  37. package/dist/react/createRuntime.js +3 -21
  38. package/dist/react/createRuntime.js.map +1 -1
  39. package/dist/react/useRegistryModule.d.ts +4 -0
  40. package/dist/react/useRegistryModule.d.ts.map +1 -0
  41. package/dist/react/useRegistryModule.js +8 -0
  42. package/dist/react/useRegistryModule.js.map +1 -0
  43. package/dist/registry.d.ts +5 -0
  44. package/dist/registry.d.ts.map +1 -1
  45. package/dist/registry.js +35 -4
  46. package/dist/registry.js.map +1 -1
  47. package/dist/registryModule.d.ts.map +1 -1
  48. package/dist/registryModule.js +13 -2
  49. package/dist/registryModule.js.map +1 -1
  50. package/dist/relativeRequires.d.ts +12 -0
  51. package/dist/relativeRequires.d.ts.map +1 -1
  52. package/dist/relativeRequires.js +33 -0
  53. package/dist/relativeRequires.js.map +1 -1
  54. package/dist/sources/remote.d.ts +7 -8
  55. package/dist/sources/remote.d.ts.map +1 -1
  56. package/dist/sources/remote.js +73 -19
  57. package/dist/sources/remote.js.map +1 -1
  58. package/dist/types.d.ts +6 -0
  59. package/dist/types.d.ts.map +1 -1
  60. package/package.json +11 -2
  61. package/src/bookPreview.ts +7 -0
  62. package/src/constants.ts +9 -0
  63. package/src/esbuildFlatten.ts +47 -0
  64. package/src/index.ts +22 -2
  65. package/src/loader.ts +42 -14
  66. package/src/node/bookConfig.ts +63 -0
  67. package/src/node/index.ts +9 -0
  68. package/src/packageScope.ts +15 -7
  69. package/src/propsSchema.ts +35 -0
  70. package/src/react/createRuntime.tsx +3 -16
  71. package/src/react/useRegistryModule.ts +15 -0
  72. package/src/registry.ts +39 -4
  73. package/src/registryModule.ts +12 -3
  74. package/src/relativeRequires.ts +48 -0
  75. package/src/sources/remote.ts +72 -26
  76. package/src/types.ts +6 -0
@@ -14,22 +14,21 @@ export interface RemoteSourceOptions {
14
14
  /**
15
15
  * Headers to send on every request. Called on each HTTP fetch and on each
16
16
  * WebSocket reconnect, so the function can return a freshly-rotated token.
17
- *
18
- * - HTTP: merged into the `Authorization: ...` / `x-api-key: ...` request headers.
19
- * - WebSocket: passed as `new WebSocket(url, undefined, { headers })`. This
20
- * works on React Native (which extends the standard constructor); browsers
21
- * silently ignore it because the spec doesn't allow custom WS handshake
22
- * headers. For browsers behind authenticated reverse proxies, use a
23
- * query-string token in `wsUrl` or front the registry with cookie auth.
24
17
  */
25
18
  headers?: () => Record<string, string>;
19
+ /**
20
+ * When false, skip WebSocket entirely (HTTP fetch only).
21
+ * @default true
22
+ */
23
+ webSocket?: boolean;
26
24
  }
27
25
 
28
26
  /**
29
27
  * Talks to @omriashke/dynamico-registry (or any compatible server).
30
28
  *
31
29
  * GET {url}/component/:name -> CompiledModule (initial fetch)
32
- * WS {wsUrl}/subscribe -> stream of CompiledModule updates
30
+ * WS {wsUrl}/subscribe -> filtered push stream; client sends
31
+ * `{ op: "watch", names: [...] }`
33
32
  */
34
33
  export function createRemoteSource(options: RemoteSourceOptions): Source {
35
34
  const fetchImpl: typeof fetch =
@@ -52,18 +51,38 @@ export function createRemoteSource(options: RemoteSourceOptions): Source {
52
51
  options.wsUrl ?? httpUrl.replace(/^http/, "ws") + "/subscribe";
53
52
 
54
53
  const listeners = new Set<(u: SourceUpdate) => void>();
54
+ const watchedNames = new Set<string>();
55
+ const watchRefCounts = new Map<string, number>();
55
56
  let socket: WebSocket | null = null;
56
57
  let disposed = false;
58
+ let pendingWatchSync = false;
57
59
  const reconnectMs = options.reconnectMs ?? 1000;
60
+ const useWebSocket = options.webSocket !== false;
61
+ const WS_OPEN = (WSCtor as unknown as { OPEN?: number }).OPEN ?? 1;
62
+ const WS_CONNECTING = (WSCtor as unknown as { CONNECTING?: number }).CONNECTING ?? 0;
63
+
64
+ function pushWatchSet(): void {
65
+ if (!useWebSocket || watchedNames.size === 0) return;
66
+ if (!socket || socket.readyState !== WS_OPEN) {
67
+ pendingWatchSync = true;
68
+ connect();
69
+ return;
70
+ }
71
+ pendingWatchSync = false;
72
+ try {
73
+ socket.send(JSON.stringify({ op: "watch", names: [...watchedNames] }));
74
+ } catch {
75
+ /* ignore */
76
+ }
77
+ }
58
78
 
59
79
  function connect(): void {
60
- if (disposed) return;
80
+ if (disposed || !useWebSocket || watchedNames.size === 0) return;
81
+ if (socket && (socket.readyState === WS_OPEN || socket.readyState === WS_CONNECTING)) {
82
+ return;
83
+ }
61
84
  try {
62
85
  const hdrs = options.headers?.();
63
- // RN's WebSocket constructor accepts a third {headers} arg that lets us
64
- // attach Bearer / x-api-key tokens to the upgrade request. The standard
65
- // browser WebSocket ignores extra constructor args, so this is a no-op
66
- // there (use cookies / a query-string token instead).
67
86
  socket = hdrs
68
87
  ? new (WSCtor as unknown as new (
69
88
  url: string,
@@ -71,10 +90,13 @@ export function createRemoteSource(options: RemoteSourceOptions): Source {
71
90
  options?: { headers?: Record<string, string> },
72
91
  ) => WebSocket)(wsUrl, undefined, { headers: hdrs })
73
92
  : new WSCtor(wsUrl);
74
- } catch (err) {
93
+ } catch {
75
94
  scheduleReconnect();
76
95
  return;
77
96
  }
97
+ socket.onopen = () => {
98
+ if (pendingWatchSync || watchedNames.size > 0) pushWatchSet();
99
+ };
78
100
  socket.onmessage = (ev: MessageEvent) => {
79
101
  try {
80
102
  const data =
@@ -103,11 +125,40 @@ export function createRemoteSource(options: RemoteSourceOptions): Source {
103
125
  }
104
126
 
105
127
  function scheduleReconnect(): void {
106
- if (disposed) return;
128
+ if (disposed || !useWebSocket || watchedNames.size === 0) return;
107
129
  setTimeout(connect, reconnectMs);
108
130
  }
109
131
 
110
- connect();
132
+ function watch(name: string): () => void {
133
+ if (!useWebSocket) return () => undefined;
134
+ const next = (watchRefCounts.get(name) ?? 0) + 1;
135
+ watchRefCounts.set(name, next);
136
+ if (next === 1) {
137
+ watchedNames.add(name);
138
+ pushWatchSet();
139
+ }
140
+ let released = false;
141
+ return () => {
142
+ if (released) return;
143
+ released = true;
144
+ const count = (watchRefCounts.get(name) ?? 1) - 1;
145
+ if (count <= 0) {
146
+ watchRefCounts.delete(name);
147
+ watchedNames.delete(name);
148
+ pushWatchSet();
149
+ if (watchedNames.size === 0) {
150
+ try {
151
+ socket?.close();
152
+ } catch {
153
+ /* noop */
154
+ }
155
+ socket = null;
156
+ }
157
+ } else {
158
+ watchRefCounts.set(name, count);
159
+ }
160
+ };
161
+ }
111
162
 
112
163
  return {
113
164
  async fetch(name: string): Promise<CompiledModule> {
@@ -136,15 +187,7 @@ export function createRemoteSource(options: RemoteSourceOptions): Source {
136
187
  listeners.delete(listener);
137
188
  };
138
189
  },
139
- /**
140
- * Tell the registry what bare specifiers the host's scope exposes. The
141
- * registry uses this to validate that every component's imports resolve
142
- * against something the host actually provides — so a typo or a forgotten
143
- * scope entry is caught at push time, not at navigation time.
144
- *
145
- * Best-effort: failures (network, 5xx, server doesn't support /scope) are
146
- * silently swallowed; they don't block the app from running.
147
- */
190
+ watch,
148
191
  async reportScope(keys, reportedBy) {
149
192
  try {
150
193
  const baseHeaders = options.headers?.() ?? {};
@@ -154,16 +197,19 @@ export function createRemoteSource(options: RemoteSourceOptions): Source {
154
197
  body: JSON.stringify({ keys: [...keys], reportedBy }),
155
198
  });
156
199
  } catch {
157
- /* best-effort: never block the host on this */
200
+ /* best-effort */
158
201
  }
159
202
  },
160
203
  dispose() {
161
204
  disposed = true;
205
+ watchedNames.clear();
206
+ watchRefCounts.clear();
162
207
  try {
163
208
  socket?.close();
164
209
  } catch {
165
210
  /* noop */
166
211
  }
212
+ socket = null;
167
213
  },
168
214
  };
169
215
  }
package/src/types.ts CHANGED
@@ -103,6 +103,12 @@ export interface Source {
103
103
  fetch(name: string): Promise<CompiledModule>;
104
104
  /** Subscribe to updates for any component. Returns unsubscribe fn. */
105
105
  subscribe(listener: (update: SourceUpdate) => void): () => void;
106
+ /**
107
+ * Subscribe to live WebSocket updates for a component. Ref-counted; the
108
+ * socket connects lazily on the first watch and only receives pushes for
109
+ * watched names. Returns a release function.
110
+ */
111
+ watch?(name: string): () => void;
106
112
  /** Optional disposal hook. */
107
113
  dispose?(): void;
108
114
  /**