@hypen-space/core 0.4.32 → 0.4.35

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 (71) hide show
  1. package/dist/app.d.ts +73 -2
  2. package/dist/app.js +112 -9
  3. package/dist/app.js.map +6 -5
  4. package/dist/components/builtin.js +112 -9
  5. package/dist/components/builtin.js.map +6 -5
  6. package/dist/context.d.ts +8 -0
  7. package/dist/context.js +6 -2
  8. package/dist/context.js.map +3 -3
  9. package/dist/datasource.d.ts +168 -0
  10. package/dist/discovery.js +114 -9
  11. package/dist/discovery.js.map +7 -6
  12. package/dist/disposable.js +6 -2
  13. package/dist/disposable.js.map +2 -2
  14. package/dist/engine.browser.d.ts +15 -0
  15. package/dist/engine.browser.js +19 -2
  16. package/dist/engine.browser.js.map +3 -3
  17. package/dist/engine.d.ts +22 -0
  18. package/dist/engine.js +37 -3
  19. package/dist/engine.js.map +3 -3
  20. package/dist/events.js +6 -2
  21. package/dist/events.js.map +2 -2
  22. package/dist/index.browser.js +295 -179
  23. package/dist/index.browser.js.map +9 -8
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +298 -179
  26. package/dist/index.js.map +10 -9
  27. package/dist/loader.js +6 -2
  28. package/dist/loader.js.map +2 -2
  29. package/dist/logger.js +6 -2
  30. package/dist/logger.js.map +2 -2
  31. package/dist/plugin.js +6 -2
  32. package/dist/plugin.js.map +2 -2
  33. package/dist/remote/client.js +6 -2
  34. package/dist/remote/client.js.map +2 -2
  35. package/dist/remote/index.js +152 -11
  36. package/dist/remote/index.js.map +9 -8
  37. package/dist/remote/server.js +152 -11
  38. package/dist/remote/server.js.map +9 -8
  39. package/dist/renderer.js +6 -2
  40. package/dist/renderer.js.map +2 -2
  41. package/dist/resolver.js +6 -2
  42. package/dist/resolver.js.map +2 -2
  43. package/dist/router.d.ts +9 -0
  44. package/dist/router.js +186 -18
  45. package/dist/router.js.map +6 -5
  46. package/dist/state.js +13 -6
  47. package/dist/state.js.map +3 -3
  48. package/package.json +1 -1
  49. package/src/app.ts +121 -4
  50. package/src/context.ts +10 -2
  51. package/src/datasource.ts +252 -0
  52. package/src/discovery.ts +2 -0
  53. package/src/engine.browser.ts +30 -0
  54. package/src/engine.ts +56 -1
  55. package/src/index.ts +16 -0
  56. package/src/managed-router.ts +3 -0
  57. package/src/remote/server.ts +11 -2
  58. package/src/router.ts +36 -10
  59. package/src/state.ts +10 -3
  60. package/wasm-browser/README.md +10 -10
  61. package/wasm-browser/hypen_engine.d.ts +368 -131
  62. package/wasm-browser/hypen_engine.js +896 -645
  63. package/wasm-browser/hypen_engine_bg.wasm +0 -0
  64. package/wasm-browser/hypen_engine_bg.wasm.d.ts +20 -13
  65. package/wasm-browser/package.json +1 -1
  66. package/wasm-node/README.md +10 -10
  67. package/wasm-node/hypen_engine.d.ts +318 -89
  68. package/wasm-node/hypen_engine.js +832 -613
  69. package/wasm-node/hypen_engine_bg.wasm +0 -0
  70. package/wasm-node/hypen_engine_bg.wasm.d.ts +20 -13
  71. package/wasm-node/package.json +1 -1
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Data Source Plugin System for Hypen
3
+ *
4
+ * Enables live subscription databases (SpacetimeDB, Firebase, Convex, Electric SQL)
5
+ * to push data into the Hypen reactive engine via a unified plugin interface.
6
+ *
7
+ * UI bindings use the `$provider.path` syntax:
8
+ * - `$spacetime.messages` → SpacetimeDB messages table
9
+ * - `$firebase.user.profile` → Firebase user profile
10
+ * - `$convex.tasks` → Convex tasks table
11
+ *
12
+ * @module
13
+ */
14
+ /** Connection status for a data source provider */
15
+ export type DataSourceStatus = "disconnected" | "connecting" | "connected" | "reconnecting" | "error";
16
+ /** Describes a subscription to a table or query */
17
+ export interface DataSourceQuery {
18
+ /** Table or collection name (e.g., "messages", "users") */
19
+ table: string;
20
+ /** Optional filter criteria */
21
+ filter?: Record<string, unknown>;
22
+ /** Optional raw query string (e.g., SQL for SpacetimeDB) */
23
+ sql?: string;
24
+ }
25
+ /** Handle returned by subscribe() — call unsubscribe() to stop receiving updates */
26
+ export interface DataSourceSubscription {
27
+ /** Stop receiving updates for this subscription */
28
+ unsubscribe(): void;
29
+ /** Current subscription status */
30
+ status: "pending" | "applied" | "error";
31
+ /** Error details if status is "error" */
32
+ error?: Error;
33
+ }
34
+ /** Describes a batch of changes pushed by a plugin */
35
+ export interface DataSourceChange {
36
+ /** Changed paths relative to the provider root (e.g., ["messages", "users"]) */
37
+ paths: string[];
38
+ /** New values at those paths */
39
+ values: Record<string, unknown>;
40
+ }
41
+ /**
42
+ * Interface that all data source plugins must implement.
43
+ *
44
+ * A plugin manages the connection lifecycle and translates database-specific
45
+ * events (row inserts, updates, deletes) into `DataSourceChange` objects that
46
+ * the Hypen engine can consume.
47
+ *
48
+ * @typeParam TConfig - Plugin-specific configuration type
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * class SpacetimeDBPlugin implements DataSourcePlugin<SpacetimeDBConfig> {
53
+ * readonly name = "spacetime";
54
+ * status: DataSourceStatus = "disconnected";
55
+ *
56
+ * async connect(config: SpacetimeDBConfig, onChange: (change: DataSourceChange) => void) {
57
+ * // Connect to SpacetimeDB, subscribe to tables, push changes via onChange
58
+ * }
59
+ * // ...
60
+ * }
61
+ * ```
62
+ */
63
+ export interface DataSourcePlugin<TConfig = unknown> {
64
+ /** Unique provider name (e.g., "spacetime", "firebase", "convex") */
65
+ readonly name: string;
66
+ /** Current connection status */
67
+ status: DataSourceStatus;
68
+ /**
69
+ * Connect to the data source and begin pushing changes.
70
+ *
71
+ * @param config - Plugin-specific configuration
72
+ * @param onChange - Callback to push data changes into the engine.
73
+ * The plugin MUST call this whenever subscribed data changes.
74
+ */
75
+ connect(config: TConfig, onChange: (change: DataSourceChange) => void): Promise<void>;
76
+ /**
77
+ * Subscribe to a specific table or query.
78
+ * Returns a handle to unsubscribe later.
79
+ */
80
+ subscribe(query: DataSourceQuery): DataSourceSubscription;
81
+ /**
82
+ * Call a remote procedure / reducer / mutation.
83
+ *
84
+ * @param method - Method name (e.g., "sendMessage" for SpacetimeDB reducer)
85
+ * @param args - Arguments to pass
86
+ * @returns The result from the remote call
87
+ */
88
+ call(method: string, ...args: unknown[]): Promise<unknown>;
89
+ /**
90
+ * Disconnect from the data source and clean up all subscriptions.
91
+ */
92
+ disconnect(): Promise<void>;
93
+ }
94
+ /** Interface for the engine's data source operations (maps to WASM setContext/removeContext) */
95
+ export interface IDataSourceEngine {
96
+ /**
97
+ * Set (or replace) a named data source context.
98
+ * Registers the provider, stores the data, and re-renders bound nodes.
99
+ * Sparse merging should happen at the SDK layer before calling this.
100
+ */
101
+ setContext(name: string, data: Record<string, unknown>): void;
102
+ /**
103
+ * Remove a data source context entirely.
104
+ * Drops the provider's state and re-renders bound nodes (they resolve to null).
105
+ */
106
+ removeContext(name: string): void;
107
+ }
108
+ /**
109
+ * Manages data source plugin lifecycles and bridges them to the Hypen engine.
110
+ *
111
+ * The manager:
112
+ * 1. Registers plugins with the engine (creates state slots)
113
+ * 2. Connects plugins and wires their onChange callbacks to engine updates
114
+ * 3. Manages subscription lifecycles
115
+ * 4. Handles cleanup on disconnect
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const manager = new DataSourceManager(engine);
120
+ *
121
+ * // Register and connect a SpacetimeDB plugin
122
+ * await manager.use(new SpacetimeDBPlugin(), {
123
+ * uri: "ws://localhost:3000",
124
+ * moduleName: "chat",
125
+ * tables: ["user", "message"],
126
+ * });
127
+ *
128
+ * // Access plugin for mutations
129
+ * await manager.get("spacetime").call("sendMessage", "Hello!");
130
+ *
131
+ * // Clean up
132
+ * await manager.disconnectAll();
133
+ * ```
134
+ */
135
+ export declare class DataSourceManager {
136
+ private plugins;
137
+ private configs;
138
+ /** Accumulated state per provider — sparse merging happens here before setContext */
139
+ private state;
140
+ private engine;
141
+ constructor(engine: IDataSourceEngine);
142
+ /**
143
+ * Register, connect, and start a data source plugin.
144
+ *
145
+ * @param plugin - The plugin instance
146
+ * @param config - Plugin-specific configuration
147
+ */
148
+ use<C>(plugin: DataSourcePlugin<C>, config: C): Promise<void>;
149
+ /**
150
+ * Get a registered plugin by name.
151
+ * Useful for calling mutations: `manager.get("spacetime").call("sendMessage", text)`
152
+ */
153
+ get<T extends DataSourcePlugin = DataSourcePlugin>(name: string): T | undefined;
154
+ /** Check if a plugin is registered */
155
+ has(name: string): boolean;
156
+ /** Get all registered provider names */
157
+ getNames(): string[];
158
+ /** Get the status of a specific plugin */
159
+ getStatus(name: string): DataSourceStatus | undefined;
160
+ /**
161
+ * Disconnect and remove a specific plugin.
162
+ */
163
+ remove(name: string): Promise<void>;
164
+ /**
165
+ * Disconnect all plugins and clean up.
166
+ */
167
+ disconnectAll(): Promise<void>;
168
+ }
package/dist/discovery.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { createRequire } from "node:module";
2
2
  var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, {
6
10
  get: all[name],
7
11
  enumerable: true,
8
12
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
13
+ set: __exportSetter.bind(all, name)
10
14
  });
11
15
  };
12
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -381,6 +385,59 @@ var init_result = __esm(() => {
381
385
  };
382
386
  });
383
387
 
388
+ // src/datasource.ts
389
+ class DataSourceManager {
390
+ plugins = new Map;
391
+ configs = new Map;
392
+ state = new Map;
393
+ engine;
394
+ constructor(engine) {
395
+ this.engine = engine;
396
+ }
397
+ async use(plugin, config2) {
398
+ const { name } = plugin;
399
+ this.state.set(name, {});
400
+ this.plugins.set(name, plugin);
401
+ this.configs.set(name, config2);
402
+ await plugin.connect(config2, (change) => {
403
+ const current = this.state.get(name) ?? {};
404
+ for (const path of change.paths) {
405
+ if (path in change.values) {
406
+ current[path] = change.values[path];
407
+ }
408
+ }
409
+ this.state.set(name, current);
410
+ this.engine.setContext(name, current);
411
+ });
412
+ }
413
+ get(name) {
414
+ return this.plugins.get(name);
415
+ }
416
+ has(name) {
417
+ return this.plugins.has(name);
418
+ }
419
+ getNames() {
420
+ return Array.from(this.plugins.keys());
421
+ }
422
+ getStatus(name) {
423
+ return this.plugins.get(name)?.status;
424
+ }
425
+ async remove(name) {
426
+ const plugin = this.plugins.get(name);
427
+ if (plugin) {
428
+ await plugin.disconnect();
429
+ this.plugins.delete(name);
430
+ this.configs.delete(name);
431
+ this.state.delete(name);
432
+ this.engine.removeContext(name);
433
+ }
434
+ }
435
+ async disconnectAll() {
436
+ const names = Array.from(this.plugins.keys());
437
+ await Promise.all(names.map((name) => this.remove(name)));
438
+ }
439
+ }
440
+
384
441
  // src/state.ts
385
442
  function deepClone(obj) {
386
443
  if (obj === null || typeof obj !== "object") {
@@ -520,9 +577,11 @@ function createObservableState(initialState, options) {
520
577
  notificationPending = false;
521
578
  if (batchDepth === 0) {
522
579
  notifyChange();
523
- }
580
+ } else {}
524
581
  });
525
582
  }
583
+ } else {
584
+ pendingChange = pendingChange || { paths: [], newValues: {} };
526
585
  }
527
586
  }
528
587
  const proxyCache = new WeakMap;
@@ -581,11 +640,12 @@ function createObservableState(initialState, options) {
581
640
  return true;
582
641
  },
583
642
  deleteProperty(obj, prop) {
584
- if (prop in obj) {
585
- delete obj[prop];
643
+ const existed = Object.prototype.hasOwnProperty.call(obj, prop);
644
+ const result = delete obj[prop];
645
+ if (existed) {
586
646
  scheduleBatch();
587
647
  }
588
- return true;
648
+ return result;
589
649
  }
590
650
  });
591
651
  proxyCache.set(target, proxy);
@@ -650,6 +710,7 @@ class HypenAppBuilder {
650
710
  errorHandler;
651
711
  template;
652
712
  _registry;
713
+ dataSourceEntries = [];
653
714
  constructor(initialState, options, registry) {
654
715
  this.initialState = initialState;
655
716
  this.options = options || {};
@@ -683,10 +744,19 @@ class HypenAppBuilder {
683
744
  this.errorHandler = fn;
684
745
  return this;
685
746
  }
747
+ useDataSource(plugin, config2) {
748
+ this.dataSourceEntries.push({ plugin, config: config2 });
749
+ return this;
750
+ }
686
751
  ui(template) {
687
752
  this.template = template;
688
753
  return this.build();
689
754
  }
755
+ uiFile(path) {
756
+ const fs = __require("fs");
757
+ this.template = fs.readFileSync(path, "utf-8").trim();
758
+ return this.build();
759
+ }
690
760
  build() {
691
761
  const stateKeys = this.initialState !== null && typeof this.initialState === "object" ? Object.keys(this.initialState) : [];
692
762
  const definition = {
@@ -697,6 +767,7 @@ class HypenAppBuilder {
697
767
  version: this.options.version,
698
768
  initialState: this.initialState,
699
769
  template: this.template,
770
+ dataSources: this.dataSourceEntries.length > 0 ? this.dataSourceEntries : undefined,
700
771
  handlers: {
701
772
  onCreated: this.createdHandler,
702
773
  onAction: this.actionHandlers,
@@ -758,6 +829,8 @@ class HypenModuleInstance {
758
829
  router;
759
830
  globalContext;
760
831
  stateChangeCallbacks = [];
832
+ dataSourceManager;
833
+ dataSourceAccessor = {};
761
834
  constructor(engine, definition, router, globalContext) {
762
835
  this.engine = engine;
763
836
  this.definition = definition;
@@ -787,7 +860,8 @@ class HypenModuleInstance {
787
860
  const result = await this.executeAction(actionName, handler, {
788
861
  action: actionCtx,
789
862
  state: this.state,
790
- context
863
+ context,
864
+ dataSources: this.dataSourceAccessor
791
865
  });
792
866
  if (!result.ok) {
793
867
  const shouldRethrow = await this.handleError(result.error, { actionName });
@@ -830,8 +904,9 @@ class HypenModuleInstance {
830
904
  on: (event, handler) => ctx.on(event, handler),
831
905
  router: this.router
832
906
  };
833
- if (ctx.__hypenEngine) {
834
- api.__hypenEngine = ctx.__hypenEngine;
907
+ const ctxRecord = ctx;
908
+ if (ctxRecord.__hypenEngine) {
909
+ api.__hypenEngine = ctxRecord.__hypenEngine;
835
910
  }
836
911
  return api;
837
912
  }
@@ -878,6 +953,25 @@ class HypenModuleInstance {
878
953
  return false;
879
954
  }
880
955
  async callCreatedHandler() {
956
+ if (this.definition.dataSources?.length) {
957
+ const dsEngine = this.engine;
958
+ this.dataSourceManager = new DataSourceManager(dsEngine);
959
+ for (const { plugin, config: config2 } of this.definition.dataSources) {
960
+ try {
961
+ await this.dataSourceManager.use(plugin, config2);
962
+ this.dataSourceAccessor[plugin.name] = new Proxy(plugin, {
963
+ get(target, prop) {
964
+ if (typeof prop === "string" && !(prop in target)) {
965
+ return (...args) => target.call(prop, ...args);
966
+ }
967
+ return target[prop];
968
+ }
969
+ });
970
+ } catch (e) {
971
+ log2.error(`Failed to connect data source "${plugin.name}":`, e);
972
+ }
973
+ }
974
+ }
881
975
  if (this.definition.handlers.onCreated) {
882
976
  const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
883
977
  try {
@@ -897,6 +991,15 @@ class HypenModuleInstance {
897
991
  async destroy() {
898
992
  if (this.isDestroyed)
899
993
  return;
994
+ if (this.dataSourceManager) {
995
+ try {
996
+ await this.dataSourceManager.disconnectAll();
997
+ } catch (e) {
998
+ log2.error("Error disconnecting data sources:", e);
999
+ }
1000
+ this.dataSourceManager = undefined;
1001
+ this.dataSourceAccessor = {};
1002
+ }
900
1003
  if (this.definition.handlers.onDestroyed) {
901
1004
  try {
902
1005
  await this.definition.handlers.onDestroyed(this.state);
@@ -1059,6 +1162,8 @@ async function discoverComponents(baseDir, options = {}) {
1059
1162
  const module = moduleExport.default;
1060
1163
  if (module && typeof module === "object" && module.template) {
1061
1164
  addSingleFileComponent(baseName, modulePath, module.template);
1165
+ } else if (module && typeof module === "object") {
1166
+ frameworkLoggers.discovery.warn(`Skipping "${entry.name}": has .ui() pattern but module has no .template property. Did you forget to call .ui(hypen\`...\`)?`);
1062
1167
  }
1063
1168
  } catch (e) {
1064
1169
  log3(`Failed to import potential single-file component: ${entry.name}`, e);
@@ -1226,4 +1331,4 @@ export {
1226
1331
  discoverComponents
1227
1332
  };
1228
1333
 
1229
- //# debugId=46EBEA75EF219F3964756E2164756E21
1334
+ //# debugId=D8CFBCA4D5F7D1BB64756E2164756E21