@peers-app/peers-sdk 0.16.4 → 0.16.6

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 (54) hide show
  1. package/dist/context/user-context.js +6 -0
  2. package/dist/contracts/__tests__/builder.test.d.ts +1 -0
  3. package/dist/contracts/__tests__/builder.test.js +411 -0
  4. package/dist/contracts/__tests__/extract.test.d.ts +1 -0
  5. package/dist/contracts/__tests__/extract.test.js +145 -0
  6. package/dist/contracts/__tests__/integration.test.d.ts +1 -0
  7. package/dist/contracts/__tests__/integration.test.js +348 -0
  8. package/dist/contracts/__tests__/registry.test.d.ts +1 -0
  9. package/dist/contracts/__tests__/registry.test.js +324 -0
  10. package/dist/contracts/__tests__/validate.test.d.ts +1 -0
  11. package/dist/contracts/__tests__/validate.test.js +699 -0
  12. package/dist/contracts/builder.d.ts +97 -0
  13. package/dist/contracts/builder.js +211 -0
  14. package/dist/contracts/contract-providers.table.d.ts +40 -0
  15. package/dist/contracts/contract-providers.table.js +41 -0
  16. package/dist/contracts/contracts.table.d.ts +44 -0
  17. package/dist/contracts/contracts.table.js +44 -0
  18. package/dist/contracts/extract.d.ts +46 -0
  19. package/dist/contracts/extract.js +51 -0
  20. package/dist/contracts/index.d.ts +9 -0
  21. package/dist/contracts/index.js +31 -0
  22. package/dist/contracts/persistent-registry.d.ts +32 -0
  23. package/dist/contracts/persistent-registry.js +138 -0
  24. package/dist/contracts/registry.d.ts +58 -0
  25. package/dist/contracts/registry.js +155 -0
  26. package/dist/contracts/types.d.ts +108 -0
  27. package/dist/contracts/types.js +10 -0
  28. package/dist/contracts/validate.d.ts +24 -0
  29. package/dist/contracts/validate.js +274 -0
  30. package/dist/data/assistants.d.ts +5 -0
  31. package/dist/data/assistants.js +1 -0
  32. package/dist/data/package-versions.d.ts +2 -2
  33. package/dist/data/persistent-vars.d.ts +3 -0
  34. package/dist/data/persistent-vars.js +17 -2
  35. package/dist/data/tools.d.ts +4 -0
  36. package/dist/data/tools.js +1 -0
  37. package/dist/data/workflows.d.ts +1 -0
  38. package/dist/events.d.ts +13 -0
  39. package/dist/events.js +218 -34
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +1 -0
  42. package/dist/package-loader/contract-package-loader.d.ts +23 -0
  43. package/dist/package-loader/contract-package-loader.js +65 -0
  44. package/dist/package-loader/index.d.ts +1 -0
  45. package/dist/package-loader/index.js +1 -0
  46. package/dist/package-loader/package-loader.d.ts +11 -0
  47. package/dist/package-loader/package-loader.js +59 -8
  48. package/dist/rpc-types.d.ts +4 -0
  49. package/dist/rpc-types.js +5 -4
  50. package/dist/types/workflow.d.ts +3 -0
  51. package/dist/types/workflow.js +1 -0
  52. package/dist/utils.d.ts +15 -0
  53. package/dist/utils.js +39 -0
  54. package/package.json +1 -1
package/dist/events.js CHANGED
@@ -1,37 +1,195 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Emitter = exports.Event = void 0;
4
+ exports.notifyClientConnected = notifyClientConnected;
5
+ exports.subscribePrefix = subscribePrefix;
4
6
  exports.subscribe = subscribe;
5
7
  exports.subscribeDebounce = subscribeDebounce;
6
8
  exports.emit = emit;
7
9
  exports.unionEvents = unionEvents;
8
10
  const rpc_types_1 = require("./rpc-types");
9
11
  const unitTests = process.env.NODE_ENV === "test";
10
- const subscriptions = [];
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ // ─── Subscription Storage ────────────────────────────────────────────────────
13
+ /** Subscriptions indexed by exact event name for O(1) dispatch. */
14
+ const nameIndex = new Map();
15
+ /** Subscriptions that use custom filter functions (checked on every emit). */
16
+ const wildcardSubscriptions = new Set();
17
+ function addSubscription(subscription) {
18
+ if (subscription.eventName) {
19
+ let bucket = nameIndex.get(subscription.eventName);
20
+ if (!bucket) {
21
+ bucket = new Set();
22
+ nameIndex.set(subscription.eventName, bucket);
23
+ }
24
+ bucket.add(subscription);
25
+ }
26
+ else {
27
+ wildcardSubscriptions.add(subscription);
28
+ }
29
+ }
30
+ function removeSubscription(subscription) {
31
+ if (subscription.eventName) {
32
+ const bucket = nameIndex.get(subscription.eventName);
33
+ if (bucket) {
34
+ const removed = bucket.delete(subscription);
35
+ if (bucket.size === 0) {
36
+ nameIndex.delete(subscription.eventName);
37
+ }
38
+ return removed;
39
+ }
40
+ return false;
41
+ }
42
+ return wildcardSubscriptions.delete(subscription);
43
+ }
44
+ // ─── Client → Server Subscription Registration ──────────────────────────────
45
+ // In multi-process mode (Electron), the client tells the server which event
46
+ // names/prefixes it cares about so the server can skip forwarding unneeded events.
47
+ /** Reference counts for event names registered with the server. */
48
+ const clientNameRefCounts = new Map();
49
+ /** Reference counts for event prefixes registered with the server. */
50
+ const clientPrefixRefCounts = new Map();
51
+ /** Whether the client-server connection is established (for deferred registration). */
52
+ let clientConnected = false;
53
+ /** Names pending registration (accumulated before connection is ready). */
54
+ const pendingNameSubscriptions = new Set();
55
+ /** Prefixes pending registration (accumulated before connection is ready). */
56
+ const pendingPrefixSubscriptions = new Set();
57
+ /**
58
+ * Notify the events system that the client connection is ready.
59
+ * Flushes any subscriptions that were registered before the connection.
60
+ */
61
+ function notifyClientConnected() {
62
+ clientConnected = true;
63
+ if (pendingNameSubscriptions.size > 0) {
64
+ const names = [...pendingNameSubscriptions];
65
+ pendingNameSubscriptions.clear();
66
+ rpc_types_1.rpcServerCalls.subscribeEvents(names).catch((err) => {
67
+ console.error("Failed to register pending event subscriptions with server", err);
68
+ });
69
+ }
70
+ if (pendingPrefixSubscriptions.size > 0) {
71
+ const prefixes = [...pendingPrefixSubscriptions];
72
+ pendingPrefixSubscriptions.clear();
73
+ rpc_types_1.rpcServerCalls.subscribePrefixes(prefixes).catch((err) => {
74
+ console.error("Failed to register pending prefix subscriptions with server", err);
75
+ });
76
+ }
77
+ }
78
+ function clientRegisterName(name) {
79
+ if (unitTests || (0, rpc_types_1.isSingleProcessClient)())
80
+ return;
81
+ const count = clientNameRefCounts.get(name) || 0;
82
+ clientNameRefCounts.set(name, count + 1);
83
+ if (count === 0) {
84
+ if (clientConnected) {
85
+ rpc_types_1.rpcServerCalls.subscribeEvents([name]).catch((err) => {
86
+ console.error(`Failed to register event subscription with server: ${name}`, err);
87
+ });
88
+ }
89
+ else {
90
+ pendingNameSubscriptions.add(name);
91
+ }
92
+ }
93
+ }
94
+ function clientUnregisterName(name) {
95
+ if (unitTests || (0, rpc_types_1.isSingleProcessClient)())
96
+ return;
97
+ const count = clientNameRefCounts.get(name) || 0;
98
+ if (count <= 1) {
99
+ clientNameRefCounts.delete(name);
100
+ pendingNameSubscriptions.delete(name);
101
+ if (clientConnected) {
102
+ rpc_types_1.rpcServerCalls.unsubscribeEvents([name]).catch((err) => {
103
+ console.error(`Failed to unregister event subscription with server: ${name}`, err);
104
+ });
105
+ }
106
+ }
107
+ else {
108
+ clientNameRefCounts.set(name, count - 1);
109
+ }
110
+ }
111
+ /**
112
+ * Register a prefix-based event subscription with the server.
113
+ * Events whose name starts with this prefix will be forwarded to the client.
114
+ * Returns an unsubscribe function that decrements the refcount.
115
+ */
116
+ function subscribePrefix(prefix) {
117
+ if (!rpc_types_1.isClient || unitTests || (0, rpc_types_1.isSingleProcessClient)()) {
118
+ return () => { };
119
+ }
120
+ const count = clientPrefixRefCounts.get(prefix) || 0;
121
+ clientPrefixRefCounts.set(prefix, count + 1);
122
+ if (count === 0) {
123
+ if (clientConnected) {
124
+ rpc_types_1.rpcServerCalls.subscribePrefixes([prefix]).catch((err) => {
125
+ console.error(`Failed to register prefix subscription with server: ${prefix}`, err);
126
+ });
127
+ }
128
+ else {
129
+ pendingPrefixSubscriptions.add(prefix);
130
+ }
131
+ }
132
+ return () => {
133
+ const current = clientPrefixRefCounts.get(prefix) || 0;
134
+ if (current <= 1) {
135
+ clientPrefixRefCounts.delete(prefix);
136
+ pendingPrefixSubscriptions.delete(prefix);
137
+ if (clientConnected) {
138
+ rpc_types_1.rpcServerCalls.unsubscribePrefixes([prefix]).catch((err) => {
139
+ console.error(`Failed to unregister prefix subscription with server: ${prefix}`, err);
140
+ });
141
+ }
142
+ }
143
+ else {
144
+ clientPrefixRefCounts.set(prefix, current - 1);
145
+ }
146
+ };
147
+ }
148
+ // ─── Server-Side Event Forwarding Gate ───────────────────────────────────────
149
+ // The server uses this to decide whether to forward an event to the client.
150
+ /** Event names the client has subscribed to. Server-side only. */
151
+ const serverClientNames = new Set();
152
+ /** Event prefixes the client has subscribed to. Server-side only. */
153
+ const serverClientPrefixes = new Set();
154
+ /**
155
+ * Check whether the client wants this event.
156
+ * Used by the server to gate `rpcClientCalls.emitEvent`.
157
+ */
158
+ function clientWantsEvent(event) {
159
+ if (serverClientNames.has(event.name))
160
+ return true;
161
+ for (const prefix of serverClientPrefixes) {
162
+ if (event.name.startsWith(prefix))
163
+ return true;
164
+ }
165
+ return false;
166
+ }
167
+ // ─── Public API ──────────────────────────────────────────────────────────────
12
168
  function subscribe(nameOrFilter, handler) {
169
+ let eventName;
170
+ let filter;
13
171
  if (typeof nameOrFilter === "string") {
14
- const name = nameOrFilter;
15
- nameOrFilter = (evt) => evt.name === name;
172
+ eventName = nameOrFilter;
173
+ filter = (evt) => evt.name === eventName;
174
+ }
175
+ else {
176
+ filter = nameOrFilter;
177
+ }
178
+ const subscription = { filter, handler, eventName };
179
+ addSubscription(subscription);
180
+ if (rpc_types_1.isClient && eventName) {
181
+ clientRegisterName(eventName);
16
182
  }
17
- const filter = nameOrFilter;
18
- const subscription = {
19
- filter,
20
- handler,
21
- };
22
- subscriptions.push(subscription);
23
183
  return {
24
184
  unsubscribe: () => {
25
- const iSub = subscriptions.indexOf(subscription);
26
- if (iSub >= 0) {
27
- subscriptions.splice(iSub, 1);
28
- return true;
185
+ const removed = removeSubscription(subscription);
186
+ if (removed && rpc_types_1.isClient && eventName) {
187
+ clientUnregisterName(eventName);
29
188
  }
30
- return false;
189
+ return removed;
31
190
  },
32
191
  };
33
192
  }
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
193
  function subscribeDebounce(nameOrFilter, handler, debounceMs) {
36
194
  let pid = 0;
37
195
  const handlerDebounced = (evt) => {
@@ -46,37 +204,64 @@ function subscribeDebounce(nameOrFilter, handler, debounceMs) {
46
204
  return subscribe(nameOrFilter, handlerDebounced);
47
205
  }
48
206
  async function emit(event, dontPropagate) {
49
- const matchedHandlerPromises = subscriptions
50
- .filter((subscription) => subscription.filter(event))
51
- .map(async (subscription) => {
207
+ const matched = [];
208
+ const bucket = nameIndex.get(event.name);
209
+ if (bucket) {
210
+ for (const sub of bucket) {
211
+ matched.push(sub);
212
+ }
213
+ }
214
+ for (const sub of wildcardSubscriptions) {
215
+ if (sub.filter(event)) {
216
+ matched.push(sub);
217
+ }
218
+ }
219
+ const handlerPromises = matched.map(async (subscription) => {
52
220
  try {
53
221
  return await subscription.handler(event);
54
222
  }
55
223
  catch (err) {
56
- console.error(`An unhandled error occurred in a handler while processing event: ${JSON.stringify({ event, subscription })}`, err);
224
+ console.error(`An unhandled error occurred in a handler while processing event: ${JSON.stringify({ event })}`, err);
57
225
  return false;
58
226
  }
59
227
  });
60
- // if (!(dontPropagate || unitTests)) {
61
- // matchedHandlerPromises.push(
62
- // isClient
63
- // ? rpcServerCalls.emitEvent(event).catch((err) => { console.error(`Error while propagating event to server`, err); return false; })
64
- // : rpcClientCalls.emitEvent(event).catch((err) => { console.error(`Error while propagating event to client`, err); return false; })
65
- // );
66
- // }
67
228
  if (!(dontPropagate || unitTests || (0, rpc_types_1.isSingleProcessClient)())) {
68
- matchedHandlerPromises.push(rpc_types_1.rpcClientCalls.emitEvent(event).catch((err) => {
69
- console.error(`Error while propagating event to client`, err);
70
- return false;
71
- }));
229
+ if (clientWantsEvent(event)) {
230
+ handlerPromises.push(rpc_types_1.rpcClientCalls.emitEvent(event).catch((err) => {
231
+ console.error(`Error while propagating event to client`, err);
232
+ return false;
233
+ }));
234
+ }
72
235
  }
73
- const results = await Promise.all(matchedHandlerPromises);
74
- // if any handlers returned false (or errored), return false, otherwise return true
236
+ const results = await Promise.all(handlerPromises);
75
237
  return !results.some((r) => r === false);
76
238
  }
77
239
  if (rpc_types_1.isClient) {
78
240
  rpc_types_1.rpcClientCalls.emitEvent = (event) => emit(event, true);
79
241
  }
242
+ // Server-side: register RPC handlers for client subscription management
243
+ if (!rpc_types_1.isClient) {
244
+ rpc_types_1.rpcServerCalls.subscribeEvents = async (names) => {
245
+ for (const name of names) {
246
+ serverClientNames.add(name);
247
+ }
248
+ };
249
+ rpc_types_1.rpcServerCalls.unsubscribeEvents = async (names) => {
250
+ for (const name of names) {
251
+ serverClientNames.delete(name);
252
+ }
253
+ };
254
+ rpc_types_1.rpcServerCalls.subscribePrefixes = async (prefixes) => {
255
+ for (const prefix of prefixes) {
256
+ serverClientPrefixes.add(prefix);
257
+ }
258
+ };
259
+ rpc_types_1.rpcServerCalls.unsubscribePrefixes = async (prefixes) => {
260
+ for (const prefix of prefixes) {
261
+ serverClientPrefixes.delete(prefix);
262
+ }
263
+ };
264
+ }
80
265
  class Event {
81
266
  _eventName;
82
267
  constructor(_eventName) {
@@ -119,7 +304,6 @@ class Emitter {
119
304
  }
120
305
  }
121
306
  exports.Emitter = Emitter;
122
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
307
  function unionEvents(...events) {
124
308
  const eventName = events.map((s) => s.eventName()).join("|");
125
309
  return {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./context";
2
+ export * from "./contracts";
2
3
  export * from "./data";
3
4
  export * as data from "./data";
4
5
  export * from "./data/orm";
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.tools = exports.workflowLogSchema = exports.WorkflowLogs = exports.getLogger = exports.orm = exports.data = void 0;
18
18
  __exportStar(require("./context"), exports);
19
+ __exportStar(require("./contracts"), exports);
19
20
  __exportStar(require("./data"), exports);
20
21
  exports.data = require("./data");
21
22
  __exportStar(require("./data/orm"), exports);
@@ -0,0 +1,23 @@
1
+ import type { DataContext } from "../context/data-context";
2
+ import type { ContractRegistry } from "../contracts/registry";
3
+ import type { IPackageDefinitionResult } from "../contracts/types";
4
+ /**
5
+ * Installs a contract-based package into a DataContext.
6
+ *
7
+ * Handles packages that export an `IPackageDefinitionResult` from `definePackage()`.
8
+ * Registers contracts in the provided ContractRegistry, saves tools/assistants/workflows
9
+ * with the `packageId` from the definition set, and registers table definitions.
10
+ */
11
+ export declare function installContractPackage(dataContext: DataContext, packageDefinition: IPackageDefinitionResult, opts?: {
12
+ registry?: ContractRegistry;
13
+ }): Promise<void>;
14
+ /**
15
+ * Attempts to extract an `IPackageDefinitionResult` from a bundle's module exports.
16
+ * Returns undefined if the bundle doesn't export a contract-based package definition.
17
+ */
18
+ export declare function extractPackageDefinition(bundleExports: Record<string, unknown>): IPackageDefinitionResult | undefined;
19
+ /**
20
+ * Determines whether a bundle exports a contract-based package definition,
21
+ * indicating it should be installed via the new contract-aware path.
22
+ */
23
+ export declare function hasPackageDefinition(bundleExports: Record<string, unknown>): boolean;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installContractPackage = installContractPackage;
4
+ exports.extractPackageDefinition = extractPackageDefinition;
5
+ exports.hasPackageDefinition = hasPackageDefinition;
6
+ const tools_1 = require("../data/tools");
7
+ const tools_2 = require("../tools");
8
+ /**
9
+ * Installs a contract-based package into a DataContext.
10
+ *
11
+ * Handles packages that export an `IPackageDefinitionResult` from `definePackage()`.
12
+ * Registers contracts in the provided ContractRegistry, saves tools/assistants/workflows
13
+ * with the `packageId` from the definition set, and registers table definitions.
14
+ */
15
+ async function installContractPackage(dataContext, packageDefinition, opts) {
16
+ const { packageId } = packageDefinition;
17
+ const registry = opts?.registry;
18
+ // Register contracts in the in-memory registry
19
+ if (registry) {
20
+ for (const contract of packageDefinition.contracts) {
21
+ const key = `${contract.contractId}@${contract.version}`;
22
+ const alsoImpl = packageDefinition.alsoImplements.get(key) ?? [];
23
+ const result = registry.register(packageId, contract, alsoImpl);
24
+ if (!result.valid) {
25
+ console.error(`[ContractPackageLoader] Contract registration failed for ${contract.name}:`, result.errors);
26
+ }
27
+ }
28
+ }
29
+ // Install tool instances with packageId
30
+ for (const toolInstance of packageDefinition.toolInstances) {
31
+ (0, tools_2.registerTool)(toolInstance);
32
+ const toolWithPackageId = { ...toolInstance.tool, packageId };
33
+ await (0, tools_1.Tools)(dataContext).save(toolWithPackageId);
34
+ }
35
+ // Install table definitions
36
+ for (const tableDefinition of packageDefinition.tableDefinitions) {
37
+ dataContext.tableContainer.registerTableDefinition(tableDefinition, {
38
+ overwrite: true,
39
+ });
40
+ }
41
+ // Install assistants with packageId
42
+ const { Assistants } = await Promise.resolve().then(() => require("../data/assistants"));
43
+ for (const assistant of packageDefinition.assistants) {
44
+ const assistantWithPackageId = { ...assistant, packageId };
45
+ await Assistants(dataContext).save(assistantWithPackageId);
46
+ }
47
+ }
48
+ /**
49
+ * Attempts to extract an `IPackageDefinitionResult` from a bundle's module exports.
50
+ * Returns undefined if the bundle doesn't export a contract-based package definition.
51
+ */
52
+ function extractPackageDefinition(bundleExports) {
53
+ const def = bundleExports?.packageDefinition;
54
+ if (def && Array.isArray(def.contracts) && def.contracts.length > 0) {
55
+ return def;
56
+ }
57
+ return undefined;
58
+ }
59
+ /**
60
+ * Determines whether a bundle exports a contract-based package definition,
61
+ * indicating it should be installed via the new contract-aware path.
62
+ */
63
+ function hasPackageDefinition(bundleExports) {
64
+ return extractPackageDefinition(bundleExports) !== undefined;
65
+ }
@@ -1 +1,2 @@
1
+ export * from "./contract-package-loader";
1
2
  export * from "./package-loader";
@@ -14,4 +14,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./contract-package-loader"), exports);
17
18
  __exportStar(require("./package-loader"), exports);
@@ -6,8 +6,12 @@ export declare class PackageLoader {
6
6
  static PeersSDK: any;
7
7
  static Zod: any;
8
8
  private packageInstances;
9
+ /** Packages that were installed via the contract-based path (tools/assistants/tables already saved with packageId). */
10
+ private contractInstalledPackages;
9
11
  require: (<T>(module: string) => T) | undefined;
10
12
  constructor(dataContext: DataContext);
13
+ /** Returns true if a package was installed via the contract-based definePackage() path. */
14
+ isContractInstalled(packageId: string): boolean;
11
15
  loadAllPackages(opts?: {
12
16
  force?: boolean;
13
17
  }): Promise<void>;
@@ -16,6 +20,13 @@ export declare class PackageLoader {
16
20
  localPath?: string;
17
21
  }): Promise<IPeersPackage | undefined>;
18
22
  private _readLocalBundle;
23
+ /**
24
+ * Post-correction: if the package definition carries version/versionTag,
25
+ * update the active IPackageVersion record to match. This fixes cases where
26
+ * the PV record was created with stale or placeholder values (e.g. "0.0.1")
27
+ * before the bundle was evaluated.
28
+ */
29
+ private correctPackageVersion;
19
30
  private _evaluateBundle;
20
31
  }
21
32
  export declare function setDefaultRequire(require: <T>(module: string) => T): void;
@@ -7,15 +7,22 @@ const package_versions_1 = require("../data/package-versions");
7
7
  const packages_1 = require("../data/packages");
8
8
  const tools_1 = require("../data/tools");
9
9
  const tools_2 = require("../tools");
10
+ const contract_package_loader_1 = require("./contract-package-loader");
10
11
  class PackageLoader {
11
12
  dataContext;
12
13
  static PeersSDK;
13
14
  static Zod;
14
15
  packageInstances = {};
16
+ /** Packages that were installed via the contract-based path (tools/assistants/tables already saved with packageId). */
17
+ contractInstalledPackages = new Set();
15
18
  require = undefined;
16
19
  constructor(dataContext) {
17
20
  this.dataContext = dataContext;
18
21
  }
22
+ /** Returns true if a package was installed via the contract-based definePackage() path. */
23
+ isContractInstalled(packageId) {
24
+ return this.contractInstalledPackages.has(packageId);
25
+ }
19
26
  async loadAllPackages(opts) {
20
27
  const packages = await (0, packages_1.Packages)(this.dataContext).list();
21
28
  await Promise.all(packages.filter((pkg) => !pkg.disabled).map((pkg) => this.loadPackage(pkg, opts)));
@@ -91,6 +98,36 @@ class PackageLoader {
91
98
  return;
92
99
  }
93
100
  }
101
+ /**
102
+ * Post-correction: if the package definition carries version/versionTag,
103
+ * update the active IPackageVersion record to match. This fixes cases where
104
+ * the PV record was created with stale or placeholder values (e.g. "0.0.1")
105
+ * before the bundle was evaluated.
106
+ */
107
+ correctPackageVersion(pkg, def) {
108
+ if (!pkg.activePackageVersionId)
109
+ return;
110
+ if (!def.version && !def.versionTag)
111
+ return;
112
+ const pvTable = (0, package_versions_1.PackageVersions)(this.dataContext);
113
+ pvTable.get(pkg.activePackageVersionId).then((pv) => {
114
+ if (!pv)
115
+ return;
116
+ let needsUpdate = false;
117
+ const updated = { ...pv };
118
+ if (def.version && pv.version !== def.version) {
119
+ updated.version = def.version;
120
+ needsUpdate = true;
121
+ }
122
+ if (def.versionTag && pv.versionTag !== def.versionTag) {
123
+ updated.versionTag = def.versionTag;
124
+ needsUpdate = true;
125
+ }
126
+ if (needsUpdate) {
127
+ pvTable.save(updated);
128
+ }
129
+ });
130
+ }
94
131
  _evaluateBundle(pkg, bundleCode) {
95
132
  // Node.js built-in modules that do not exist in React Native (and would cause
96
133
  // a fatal native error if passed through to RN's require). We block them here
@@ -156,18 +193,32 @@ class PackageLoader {
156
193
  const bundleFunction = new Function("module", "exports", "require", bundleCode);
157
194
  bundleFunction(module, moduleExports, customRequire);
158
195
  const bundleExports = module.exports;
196
+ // Check for contract-based package definition (new path)
197
+ const packageDefinition = (0, contract_package_loader_1.extractPackageDefinition)(bundleExports);
198
+ if (packageDefinition) {
199
+ if (packageDefinition.packageId !== pkg.packageId) {
200
+ console.warn(`[PackageLoader] packageId mismatch: definition has "${packageDefinition.packageId}" but DB record has "${pkg.packageId}"`);
201
+ }
202
+ (0, contract_package_loader_1.installContractPackage)(this.dataContext, packageDefinition);
203
+ this.contractInstalledPackages.add(packageDefinition.packageId);
204
+ this.correctPackageVersion(pkg, packageDefinition);
205
+ }
206
+ // Legacy path: extract IPeersPackage for backward compat
159
207
  const packageInstance = bundleExports?.exports || bundleExports?.package || bundleExports?.default || bundleExports;
160
208
  this.packageInstances[pkg.packageId] = packageInstance;
161
209
  if (packageInstance && typeof packageInstance === "object") {
162
- packageInstance.toolInstances?.forEach((toolInstance) => {
163
- (0, tools_2.registerTool)(toolInstance);
164
- (0, tools_1.Tools)(this.dataContext).save(toolInstance.tool);
165
- });
166
- packageInstance.tableDefinitions?.forEach((tableDefinition) => {
167
- this.dataContext.tableContainer.registerTableDefinition(tableDefinition, {
168
- overwrite: true,
210
+ // Skip legacy tool/table registration if the contract path already handled it
211
+ if (!packageDefinition) {
212
+ packageInstance.toolInstances?.forEach((toolInstance) => {
213
+ (0, tools_2.registerTool)(toolInstance);
214
+ (0, tools_1.Tools)(this.dataContext).save(toolInstance.tool);
215
+ });
216
+ packageInstance.tableDefinitions?.forEach((tableDefinition) => {
217
+ this.dataContext.tableContainer.registerTableDefinition(tableDefinition, {
218
+ overwrite: true,
219
+ });
169
220
  });
170
- });
221
+ }
171
222
  return packageInstance;
172
223
  }
173
224
  return;
@@ -116,6 +116,10 @@ export declare const rpcServerCalls: {
116
116
  voiceDisable: () => Promise<void>;
117
117
  voiceEnable: () => Promise<void>;
118
118
  flushDatabases: () => Promise<void>;
119
+ subscribeEvents: (names: string[]) => Promise<void>;
120
+ unsubscribeEvents: (names: string[]) => Promise<void>;
121
+ subscribePrefixes: (prefixes: string[]) => Promise<void>;
122
+ unsubscribePrefixes: (prefixes: string[]) => Promise<void>;
119
123
  };
120
124
  export declare const rpcClientCalls: {
121
125
  ping: (msg: string) => Promise<string>;
package/dist/rpc-types.js CHANGED
@@ -67,10 +67,11 @@ exports.rpcServerCalls = {
67
67
  // Flush all in-memory database snapshots to durable storage (IndexedDB in PWA).
68
68
  // No-op on Electron where better-sqlite3 writes are synchronous to disk.
69
69
  flushDatabases: (async () => { }),
70
- // TODO try to get rid of this and rely on the client-side table and server-side table individually emitting events
71
- // TODO TODO before deleting this, check if we can stop client-side tables from emitting events and rely solely on server-side tables
72
- // propagating events with rpcClientCalls.emitEvent. It's very likely we're currently seeing two events for every one write originating from the UI
73
- // emitEvent: _na as ((event: IEventData) => Promise<boolean>),
70
+ // Event subscription management (for selective proxying)
71
+ subscribeEvents: rpcStub("subscribeEvents"),
72
+ unsubscribeEvents: rpcStub("unsubscribeEvents"),
73
+ subscribePrefixes: rpcStub("subscribePrefixes"),
74
+ unsubscribePrefixes: rpcStub("unsubscribePrefixes"),
74
75
  };
75
76
  exports.rpcClientCalls = {
76
77
  ping: async (msg) => `pong: ${msg}`,
@@ -40,6 +40,7 @@ export declare const workflowSchema: z.ZodObject<{
40
40
  createdBy: z.ZodEffects<z.ZodString, string, string>;
41
41
  createdAt: z.ZodDefault<z.ZodDate>;
42
42
  updatedAt: z.ZodDefault<z.ZodDate>;
43
+ packageId: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
43
44
  }, "strip", z.ZodTypeAny, {
44
45
  name: string;
45
46
  description: string;
@@ -54,12 +55,14 @@ export declare const workflowSchema: z.ZodObject<{
54
55
  subWorkflowId?: string | undefined;
55
56
  }[];
56
57
  updatedAt: Date;
58
+ packageId?: string | undefined;
57
59
  }, {
58
60
  name: string;
59
61
  description: string;
60
62
  createdBy: string;
61
63
  workflowId: string;
62
64
  defaultAssistantId: string;
65
+ packageId?: string | undefined;
63
66
  createdAt?: Date | undefined;
64
67
  instructions?: {
65
68
  markdown?: string | undefined;
@@ -35,4 +35,5 @@ exports.workflowSchema = zod_1.z.object({
35
35
  .date()
36
36
  .default(() => new Date())
37
37
  .describe("The date the workflow was last updated"),
38
+ packageId: zod_types_1.zodPeerId.optional().describe("The package that installed this workflow, if any"),
38
39
  });
package/dist/utils.d.ts CHANGED
@@ -31,6 +31,21 @@ export declare function simpleHash(str: string): number;
31
31
  export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
32
32
  /** `simpleHash` of JSON-stable stringification of an object. */
33
33
  export declare function simpleObjectHash(obj: any): number;
34
+ /**
35
+ * Generates a deterministic 25-character peer ID for a persistent variable.
36
+ * The ID has the fixed prefix `000pvar0` (8 chars) followed by 17 base-36 characters
37
+ * derived from a SHA-512 hash of the input name. Two calls with the same name always
38
+ * return the same ID.
39
+ *
40
+ * @param name The persistent variable name as stored in the database
41
+ * (already includes group suffixes for groupDevice/groupUser scopes)
42
+ */
43
+ export declare function deterministicPvarId(name: string): string;
44
+ /**
45
+ * Returns whether a peer ID was generated by {@link deterministicPvarId}
46
+ * (starts with the `000pvar0` prefix).
47
+ */
48
+ export declare function isDeterministicPvarId(id: string): boolean;
34
49
  /** Resolves after `ms` milliseconds (0 yields a microtask-yield on most runtimes). */
35
50
  export declare function sleep(ms?: number): Promise<void>;
36
51
  /** Turns `fooBar` into `Foo Bar` for labels (also replaces `_` with space). */