@granular-software/sdk 0.4.12 → 0.4.14

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/index.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  import * as Automerge from '@automerge/automerge';
2
+ import { normalizeEffectBehaviorSummary } from '@granular-software/metamodel-effect-behaviors';
3
+ import { DEFAULT_METAMODEL_PACKAGES } from '@granular-software/metamodel-preset-default';
2
4
 
3
5
  var __create = Object.create;
4
6
  var __defProp = Object.defineProperty;
@@ -5383,6 +5385,123 @@ function resolveApiUrl(explicitApiUrl, mode) {
5383
5385
  }
5384
5386
  return resolveEndpointMode(mode) === "local" ? LOCAL_API_URL : PRODUCTION_API_URL;
5385
5387
  }
5388
+ function computeEffectKey(effect) {
5389
+ const attachedClass = effect.className?.trim();
5390
+ if (!attachedClass) {
5391
+ return `global:${effect.name}`;
5392
+ }
5393
+ return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
5394
+ }
5395
+ function normalizeEffectBehaviors(value) {
5396
+ return normalizeEffectBehaviorSummary(
5397
+ value
5398
+ ) || {};
5399
+ }
5400
+ function resolveInvocationMode(context) {
5401
+ const mode = context?.invocation?.mode;
5402
+ if (mode === "dryRun" || mode === "reverse") {
5403
+ return mode;
5404
+ }
5405
+ return "execute";
5406
+ }
5407
+ function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
5408
+ const explicitReverseHandler = request.context?.invocation?.reverseHandler?.trim();
5409
+ const configuredReverseHandler = behaviors.reverse?.handler?.trim();
5410
+ const reverseHandler = explicitReverseHandler || configuredReverseHandler;
5411
+ if (!reverseHandler) {
5412
+ return void 0;
5413
+ }
5414
+ if (reverseHandler.includes(":")) {
5415
+ return effectMap.get(reverseHandler);
5416
+ }
5417
+ const directMatch = effectMap.get(reverseHandler);
5418
+ if (directMatch) {
5419
+ return directMatch;
5420
+ }
5421
+ const candidateKeys = [
5422
+ computeEffectKey({
5423
+ name: reverseHandler,
5424
+ className: currentEffect.className,
5425
+ static: currentEffect.static
5426
+ }),
5427
+ computeEffectKey({
5428
+ name: reverseHandler
5429
+ })
5430
+ ];
5431
+ for (const candidateKey of candidateKeys) {
5432
+ const candidate = effectMap.get(candidateKey);
5433
+ if (candidate) {
5434
+ return candidate;
5435
+ }
5436
+ }
5437
+ return void 0;
5438
+ }
5439
+ function resolveHandlerForMode(effectMap, effect, request) {
5440
+ const behaviors = normalizeEffectBehaviors(request.context?.behaviors || effect.metamodels || void 0);
5441
+ const mode = resolveInvocationMode(request.context);
5442
+ if (mode === "dryRun") {
5443
+ if (effect.dryRunHandler) {
5444
+ return { effect, mode, handler: effect.dryRunHandler };
5445
+ }
5446
+ if (behaviors.dryRun?.enabled) {
5447
+ return { effect, mode, handler: effect.handler };
5448
+ }
5449
+ throw new Error(`Dry run is not supported for ${request.effectKey}`);
5450
+ }
5451
+ if (mode === "reverse") {
5452
+ if (effect.reverseHandler) {
5453
+ return { effect, mode, handler: effect.reverseHandler };
5454
+ }
5455
+ const reverseEffect = resolveReverseEffect(effectMap, effect, request, behaviors);
5456
+ if (reverseEffect) {
5457
+ return {
5458
+ effect: reverseEffect,
5459
+ mode,
5460
+ handler: reverseEffect.reverseHandler || reverseEffect.handler
5461
+ };
5462
+ }
5463
+ throw new Error(`Reverse execution is not supported for ${request.effectKey}`);
5464
+ }
5465
+ return { effect, mode, handler: effect.handler };
5466
+ }
5467
+ async function invokeRegisteredEffect(effectMap, request) {
5468
+ const effect = effectMap.get(request.effectKey);
5469
+ if (!effect) {
5470
+ throw new Error(`Effect handler not found: ${request.effectKey}`);
5471
+ }
5472
+ const resolved = resolveHandlerForMode(effectMap, effect, request);
5473
+ const context = {
5474
+ ...request.context || {},
5475
+ behaviors: normalizeEffectBehaviors(request.context?.behaviors || effect.metamodels || void 0),
5476
+ invocation: {
5477
+ mode: resolved.mode,
5478
+ sourceEffectKey: request.effectKey,
5479
+ sourceEffectName: request.effectName,
5480
+ ...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {}
5481
+ }
5482
+ };
5483
+ if (resolved.effect.className && !resolved.effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
5484
+ const { _objectId, ...rest } = request.input;
5485
+ return resolved.handler(_objectId, rest, context);
5486
+ }
5487
+ return resolved.handler(request.input, context);
5488
+ }
5489
+ function buildFieldMetamodelMutations(fieldPath, spec) {
5490
+ return DEFAULT_METAMODEL_PACKAGES.flatMap(
5491
+ (pkg) => pkg.manifest?.buildFieldMutations?.(fieldPath, spec) || []
5492
+ );
5493
+ }
5494
+ function buildModelMetamodelMutations(modelPath, spec) {
5495
+ return DEFAULT_METAMODEL_PACKAGES.flatMap(
5496
+ (pkg) => pkg.manifest?.buildModelMutations?.(modelPath, spec) || []
5497
+ );
5498
+ }
5499
+ function buildEffectMetamodelMutations(toolPath, spec) {
5500
+ if (!spec) return [];
5501
+ return DEFAULT_METAMODEL_PACKAGES.flatMap(
5502
+ (pkg) => pkg.manifest?.buildEffectMutations?.(toolPath, spec) || []
5503
+ );
5504
+ }
5386
5505
 
5387
5506
  // src/client.ts
5388
5507
  var STANDARD_MODULES_OPERATIONS = [
@@ -5398,7 +5517,7 @@ var STANDARD_MODULES_OPERATIONS = [
5398
5517
  var BUILTIN_MODULES = {
5399
5518
  "standard_modules": STANDARD_MODULES_OPERATIONS
5400
5519
  };
5401
- function computeEffectKey(effect) {
5520
+ function computeEffectKey2(effect) {
5402
5521
  const attachedClass = effect.className?.trim();
5403
5522
  if (!attachedClass) {
5404
5523
  return `global:${effect.name}`;
@@ -5472,6 +5591,19 @@ function normalizeUser(user) {
5472
5591
  permissions: Array.isArray(user.permissions) ? user.permissions : []
5473
5592
  };
5474
5593
  }
5594
+ function normalizeEnvironmentData(environment) {
5595
+ const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : { mode: "pinned", versionId: environment.versionId || environment.buildId });
5596
+ const environmentName = environment.environment || environment.envName || "prod";
5597
+ return {
5598
+ ...environment,
5599
+ ontologyId: environment.ontologyId || environment.sandboxId,
5600
+ versionId: environment.versionId || environment.buildId,
5601
+ envName: environmentName,
5602
+ environment: environmentName,
5603
+ buildPolicy,
5604
+ tracking: environment.tracking || buildPolicy
5605
+ };
5606
+ }
5475
5607
  var Environment = class extends Session {
5476
5608
  envData;
5477
5609
  _apiKey;
@@ -5490,10 +5622,26 @@ var Environment = class extends Session {
5490
5622
  get sandboxId() {
5491
5623
  return this.envData.sandboxId;
5492
5624
  }
5625
+ /** The ontology ID */
5626
+ get ontologyId() {
5627
+ return this.envData.ontologyId || this.envData.sandboxId;
5628
+ }
5493
5629
  /** The subject ID */
5494
5630
  get subjectId() {
5495
5631
  return this.envData.subjectId;
5496
5632
  }
5633
+ /** The named environment slot, such as dev or prod */
5634
+ get envName() {
5635
+ return this.envData.envName;
5636
+ }
5637
+ /** The named environment slot, such as dev or prod */
5638
+ get environment() {
5639
+ return this.envData.environment || this.envData.envName;
5640
+ }
5641
+ /** The resolved ontology version backing this environment */
5642
+ get versionId() {
5643
+ return this.envData.versionId || this.envData.buildId;
5644
+ }
5497
5645
  /** Internal Granular user identifier for this environment */
5498
5646
  get granularId() {
5499
5647
  return this.envData.subjectId;
@@ -5975,6 +6123,87 @@ var Environment = class extends Session {
5975
6123
  }
5976
6124
  return ref;
5977
6125
  }
6126
+ async _runGraphql(query, label) {
6127
+ const result = await this.graphql(query);
6128
+ if (result.errors?.length) {
6129
+ throw new Error(`${label}: ${result.errors[0].message}`);
6130
+ }
6131
+ return result.data;
6132
+ }
6133
+ async _applyFieldMetamodels(fieldPath, spec) {
6134
+ for (const mutation of buildFieldMetamodelMutations(fieldPath, spec)) {
6135
+ await this._runGraphql(mutation.query, mutation.label);
6136
+ }
6137
+ }
6138
+ async _applyModelMetamodels(modelPath, op) {
6139
+ for (const mutation of buildModelMetamodelMutations(modelPath, op)) {
6140
+ await this._runGraphql(mutation.query, mutation.label);
6141
+ }
6142
+ }
6143
+ async _ensureWorkspaceToolsRoot() {
6144
+ await this._runGraphql(
6145
+ `mutation { create_model(path: "workspace", label: "workspace") { model { path } } }`,
6146
+ "ensure workspace"
6147
+ ).catch((error) => {
6148
+ if (!error.message.includes("already exists")) throw error;
6149
+ });
6150
+ await this._runGraphql(
6151
+ `mutation { at(path: "workspace") { create_submodel(subpath: "tools", label: "Tools") { model { path } } } }`,
6152
+ "ensure workspace:tools"
6153
+ ).catch((error) => {
6154
+ if (!error.message.includes("already exists")) throw error;
6155
+ });
6156
+ await this._runGraphql(
6157
+ `mutation { at(path: "workspace:tools") { create_submodel(subpath: "declared", label: "Declared") { model { path } } } }`,
6158
+ "ensure workspace:tools:declared"
6159
+ ).catch((error) => {
6160
+ if (!error.message.includes("already exists")) throw error;
6161
+ });
6162
+ }
6163
+ async _storeEffectSchemas(toolPath, effect) {
6164
+ await this._runGraphql(
6165
+ `mutation { at(path: ${JSON.stringify(toolPath)}) { create_submodel(subpath: "inputSchema", label: "inputSchema") { set_string_value(value: ${JSON.stringify(JSON.stringify(effect.inputSchema))}) { done } } } }`,
6166
+ `store inputSchema on ${toolPath}`
6167
+ ).catch((error) => {
6168
+ if (!error.message.includes("already exists")) throw error;
6169
+ });
6170
+ if (effect.outputSchema) {
6171
+ await this._runGraphql(
6172
+ `mutation { at(path: ${JSON.stringify(toolPath)}) { create_submodel(subpath: "outputSchema", label: "outputSchema") { set_string_value(value: ${JSON.stringify(JSON.stringify(effect.outputSchema))}) { done } } } }`,
6173
+ `store outputSchema on ${toolPath}`
6174
+ ).catch((error) => {
6175
+ if (!error.message.includes("already exists")) throw error;
6176
+ });
6177
+ }
6178
+ }
6179
+ async _applyEffectMetamodels(toolPath, metamodels) {
6180
+ for (const mutation of buildEffectMetamodelMutations(toolPath, metamodels)) {
6181
+ await this._runGraphql(mutation.query, mutation.label);
6182
+ }
6183
+ }
6184
+ async _applyEffectDeclaration(effect, aliasMap) {
6185
+ let containerPath = "workspace:tools:declared";
6186
+ if (effect.attachedClass) {
6187
+ containerPath = this._resolveAlias(effect.attachedClass, aliasMap);
6188
+ } else {
6189
+ await this._ensureWorkspaceToolsRoot();
6190
+ }
6191
+ await this._runGraphql(
6192
+ `mutation { at(path: ${JSON.stringify(containerPath)}) { create_submodel(subpath: ${JSON.stringify(effect.name)}, label: ${JSON.stringify(effect.name)}) { model { path } } } }`,
6193
+ `create effect model ${containerPath}:${effect.name}`
6194
+ ).catch((error) => {
6195
+ if (!error.message.includes("already exists")) throw error;
6196
+ });
6197
+ const toolPath = `${containerPath}:${effect.name}`;
6198
+ if (effect.description) {
6199
+ await this._runGraphql(
6200
+ `mutation { at(path: ${JSON.stringify(toolPath)}) { set_description(description: ${JSON.stringify(effect.description)}) { done } } }`,
6201
+ `set effect description on ${toolPath}`
6202
+ );
6203
+ }
6204
+ await this._storeEffectSchemas(toolPath, effect);
6205
+ await this._applyEffectMetamodels(toolPath, effect.metamodels);
6206
+ }
5978
6207
  /**
5979
6208
  * Apply a single manifest operation via GraphQL
5980
6209
  */
@@ -5997,24 +6226,32 @@ var Environment = class extends Session {
5997
6226
  const extendsRef = op.extends ? this._resolveAlias(op.extends, aliasMap) : void 0;
5998
6227
  const instanceOfRef = op.instanceOf ? this._resolveAlias(op.instanceOf, aliasMap) : void 0;
5999
6228
  if (extendsRef) {
6000
- await this.graphql(
6001
- `mutation { create_model(path: "${op.create}", label: "${label}") { model { path } } }`
6229
+ await this._runGraphql(
6230
+ `mutation { create_model(path: "${op.create}", label: "${label}") { model { path } } }`,
6231
+ `create model ${op.create}`
6002
6232
  );
6003
- await this.graphql(
6004
- `mutation { at(path: "${op.create}") { add_superclass(superclass: "${extendsRef}") { model { path } } } }`
6233
+ await this._runGraphql(
6234
+ `mutation { at(path: "${op.create}") { add_superclass(superclass: "${extendsRef}") { model { path } } } }`,
6235
+ `add superclass ${extendsRef} to ${op.create}`
6005
6236
  );
6006
6237
  } else if (instanceOfRef) {
6007
- await this.graphql(
6008
- `mutation { at(path: "${instanceOfRef}") { instantiate(path: "${op.create}", label: "${label}") { model { path } } } }`
6238
+ await this._runGraphql(
6239
+ `mutation { at(path: "${instanceOfRef}") { instantiate(path: "${op.create}", label: "${label}") { model { path } } } }`,
6240
+ `instantiate ${op.create} from ${instanceOfRef}`
6009
6241
  );
6010
6242
  } else {
6011
- await this.graphql(
6012
- `mutation { create_model(path: "${op.create}", label: "${label}") { model { path } } }`
6243
+ await this._runGraphql(
6244
+ `mutation { create_model(path: "${op.create}", label: "${label}") { model { path } } }`,
6245
+ `create model ${op.create}`
6013
6246
  );
6014
6247
  }
6015
6248
  if (op.has) {
6016
6249
  await this._applyFields(op.create, op.has);
6017
6250
  }
6251
+ await this._applyModelMetamodels(op.create, op);
6252
+ if (op.withEffect) {
6253
+ await this._applyEffectDeclaration(op.withEffect, aliasMap);
6254
+ }
6018
6255
  return;
6019
6256
  }
6020
6257
  if (op.on) {
@@ -6022,8 +6259,15 @@ var Environment = class extends Session {
6022
6259
  if (op.has) {
6023
6260
  await this._applyFields(target, op.has);
6024
6261
  }
6262
+ await this._applyModelMetamodels(target, op);
6263
+ if (op.withEffect) {
6264
+ await this._applyEffectDeclaration(op.withEffect, aliasMap);
6265
+ }
6025
6266
  return;
6026
6267
  }
6268
+ if (op.withEffect) {
6269
+ await this._applyEffectDeclaration(op.withEffect, aliasMap);
6270
+ }
6027
6271
  }
6028
6272
  /**
6029
6273
  * Apply field definitions (has) to a model via GraphQL
@@ -6031,81 +6275,91 @@ var Environment = class extends Session {
6031
6275
  async _applyFields(modelPath, fields) {
6032
6276
  for (const [fieldName, spec] of Object.entries(fields)) {
6033
6277
  const fieldLabel = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
6034
- await this.graphql(
6278
+ await this._runGraphql(
6035
6279
  `mutation {
6036
- at(path: "${modelPath}") {
6037
- create_submodel(subpath: "${fieldName}", label: "${fieldLabel}") {
6280
+ at(path: ${JSON.stringify(modelPath)}) {
6281
+ create_submodel(subpath: ${JSON.stringify(fieldName)}, label: ${JSON.stringify(fieldLabel)}) {
6038
6282
  model { path }
6039
6283
  }
6040
6284
  }
6041
- }`
6042
- );
6285
+ }`,
6286
+ `create field ${modelPath}.${fieldName}`
6287
+ ).catch((error) => {
6288
+ if (!error.message.includes("already exists")) throw error;
6289
+ });
6043
6290
  if (spec.type) {
6044
- await this.graphql(
6291
+ await this._runGraphql(
6045
6292
  `mutation {
6046
- at(path: "${modelPath}") {
6047
- at(submodel: "${fieldName}") {
6048
- add_prototype(prototype: "${spec.type}") { done }
6293
+ at(path: ${JSON.stringify(modelPath)}) {
6294
+ at(submodel: ${JSON.stringify(fieldName)}) {
6295
+ add_prototype(prototype: ${JSON.stringify(spec.type)}) { done }
6049
6296
  }
6050
6297
  }
6051
- }`
6298
+ }`,
6299
+ `set prototype on ${modelPath}:${fieldName}`
6052
6300
  );
6053
6301
  }
6054
6302
  if (spec.description) {
6055
- await this.graphql(
6303
+ await this._runGraphql(
6056
6304
  `mutation {
6057
- at(path: "${modelPath}") {
6058
- at(submodel: "${fieldName}") {
6059
- set_description(description: "${spec.description}") { done }
6305
+ at(path: ${JSON.stringify(modelPath)}) {
6306
+ at(submodel: ${JSON.stringify(fieldName)}) {
6307
+ set_description(description: ${JSON.stringify(spec.description)}) { done }
6060
6308
  }
6061
6309
  }
6062
- }`
6310
+ }`,
6311
+ `set description on ${modelPath}:${fieldName}`
6063
6312
  );
6064
6313
  }
6065
6314
  if (spec.value !== void 0 && spec.value !== null) {
6066
6315
  if (typeof spec.value === "string") {
6067
- await this.graphql(
6316
+ await this._runGraphql(
6068
6317
  `mutation {
6069
- at(path: "${modelPath}") {
6070
- at(submodel: "${fieldName}") {
6071
- set_string_value(value: "${spec.value}") { done }
6318
+ at(path: ${JSON.stringify(modelPath)}) {
6319
+ at(submodel: ${JSON.stringify(fieldName)}) {
6320
+ set_string_value(value: ${JSON.stringify(spec.value)}) { done }
6072
6321
  }
6073
6322
  }
6074
- }`
6323
+ }`,
6324
+ `set string value on ${modelPath}:${fieldName}`
6075
6325
  );
6076
6326
  } else if (typeof spec.value === "number") {
6077
- await this.graphql(
6327
+ await this._runGraphql(
6078
6328
  `mutation {
6079
- at(path: "${modelPath}") {
6080
- at(submodel: "${fieldName}") {
6329
+ at(path: ${JSON.stringify(modelPath)}) {
6330
+ at(submodel: ${JSON.stringify(fieldName)}) {
6081
6331
  set_number_value(value: ${spec.value}) { done }
6082
6332
  }
6083
6333
  }
6084
- }`
6334
+ }`,
6335
+ `set number value on ${modelPath}:${fieldName}`
6085
6336
  );
6086
6337
  } else if (typeof spec.value === "boolean") {
6087
- await this.graphql(
6338
+ await this._runGraphql(
6088
6339
  `mutation {
6089
- at(path: "${modelPath}") {
6090
- at(submodel: "${fieldName}") {
6340
+ at(path: ${JSON.stringify(modelPath)}) {
6341
+ at(submodel: ${JSON.stringify(fieldName)}) {
6091
6342
  set_boolean_value(value: ${spec.value}) { done }
6092
6343
  }
6093
6344
  }
6094
- }`
6345
+ }`,
6346
+ `set boolean value on ${modelPath}:${fieldName}`
6095
6347
  );
6096
6348
  }
6097
6349
  }
6098
6350
  if (spec.ref) {
6099
- await this.graphql(
6351
+ await this._runGraphql(
6100
6352
  `mutation {
6101
- at(path: "${modelPath}") {
6102
- at(submodel: "${fieldName}") {
6103
- set_reference(reference: "${spec.ref}") { done }
6353
+ at(path: ${JSON.stringify(modelPath)}) {
6354
+ at(submodel: ${JSON.stringify(fieldName)}) {
6355
+ set_reference(reference: ${JSON.stringify(spec.ref)}) { done }
6104
6356
  }
6105
6357
  }
6106
- }`
6358
+ }`,
6359
+ `set reference on ${modelPath}:${fieldName}`
6107
6360
  );
6108
6361
  }
6362
+ await this._applyFieldMetamodels(`${modelPath}:${fieldName}`, spec);
6109
6363
  if (spec.has) {
6110
6364
  await this._applyFields(`${modelPath}:${fieldName}`, spec.has);
6111
6365
  }
@@ -6359,7 +6613,7 @@ var Granular = class {
6359
6613
  throw new Error("connect() requires either userId, granularId, or a user object returned by recordUser().");
6360
6614
  }
6361
6615
  /**
6362
- * Connect to a sandbox and establish a real-time environment session.
6616
+ * Connect to an ontology environment and establish a real-time session.
6363
6617
  *
6364
6618
  * Effects are registered at the sandbox level via `granular.registerEffect()`
6365
6619
  * or `granular.registerEffects()`. Sessions pick up live availability from
@@ -6371,7 +6625,8 @@ var Granular = class {
6371
6625
  * @example
6372
6626
  * ```typescript
6373
6627
  * const environment = await granular.connect({
6374
- * sandbox: 'my-sandbox',
6628
+ * ontology: 'my-ontology',
6629
+ * environment: 'dev',
6375
6630
  * userId: 'user_123',
6376
6631
  * permissions: ['agent'],
6377
6632
  * });
@@ -6391,17 +6646,28 @@ var Granular = class {
6391
6646
  *
6392
6647
  * console.log(await job.result); // 'Hello!'
6393
6648
  * ```
6394
- */
6649
+ */
6395
6650
  async connect(options) {
6396
6651
  const clientId = options.clientId || `client_${Date.now()}`;
6652
+ const ontology = options.ontology;
6653
+ if (!ontology) {
6654
+ throw new Error("connect() requires `ontology`.");
6655
+ }
6656
+ const environmentName = options.environment;
6657
+ if (!environmentName) {
6658
+ throw new Error("connect() requires `environment`.");
6659
+ }
6660
+ const tagName = options.tagName?.trim() || void 0;
6397
6661
  const user = await this.resolveConnectUser(options);
6398
- const sandbox = await this.findOrCreateSandbox(options.sandbox);
6662
+ const sandbox = await this.findOrCreateSandbox(ontology);
6399
6663
  for (const profileName of user.permissions) {
6400
6664
  const profileId = await this.ensurePermissionProfile(sandbox.sandboxId, profileName);
6401
6665
  await this.ensureAssignment(user.granularId, sandbox.sandboxId, profileId);
6402
6666
  }
6403
6667
  const envData = await this.environments.create(sandbox.sandboxId, {
6404
6668
  subjectId: user.granularId,
6669
+ environment: environmentName,
6670
+ tagName,
6405
6671
  permissionProfileId: null
6406
6672
  });
6407
6673
  await this.activateEnvironment(envData.environmentId);
@@ -6445,7 +6711,7 @@ var Granular = class {
6445
6711
  }
6446
6712
  serializeEffect(effect) {
6447
6713
  return {
6448
- effectKey: computeEffectKey(effect),
6714
+ effectKey: computeEffectKey2(effect),
6449
6715
  name: effect.name,
6450
6716
  description: effect.description,
6451
6717
  inputSchema: effect.inputSchema,
@@ -6481,23 +6747,54 @@ var Granular = class {
6481
6747
  const host = await this.ensureSandboxEffectHost(sandboxId);
6482
6748
  await this.publishSandboxEffectCatalog(host);
6483
6749
  }
6484
- startEffectHostHeartbeat(host) {
6485
- if (host.heartbeatTimer) {
6486
- clearInterval(host.heartbeatTimer);
6750
+ recoverEffectHost(host, error) {
6751
+ if (host.recovering) {
6752
+ return;
6753
+ }
6754
+ if (this.sandboxEffectHosts.get(host.sandboxId) !== host) {
6755
+ return;
6487
6756
  }
6488
- host.wsClient.call("client.heartbeat", {}).catch((error) => {
6757
+ host.recovering = true;
6758
+ this.stopEffectHostHeartbeat(host);
6759
+ this.sandboxEffectHosts.delete(host.sandboxId);
6760
+ try {
6761
+ host.wsClient.disconnect();
6762
+ } catch (disconnectError) {
6489
6763
  console.warn(
6490
- `[Granular] Initial effect host heartbeat failed for sandbox ${host.sandboxId}:`,
6491
- error
6764
+ `[Granular] Failed to disconnect stale effect host for sandbox ${host.sandboxId}:`,
6765
+ disconnectError
6766
+ );
6767
+ }
6768
+ void this.ensureSandboxEffectHost(host.sandboxId).catch((reconnectError) => {
6769
+ console.error(
6770
+ `[Granular] Failed to recover effect host for sandbox ${host.sandboxId} after heartbeat failure:`,
6771
+ reconnectError
6492
6772
  );
6773
+ console.error("[Granular] Original heartbeat failure:", error);
6493
6774
  });
6494
- host.heartbeatTimer = setInterval(() => {
6775
+ }
6776
+ startEffectHostHeartbeat(host) {
6777
+ if (host.heartbeatTimer) {
6778
+ clearInterval(host.heartbeatTimer);
6779
+ }
6780
+ host.heartbeatInFlight = false;
6781
+ const sendHeartbeat = (failureLabel, recoverOnFailure) => {
6782
+ if (host.heartbeatInFlight || host.recovering) {
6783
+ return;
6784
+ }
6785
+ host.heartbeatInFlight = true;
6495
6786
  host.wsClient.call("client.heartbeat", {}).catch((error) => {
6496
- console.warn(
6497
- `[Granular] Effect host heartbeat failed for sandbox ${host.sandboxId}:`,
6498
- error
6499
- );
6787
+ console.warn(`${failureLabel} ${host.sandboxId}:`, error);
6788
+ if (recoverOnFailure) {
6789
+ this.recoverEffectHost(host, error);
6790
+ }
6791
+ }).finally(() => {
6792
+ host.heartbeatInFlight = false;
6500
6793
  });
6794
+ };
6795
+ sendHeartbeat("[Granular] Initial effect host heartbeat failed for sandbox", false);
6796
+ host.heartbeatTimer = setInterval(() => {
6797
+ sendHeartbeat("[Granular] Effect host heartbeat failed for sandbox", true);
6501
6798
  }, 1e4);
6502
6799
  }
6503
6800
  stopEffectHostHeartbeat(host) {
@@ -6506,6 +6803,7 @@ var Granular = class {
6506
6803
  }
6507
6804
  clearInterval(host.heartbeatTimer);
6508
6805
  host.heartbeatTimer = null;
6806
+ host.heartbeatInFlight = false;
6509
6807
  }
6510
6808
  async synchronizeEffectHost(host) {
6511
6809
  await host.wsClient.call("client.hello", {
@@ -6541,19 +6839,13 @@ var Granular = class {
6541
6839
  effectClientId,
6542
6840
  clientId,
6543
6841
  wsClient,
6544
- heartbeatTimer: null
6842
+ heartbeatTimer: null,
6843
+ heartbeatInFlight: false,
6844
+ recovering: false
6545
6845
  };
6546
6846
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
6547
6847
  const request = params;
6548
- const effect = this.getSandboxEffectMap(sandboxId).get(request.effectKey);
6549
- if (!effect) {
6550
- throw new Error(`Effect handler not found: ${request.effectKey}`);
6551
- }
6552
- if (effect.className && !effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
6553
- const { _objectId, ...rest } = request.input;
6554
- return effect.handler(_objectId, rest, request.context);
6555
- }
6556
- return effect.handler(request.input, request.context);
6848
+ return invokeRegisteredEffect(this.getSandboxEffectMap(sandboxId), request);
6557
6849
  });
6558
6850
  wsClient.on("open", () => {
6559
6851
  void this.synchronizeEffectHost(host).catch((error) => {
@@ -6596,7 +6888,7 @@ var Granular = class {
6596
6888
  async registerEffect(sandboxNameOrId, effect) {
6597
6889
  const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
6598
6890
  const sandboxId = sandbox.sandboxId;
6599
- this.getSandboxEffectMap(sandboxId).set(computeEffectKey(effect), effect);
6891
+ this.getSandboxEffectMap(sandboxId).set(computeEffectKey2(effect), effect);
6600
6892
  await this.syncSandboxEffectCatalog(sandboxId);
6601
6893
  }
6602
6894
  /**
@@ -6609,7 +6901,7 @@ var Granular = class {
6609
6901
  const sandboxId = sandbox.sandboxId;
6610
6902
  const map = this.getSandboxEffectMap(sandboxId);
6611
6903
  for (const effect of effects) {
6612
- map.set(computeEffectKey(effect), effect);
6904
+ map.set(computeEffectKey2(effect), effect);
6613
6905
  }
6614
6906
  await this.syncSandboxEffectCatalog(sandboxId);
6615
6907
  }
@@ -6810,16 +7102,22 @@ var Granular = class {
6810
7102
  const result = await this.request(
6811
7103
  `/control/sandboxes/${sandboxId}/environments`
6812
7104
  );
6813
- return result.items;
7105
+ return result.items.map(normalizeEnvironmentData);
6814
7106
  },
6815
7107
  get: async (environmentId) => {
6816
- return this.request(`/control/environments/${environmentId}`);
7108
+ return normalizeEnvironmentData(
7109
+ await this.request(`/control/environments/${environmentId}`)
7110
+ );
6817
7111
  },
6818
7112
  create: async (sandboxId, data) => {
6819
- return this.request(`/control/sandboxes/${sandboxId}/environments`, {
7113
+ const environmentName = data.environment || data.envName;
7114
+ return normalizeEnvironmentData(await this.request(`/control/sandboxes/${sandboxId}/environments`, {
6820
7115
  method: "POST",
6821
- body: JSON.stringify(data)
6822
- });
7116
+ body: JSON.stringify({
7117
+ ...data,
7118
+ envName: environmentName
7119
+ })
7120
+ }));
6823
7121
  },
6824
7122
  delete: async (environmentId) => {
6825
7123
  return this.request(`/control/environments/${environmentId}`, {
@@ -6886,6 +7184,6 @@ var Granular = class {
6886
7184
  }
6887
7185
  };
6888
7186
 
6889
- export { Environment, Granular, Session, WSClient };
7187
+ export { Environment, Granular, Session, WSClient, invokeRegisteredEffect, normalizeEffectBehaviors };
6890
7188
  //# sourceMappingURL=index.mjs.map
6891
7189
  //# sourceMappingURL=index.mjs.map