@granular-software/sdk 0.4.65 → 0.4.67

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/cli/index.js CHANGED
@@ -14505,6 +14505,7 @@ function mergeClassSummaryPatch(target, patch) {
14505
14505
  function mergePropertySummaryPatch(target, patch) {
14506
14506
  pushUnique(target.notes, patch.notes || []);
14507
14507
  if (patch.searchable !== void 0) target.searchable = patch.searchable;
14508
+ if (patch.format !== void 0) target.format = patch.format;
14508
14509
  if (patch.searchablePhonetic !== void 0) {
14509
14510
  target.searchablePhonetic = patch.searchablePhonetic;
14510
14511
  }
@@ -16246,6 +16247,91 @@ var stateMachineMetamodelPackage = defineMetamodelPackage({
16246
16247
  }
16247
16248
  });
16248
16249
 
16250
+ // ../metamodel-temporal/src/index.ts
16251
+ function normalizeTemporalFormat(format) {
16252
+ return format === "date" || format === "date-time" ? format : null;
16253
+ }
16254
+ function buildTemporalFieldMutations(fieldPath, format) {
16255
+ const normalized = normalizeTemporalFormat(format);
16256
+ if (!normalized) return [];
16257
+ return [
16258
+ {
16259
+ label: `set temporal format on ${fieldPath}`,
16260
+ query: `mutation { at(path: ${JSON.stringify(fieldPath)}) { set_temporal_format(format: ${JSON.stringify(normalized)}) { format } } }`
16261
+ }
16262
+ ];
16263
+ }
16264
+ var temporalMetamodelPackage = defineMetamodelPackage({
16265
+ id: "temporal",
16266
+ docs: {
16267
+ fieldRows: [
16268
+ {
16269
+ key: "format",
16270
+ description: "Temporal representation for date fields. Use `date` for a civil calendar date and `date-time` when the instant and time matter."
16271
+ }
16272
+ ]
16273
+ },
16274
+ graphql: {
16275
+ typeDefs: [
16276
+ `
16277
+ type TemporalFormatMetamodel {
16278
+ model: Model!
16279
+ format: String!
16280
+ }
16281
+
16282
+ extend type Model {
16283
+ temporal_format: TemporalFormatMetamodel
16284
+ }
16285
+
16286
+ extend type ModelMutation {
16287
+ set_temporal_format(format: String!): TemporalFormatMetamodel
16288
+ }
16289
+ `
16290
+ ],
16291
+ createResolvers({ run }) {
16292
+ return {
16293
+ TemporalFormatMetamodel: {
16294
+ model: (value) => value.model,
16295
+ format: (value) => value.format
16296
+ },
16297
+ Model: {
16298
+ temporal_format: async (ant) => await run(ant.temporal_format())
16299
+ },
16300
+ ModelMutation: {
16301
+ set_temporal_format: async (ant, { format }) => {
16302
+ if (format !== "date" && format !== "date-time") {
16303
+ throw new Error(`Unsupported temporal format: ${format}`);
16304
+ }
16305
+ return {
16306
+ model: await run(ant.set_temporal_format(format)),
16307
+ format
16308
+ };
16309
+ }
16310
+ }
16311
+ };
16312
+ }
16313
+ },
16314
+ manifest: {
16315
+ buildFieldMutations(fieldPath, spec) {
16316
+ return buildTemporalFieldMutations(fieldPath, spec.format);
16317
+ }
16318
+ },
16319
+ summary: {
16320
+ selections: {
16321
+ propertyFields: ["temporal_format { format }"]
16322
+ },
16323
+ readPropertySummary(rawProperty) {
16324
+ const format = rawProperty.temporal_format?.format;
16325
+ return format === "date" || format === "date-time" ? { format } : {};
16326
+ }
16327
+ },
16328
+ domain: {
16329
+ applyToPropertyIR(propertyIR, _propertySummary) {
16330
+ return propertyIR;
16331
+ }
16332
+ }
16333
+ });
16334
+
16249
16335
  // ../metamodel-validation-rule/src/index.ts
16250
16336
  function describeRule(rule) {
16251
16337
  if (rule.message) return rule.message;
@@ -16417,6 +16503,7 @@ var DEFAULT_METAMODEL_PACKAGES = [
16417
16503
  searchableMetamodelPackage,
16418
16504
  validationRuleMetamodelPackage,
16419
16505
  stateMachineMetamodelPackage,
16506
+ temporalMetamodelPackage,
16420
16507
  effectBehaviorsMetamodelPackage,
16421
16508
  policyMetamodelPackage
16422
16509
  ];
@@ -16551,6 +16638,7 @@ Each key under \`has\` is a **field name** on the class. Each value is a **field
16551
16638
  | Property | Required? | Purpose |
16552
16639
  |----------|-----------|---------|
16553
16640
  | \`type\` | **Yes** for normal scalar fields | \`string\`, \`number\`, \`boolean\`, \`email\`, \`url\`, \`date\`, etc. |
16641
+ | \`format\` | For temporal fields | Use \`date\` for a civil calendar date; use \`date-time\` when the time and instant matter. |
16554
16642
  | \`description\` | Strongly recommended | Used in docs and UX. |
16555
16643
  | \`value\` | Optional | **Default** baked into the model (static metadata), not per-record data. Rare for entity fields. |
16556
16644
  | \`ref\` | Optional | Advanced reference. Prefer \`defineRelationship\` for links between entities. |
@@ -20883,7 +20971,6 @@ var DEFAULT_RPC_TIMEOUT_MS = 3e4;
20883
20971
  var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
20884
20972
  var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
20885
20973
  var HARNESS_RUN_RPC_TIMEOUT_MS = 6e5;
20886
- var HEARTBEAT_RPC_TIMEOUT_MS = 15e3;
20887
20974
  var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
20888
20975
  var DEFAULT_RECONNECT_DELAY_MS = 3e3;
20889
20976
  var DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
@@ -20929,8 +21016,6 @@ function rpcTimeoutMsForMethod(method) {
20929
21016
  case "domain.fetchPackagePart":
20930
21017
  case "domain.getSummary":
20931
21018
  return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
20932
- case "client.heartbeat":
20933
- return HEARTBEAT_RPC_TIMEOUT_MS;
20934
21019
  case "effects.publishCatalog":
20935
21020
  case "effects.resetCatalog":
20936
21021
  case "effects.addCatalog":
@@ -20950,6 +21035,7 @@ var WSClient = class {
20950
21035
  sessionId;
20951
21036
  token;
20952
21037
  messageQueue = [];
21038
+ pendingRpcPeriodStartedAt = null;
20953
21039
  rpcHandlers = /* @__PURE__ */ new Map();
20954
21040
  eventHandlers = /* @__PURE__ */ new Map();
20955
21041
  canonicalDocumentQuarantineHandlers = /* @__PURE__ */ new Set();
@@ -20964,6 +21050,7 @@ var WSClient = class {
20964
21050
  cancelConnectAttempt = null;
20965
21051
  documentRepairRequired = false;
20966
21052
  canonicalDocumentActivated = false;
21053
+ lastInboundActivityAt = null;
20967
21054
  options;
20968
21055
  constructor(options) {
20969
21056
  this.options = options;
@@ -20977,6 +21064,29 @@ var WSClient = class {
20977
21064
  get currentSessionId() {
20978
21065
  return this.sessionId;
20979
21066
  }
21067
+ isTransportOpen() {
21068
+ return this.ws?.readyState === READY_STATE_OPEN;
21069
+ }
21070
+ hasPendingRpcRequests() {
21071
+ return this.messageQueue.length > 0;
21072
+ }
21073
+ hasRecentlyPendingRpcPeriod(maxAgeMs, now = Date.now()) {
21074
+ return this.messageQueue.length > 0 && this.pendingRpcPeriodStartedAt !== null && now - this.pendingRpcPeriodStartedAt <= maxAgeMs;
21075
+ }
21076
+ removePendingRpc(id) {
21077
+ this.messageQueue = this.messageQueue.filter(
21078
+ (pending) => pending.id !== id
21079
+ );
21080
+ if (this.messageQueue.length === 0) this.pendingRpcPeriodStartedAt = null;
21081
+ }
21082
+ /**
21083
+ * Whether the current socket has delivered any frame recently. SessionDO
21084
+ * broadcasts a one-way heartbeat every five seconds, so this measures the
21085
+ * transport without adding an RPC that can be delayed by application work.
21086
+ */
21087
+ hasRecentInboundActivity(maxAgeMs, now = Date.now()) {
21088
+ return this.ws?.readyState === READY_STATE_OPEN && this.lastInboundActivityAt !== null && now - this.lastInboundActivityAt <= maxAgeMs;
21089
+ }
20980
21090
  seedDocumentSnapshot(document) {
20981
21091
  if (!document || typeof document !== "object" || Array.isArray(document)) {
20982
21092
  return;
@@ -21128,6 +21238,7 @@ var WSClient = class {
21128
21238
  const socket = this.ws;
21129
21239
  this.connectionEpoch += 1;
21130
21240
  this.ws = null;
21241
+ this.lastInboundActivityAt = null;
21131
21242
  try {
21132
21243
  socket?.close(4001, "Transport failure");
21133
21244
  } catch {
@@ -21210,12 +21321,14 @@ var WSClient = class {
21210
21321
  closeStaleSocket();
21211
21322
  return;
21212
21323
  }
21324
+ this.lastInboundActivityAt = Date.now();
21213
21325
  this.reconnectAttempts = 0;
21214
21326
  this.emit("open", {});
21215
21327
  finish();
21216
21328
  };
21217
21329
  const handleMessage = (data) => {
21218
21330
  if (!isCurrent()) return;
21331
+ this.lastInboundActivityAt = Date.now();
21219
21332
  try {
21220
21333
  const text = typeof data === "string" ? data : data && typeof data === "object" && "toString" in data ? String(data.toString()) : "";
21221
21334
  this.handleMessage(JSON.parse(text));
@@ -21250,9 +21363,7 @@ var WSClient = class {
21250
21363
  this.ws = null;
21251
21364
  closeStaleSocket();
21252
21365
  finish(
21253
- new Error(
21254
- `WebSocket connect timed out after ${connectTimeoutMs}ms`
21255
- )
21366
+ new Error(`WebSocket connect timed out after ${connectTimeoutMs}ms`)
21256
21367
  );
21257
21368
  }, connectTimeoutMs);
21258
21369
  signal?.addEventListener("abort", handleAbort, { once: true });
@@ -21292,6 +21403,7 @@ var WSClient = class {
21292
21403
  pending.reject(error2);
21293
21404
  });
21294
21405
  this.messageQueue = [];
21406
+ this.pendingRpcPeriodStartedAt = null;
21295
21407
  }
21296
21408
  emitReconnectErrorMessage(error2, terminal2 = false) {
21297
21409
  const reconnectInfo = {
@@ -21695,9 +21807,7 @@ var WSClient = class {
21695
21807
  } else {
21696
21808
  pending.resolve(response.result);
21697
21809
  }
21698
- this.messageQueue = this.messageQueue.filter(
21699
- (q) => q.id !== response.id
21700
- );
21810
+ this.removePendingRpc(response.id);
21701
21811
  }
21702
21812
  return;
21703
21813
  }
@@ -21735,16 +21845,24 @@ var WSClient = class {
21735
21845
  const timeout = setTimeout(() => {
21736
21846
  const pending = this.messageQueue.find((q) => q.id === id);
21737
21847
  if (pending) {
21738
- this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
21848
+ this.removePendingRpc(id);
21739
21849
  reject(new Error(`RPC timeout: ${method}`));
21740
21850
  }
21741
21851
  }, timeoutMs);
21742
- this.messageQueue.push({ resolve: resolve2, reject, id, timeout });
21852
+ if (this.messageQueue.length === 0) {
21853
+ this.pendingRpcPeriodStartedAt = Date.now();
21854
+ }
21855
+ this.messageQueue.push({
21856
+ resolve: resolve2,
21857
+ reject,
21858
+ id,
21859
+ timeout
21860
+ });
21743
21861
  try {
21744
21862
  this.ws.send(JSON.stringify(request));
21745
21863
  } catch (error2) {
21746
21864
  clearTimeout(timeout);
21747
- this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
21865
+ this.removePendingRpc(id);
21748
21866
  reject(error2 instanceof Error ? error2 : new Error(String(error2)));
21749
21867
  }
21750
21868
  });
@@ -21849,6 +21967,7 @@ var WSClient = class {
21849
21967
  this.cancelConnectAttempt?.();
21850
21968
  this.cancelConnectAttempt = null;
21851
21969
  this.connectionEpoch += 1;
21970
+ this.lastInboundActivityAt = null;
21852
21971
  if (this.reconnectTimer) {
21853
21972
  clearTimeout(this.reconnectTimer);
21854
21973
  this.reconnectTimer = null;
@@ -25001,16 +25120,100 @@ function normalizeEnvironmentData(environment) {
25001
25120
  setup: normalizeEnvironmentSetupSummary(environment.setup)
25002
25121
  };
25003
25122
  }
25123
+ function isGraphQLMutationDocument(query) {
25124
+ if (typeof query !== "string") return false;
25125
+ const tokens = [];
25126
+ for (let index = 0; index < query.length; ) {
25127
+ const character = query[index];
25128
+ if (/[,\s\ufeff]/.test(character)) {
25129
+ index += 1;
25130
+ continue;
25131
+ }
25132
+ if (character === "#") {
25133
+ while (index < query.length && !/[\r\n]/.test(query[index])) index += 1;
25134
+ continue;
25135
+ }
25136
+ if (character === '"') {
25137
+ const block = query.startsWith('"""', index);
25138
+ let closed = false;
25139
+ index += block ? 3 : 1;
25140
+ while (index < query.length) {
25141
+ if (block && query.startsWith('"""', index) && query[index - 1] !== "\\") {
25142
+ index += 3;
25143
+ closed = true;
25144
+ break;
25145
+ }
25146
+ if (!block && query[index] === '"') {
25147
+ index += 1;
25148
+ closed = true;
25149
+ break;
25150
+ }
25151
+ index += query[index] === "\\" && !block ? 2 : 1;
25152
+ }
25153
+ if (!closed) return true;
25154
+ continue;
25155
+ }
25156
+ const name = query.slice(index).match(/^[_A-Za-z][_0-9A-Za-z]*/)?.[0];
25157
+ if (name) {
25158
+ tokens.push(name);
25159
+ index += name.length;
25160
+ continue;
25161
+ }
25162
+ if ("{}()[]".includes(character)) tokens.push(character);
25163
+ index += 1;
25164
+ }
25165
+ const skipSelection = (start) => {
25166
+ let depth = 0;
25167
+ for (let index = start; index < tokens.length; index += 1) {
25168
+ if (tokens[index] === "{") depth += 1;
25169
+ if (tokens[index] === "}" && --depth === 0) return index + 1;
25170
+ }
25171
+ return null;
25172
+ };
25173
+ for (let index = 0; index < tokens.length; ) {
25174
+ const definition = tokens[index++];
25175
+ if (definition === "mutation") return true;
25176
+ if (definition === "{") {
25177
+ const nextDefinition2 = skipSelection(index - 1);
25178
+ if (nextDefinition2 === null) return true;
25179
+ index = nextDefinition2;
25180
+ continue;
25181
+ }
25182
+ if (!["query", "subscription", "fragment"].includes(definition || "")) {
25183
+ return true;
25184
+ }
25185
+ let parentheses = 0;
25186
+ let brackets = 0;
25187
+ while (index < tokens.length) {
25188
+ const token = tokens[index];
25189
+ if (token === "(") parentheses += 1;
25190
+ else if (token === ")") parentheses -= 1;
25191
+ else if (token === "[") brackets += 1;
25192
+ else if (token === "]") brackets -= 1;
25193
+ else if (token === "{" && parentheses === 0 && brackets === 0) break;
25194
+ index += 1;
25195
+ }
25196
+ if (index === tokens.length) return true;
25197
+ const nextDefinition = skipSelection(index);
25198
+ if (nextDefinition === null) return true;
25199
+ index = nextDefinition;
25200
+ }
25201
+ return false;
25202
+ }
25004
25203
  var Environment = class _Environment {
25005
25204
  granular;
25006
25205
  envData;
25007
25206
  _apiKey;
25008
25207
  _apiEndpoint;
25009
- constructor(granular, envData, apiKey, apiEndpoint) {
25208
+ _tokenProvider;
25209
+ _beforeMutationSend;
25210
+ constructor(granular, envData, apiKey, apiEndpoint, tokenProvider, beforeMutationSend) {
25010
25211
  this.granular = granular;
25011
25212
  this.envData = envData;
25012
25213
  this._apiKey = apiKey;
25013
25214
  this._apiEndpoint = apiEndpoint;
25215
+ this._tokenProvider = tokenProvider;
25216
+ this._beforeMutationSend = beforeMutationSend;
25014
25217
  }
25015
25218
  /** The environment ID */
25016
25219
  get environmentId() {
@@ -25076,6 +25279,15 @@ var Environment = class _Environment {
25076
25279
  get authToken() {
25077
25280
  return this._apiKey;
25078
25281
  }
25282
+ resolveAuthToken() {
25283
+ if (!this._tokenProvider) return this._apiKey;
25284
+ return Promise.resolve(this._tokenProvider()).then((provided) => {
25285
+ if (typeof provided === "string" && provided.trim()) {
25286
+ this._apiKey = provided.trim();
25287
+ }
25288
+ return this._apiKey;
25289
+ });
25290
+ }
25079
25291
  /** Base runtime URL derived from the GraphQL endpoint */
25080
25292
  get runtimeBaseUrl() {
25081
25293
  return this.getRuntimeBaseUrl();
@@ -25467,10 +25679,15 @@ var Environment = class _Environment {
25467
25679
  async controlPlaneRequest(path7, options = {}) {
25468
25680
  const runtimeBase = this.getRuntimeBaseUrl();
25469
25681
  const url = `${runtimeBase}${path7}`;
25682
+ const authToken = await this.resolveAuthToken();
25683
+ const method = (options.method || "GET").toUpperCase();
25684
+ if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
25685
+ this._beforeMutationSend?.();
25686
+ }
25470
25687
  const response = await fetch(url, {
25471
25688
  ...options,
25472
25689
  headers: {
25473
- Authorization: `Bearer ${this._apiKey}`,
25690
+ Authorization: `Bearer ${authToken}`,
25474
25691
  "Content-Type": "application/json",
25475
25692
  ...options.headers
25476
25693
  }
@@ -25535,11 +25752,15 @@ var Environment = class _Environment {
25535
25752
  * ```
25536
25753
  */
25537
25754
  async graphql(query, variables) {
25755
+ const authToken = await this.resolveAuthToken();
25756
+ if (this._beforeMutationSend && isGraphQLMutationDocument(query)) {
25757
+ this._beforeMutationSend?.();
25758
+ }
25538
25759
  const response = await fetch(this._apiEndpoint, {
25539
25760
  method: "POST",
25540
25761
  headers: {
25541
25762
  "Content-Type": "application/json",
25542
- Authorization: `Bearer ${this._apiKey}`
25763
+ Authorization: `Bearer ${authToken}`
25543
25764
  },
25544
25765
  body: JSON.stringify({
25545
25766
  environmentId: this.environmentId,
@@ -26018,8 +26239,10 @@ var Environment = class _Environment {
26018
26239
  eventType.payloadSchema.properties
26019
26240
  )) {
26020
26241
  const schema = propSchema;
26242
+ const format = schema.format === "date" || schema.format === "date-time" ? schema.format : void 0;
26021
26243
  fieldSpecs[propName] = {
26022
26244
  type: schema.type ?? "string",
26245
+ ...format ? { format } : {},
26023
26246
  description: schema.description
26024
26247
  };
26025
26248
  }
@@ -26441,40 +26664,36 @@ var EnvironmentSession = class extends Session {
26441
26664
  environment;
26442
26665
  sessionDataRoutePrefix;
26443
26666
  sessionDataHeaders;
26667
+ beforeMutationSend;
26444
26668
  heartbeatTimer = null;
26445
- heartbeatInFlight = false;
26446
- /** The last known graph container status, updated by checkReadiness() or on heartbeat */
26669
+ /** The last known graph container status, updated by checkReadiness(). */
26447
26670
  graphContainerStatus = null;
26448
26671
  constructor(client, environment, clientId, options = {}) {
26449
26672
  super(client, clientId, { initialQuota: options.initialQuota });
26450
26673
  this.environment = environment;
26451
26674
  this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
26452
26675
  this.sessionDataHeaders = options.sessionDataHeaders || {};
26676
+ this.beforeMutationSend = options.beforeMutationSend;
26453
26677
  this.setFeedListTransport(
26454
26678
  (feedOptions) => this.sessionDataRequest("/feed", feedOptions)
26455
26679
  );
26456
26680
  }
26457
26681
  /**
26458
26682
  * Keep the browser session transport observable from the client side.
26459
- * A worker/proxy restart can leave a browser WebSocket appearing OPEN even
26460
- * though the server-side Durable Object has already closed its peer. The
26461
- * heartbeat gives the SDK a bounded failure signal so it can revoke that
26462
- * stale socket and use WSClient's normal reconnect path.
26683
+ * SessionDO already broadcasts a heartbeat every five seconds. Treat that
26684
+ * and all other inbound frames as proof of life instead of issuing an RPC:
26685
+ * an application RPC may be delayed by Durable Object work even while the
26686
+ * underlying WebSocket is healthy.
26463
26687
  */
26464
26688
  startHeartbeat() {
26465
26689
  if (this.heartbeatTimer) return;
26466
26690
  this.heartbeatTimer = setInterval(() => {
26467
- if (this.heartbeatInFlight) return;
26468
- this.heartbeatInFlight = true;
26469
- void this.client.call("client.heartbeat", {}).then((result) => {
26470
- if (result?.graphContainerStatus) {
26471
- this.graphContainerStatus = result.graphContainerStatus;
26472
- }
26473
- }).catch((error2) => {
26474
- this.client.reportTransportFailure(error2);
26475
- }).finally(() => {
26476
- this.heartbeatInFlight = false;
26477
- });
26691
+ if (!this.client.isTransportOpen()) return;
26692
+ if (this.client.hasRecentInboundActivity(3e4)) return;
26693
+ if (this.client.hasRecentlyPendingRpcPeriod(6e4)) return;
26694
+ this.client.reportTransportFailure(
26695
+ new Error("No server activity received for 30 seconds")
26696
+ );
26478
26697
  }, 5e3);
26479
26698
  this.heartbeatTimer.unref?.();
26480
26699
  }
@@ -26483,7 +26702,6 @@ var EnvironmentSession = class extends Session {
26483
26702
  clearInterval(this.heartbeatTimer);
26484
26703
  this.heartbeatTimer = null;
26485
26704
  }
26486
- this.heartbeatInFlight = false;
26487
26705
  }
26488
26706
  async hello() {
26489
26707
  const result = await super.hello();
@@ -26544,7 +26762,13 @@ var EnvironmentSession = class extends Session {
26544
26762
  for (let attempt = 1; attempt <= SESSION_DATA_REQUEST_RETRY_COUNT; attempt += 1) {
26545
26763
  try {
26546
26764
  const headers = new Headers(init2.headers);
26547
- headers.set("Authorization", `Bearer ${this.environment.authToken}`);
26765
+ const resolvedAuthToken = this.environment.resolveAuthToken();
26766
+ const authToken = typeof resolvedAuthToken === "string" ? resolvedAuthToken : await resolvedAuthToken;
26767
+ headers.set("Authorization", `Bearer ${authToken}`);
26768
+ const method = (init2.method || "GET").toUpperCase();
26769
+ if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
26770
+ this.beforeMutationSend?.();
26771
+ }
26548
26772
  const response = await fetch(url, {
26549
26773
  method: init2.method || "GET",
26550
26774
  headers,
@@ -27003,7 +27227,7 @@ var EnvironmentSession = class extends Session {
27003
27227
  method: "POST",
27004
27228
  headers: {
27005
27229
  "Content-Type": "application/json",
27006
- Authorization: `Bearer ${this.environment.authToken}`
27230
+ Authorization: `Bearer ${await this.environment.resolveAuthToken()}`
27007
27231
  },
27008
27232
  body: JSON.stringify({
27009
27233
  reason: "sdk_disconnect_http_fallback",
@@ -27795,6 +28019,9 @@ var Granular = class _Granular {
27795
28019
  if (typeof options.limit === "number") {
27796
28020
  query.set("limit", String(options.limit));
27797
28021
  }
28022
+ if (options.consistency === "refresh") {
28023
+ query.set("consistency", "refresh");
28024
+ }
27798
28025
  const state = await this.request(
27799
28026
  `/sdk/user-environment-state?${query.toString()}`
27800
28027
  );
@@ -27938,6 +28165,10 @@ var Granular = class _Granular {
27938
28165
  });
27939
28166
  }
27940
28167
  async recordOpenAIUsageSpend(usage, context, options) {
28168
+ const providedToken = await this.tokenProvider?.();
28169
+ if (typeof providedToken === "string" && providedToken.trim()) {
28170
+ this.apiKey = providedToken.trim();
28171
+ }
27941
28172
  return recordOpenAIUsageSpend({
27942
28173
  apiUrl: this.apiUrl,
27943
28174
  token: this.apiKey,
@@ -28110,7 +28341,13 @@ var Granular = class _Granular {
28110
28341
  }
28111
28342
  bindEnvironmentHandle(envData) {
28112
28343
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
28113
- return new Environment(this, envData, this.apiKey, graphqlEndpoint);
28344
+ return new Environment(
28345
+ this,
28346
+ envData,
28347
+ this.apiKey,
28348
+ graphqlEndpoint,
28349
+ this.tokenProvider
28350
+ );
28114
28351
  }
28115
28352
  async bindWebSocketEnvironmentSession(environment, clientId, session2, transportOptions = {}) {
28116
28353
  const client = new WSClient({
@@ -28994,6 +29231,10 @@ var Granular = class _Granular {
28994
29231
  console.log(`[SDK] Requesting: ${url}`);
28995
29232
  }
28996
29233
  for (let attempt = 1; attempt <= LOCAL_CONTROL_REQUEST_RETRY_COUNT; attempt += 1) {
29234
+ const providedToken = await this.tokenProvider?.();
29235
+ if (typeof providedToken === "string" && providedToken.trim()) {
29236
+ this.apiKey = providedToken.trim();
29237
+ }
28997
29238
  const response = await fetch(url, {
28998
29239
  ...options,
28999
29240
  headers: {
@@ -1,4 +1,4 @@
1
- import { de as WSClientOptions, a5 as SessionFeedApi, b9 as GranularQuotaProgress, T as ToolWithHandler, o as PublishToolsResult, cg as Job, p as ToolHandler, I as InstanceToolHandler, cj as UserMessageInput, ck as UserMessageAppendResult, c3 as EffectInfo, l as Prompt, c2 as ToolInfo, c5 as EffectsChangedEvent, c4 as ToolsChangedEvent, D as DomainState, a8 as FeedListTransport, aR as GranularOptions, dM as EnvironmentImporter, aU as RecordUserOptions, aT as User, aW as OpenEnvironmentOptions, bm as EnvironmentData, bj as BuildPolicy, dJ as EnvironmentSetupSummary, b0 as ConversationSessionListOptions, a_ as ConversationSessionInfo, b1 as ConversationSessionListResponse, aZ as CreateSessionOptions, f as RecordObjectOptions, dv as RecordObjectResult, dx as RecordObjectsOptions, dE as RecordImport, dA as RecordImportStatus, dD as RecordImportItem, dF as EnvironmentRecordImportSummary, ce as EnvironmentFeedbackRecord, k as SessionHeapSnapshot, d2 as SessionDocumentResult, cl as AssistantReplyPublicationInput, cm as AssistantReplyPublicationResult, d3 as SessionCollectionListOptions, d5 as SessionCollectionListResult, cp as SessionTimelineEvent, d4 as SessionJobListOptions, cX as SessionJobRecord, bO as SessionMutationView, cy as SessionArtifactListOptions, cx as SessionArtifactRecord, cA as SessionArtifactRelationshipOptionsInput, cC as SessionArtifactRelationshipOptionsResult, cD as SessionArtifactRelationshipCreateInput, cz as SessionArtifactValidationResult, cF as SessionArtifactExecutionOptions, cE as SessionArtifactExecutionResult, cG as SessionArtifactApprovalOptions, cQ as RecordManualActionInput, cS as ManualActionRecordResult, cT as ManualActionListOptions, cR as ManualActionOccurrence, cV as ManualActionSuggestionOptions, cU as ManualActionSuggestion, cJ as ArtifactApprovalTaskListOptions, cI as ArtifactApprovalTask, cK as ArtifactApprovalDecisionInput, cL as ArtifactApprovalDecisionResult, ct as SessionFileRecord, cW as SessionFileUploadOptions, S as SessionHeapEntry, i as SessionHeapList, c_ as SessionHeapVariable, m as SessionTranscriptEntry, ee as GraphQLResult, d0 as RecordSearchOptions, c$ as RecordSearchResult, d1 as RecordMentionInput, dn as DefineRelationshipOptions, dm as RelationshipInfo, dl as ModelRef, g as ManifestContent, ds as EnvironmentStateUpdateInput, dq as EnvironmentStateTarget, du as EnvironmentStateProxy, dz as RecordImportOptions, da as UserEnvironmentStateOptions, d9 as UserEnvironmentState, db as MarkUserEnvironmentReadOptions, b as ProjectionMapper, bH as EffectProjectionResult, bM as ExternalCommitOptions, C as CommitReceipt, bI as TransitionProjectionResult, bN as ObservationReceipt, aX as AdoptEnvironmentOptions, aY as ConnectOptions, dH as RunEnvironmentImporterOptions, aD as OpenAIUsageSpendEvent, aC as GranularSpendContext, aF as RecordOpenAIUsageSpendResult, bc as SandboxListResponse, ba as Sandbox, bb as CreateSandboxData, eg as DeleteResponse, be as PermissionProfile, bf as CreatePermissionProfileData, bn as CreateEnvironmentData, eh as StreamEvent, ei as StreamSubscription, ej as StreamStats, aV as Subject, bi as AssignmentListResponse } from './spend-CESUvrZC.mjs';
1
+ import { de as WSClientOptions, a5 as SessionFeedApi, b9 as GranularQuotaProgress, T as ToolWithHandler, o as PublishToolsResult, cg as Job, p as ToolHandler, I as InstanceToolHandler, cj as UserMessageInput, ck as UserMessageAppendResult, c3 as EffectInfo, l as Prompt, c2 as ToolInfo, c5 as EffectsChangedEvent, c4 as ToolsChangedEvent, D as DomainState, a8 as FeedListTransport, aR as GranularOptions, dM as EnvironmentImporter, aU as RecordUserOptions, aT as User, aW as OpenEnvironmentOptions, bm as EnvironmentData, aQ as AccessTokenProvider, bj as BuildPolicy, dJ as EnvironmentSetupSummary, b0 as ConversationSessionListOptions, a_ as ConversationSessionInfo, b1 as ConversationSessionListResponse, aZ as CreateSessionOptions, f as RecordObjectOptions, dv as RecordObjectResult, dx as RecordObjectsOptions, dE as RecordImport, dA as RecordImportStatus, dD as RecordImportItem, dF as EnvironmentRecordImportSummary, ce as EnvironmentFeedbackRecord, k as SessionHeapSnapshot, d2 as SessionDocumentResult, cl as AssistantReplyPublicationInput, cm as AssistantReplyPublicationResult, d3 as SessionCollectionListOptions, d5 as SessionCollectionListResult, cp as SessionTimelineEvent, d4 as SessionJobListOptions, cX as SessionJobRecord, bO as SessionMutationView, cy as SessionArtifactListOptions, cx as SessionArtifactRecord, cA as SessionArtifactRelationshipOptionsInput, cC as SessionArtifactRelationshipOptionsResult, cD as SessionArtifactRelationshipCreateInput, cz as SessionArtifactValidationResult, cF as SessionArtifactExecutionOptions, cE as SessionArtifactExecutionResult, cG as SessionArtifactApprovalOptions, cQ as RecordManualActionInput, cS as ManualActionRecordResult, cT as ManualActionListOptions, cR as ManualActionOccurrence, cV as ManualActionSuggestionOptions, cU as ManualActionSuggestion, cJ as ArtifactApprovalTaskListOptions, cI as ArtifactApprovalTask, cK as ArtifactApprovalDecisionInput, cL as ArtifactApprovalDecisionResult, ct as SessionFileRecord, cW as SessionFileUploadOptions, S as SessionHeapEntry, i as SessionHeapList, c_ as SessionHeapVariable, m as SessionTranscriptEntry, ee as GraphQLResult, d0 as RecordSearchOptions, c$ as RecordSearchResult, d1 as RecordMentionInput, dn as DefineRelationshipOptions, dm as RelationshipInfo, dl as ModelRef, g as ManifestContent, ds as EnvironmentStateUpdateInput, dq as EnvironmentStateTarget, du as EnvironmentStateProxy, dz as RecordImportOptions, da as UserEnvironmentStateOptions, d9 as UserEnvironmentState, db as MarkUserEnvironmentReadOptions, b as ProjectionMapper, bH as EffectProjectionResult, bM as ExternalCommitOptions, C as CommitReceipt, bI as TransitionProjectionResult, bN as ObservationReceipt, aX as AdoptEnvironmentOptions, aY as ConnectOptions, dH as RunEnvironmentImporterOptions, aD as OpenAIUsageSpendEvent, aC as GranularSpendContext, aF as RecordOpenAIUsageSpendResult, bc as SandboxListResponse, ba as Sandbox, bb as CreateSandboxData, eg as DeleteResponse, be as PermissionProfile, bf as CreatePermissionProfileData, bn as CreateEnvironmentData, eh as StreamEvent, ei as StreamSubscription, ej as StreamStats, aV as Subject, bi as AssignmentListResponse } from './spend-D4r0SA-H.js';
2
2
  import * as Automerge from '@automerge/automerge';
3
3
  import { Doc } from '@automerge/automerge/slim';
4
4
 
@@ -13,6 +13,7 @@ declare class WSClient {
13
13
  private sessionId;
14
14
  private token;
15
15
  private messageQueue;
16
+ private pendingRpcPeriodStartedAt;
16
17
  private rpcHandlers;
17
18
  private eventHandlers;
18
19
  private canonicalDocumentQuarantineHandlers;
@@ -27,9 +28,20 @@ declare class WSClient {
27
28
  private cancelConnectAttempt;
28
29
  private documentRepairRequired;
29
30
  private canonicalDocumentActivated;
31
+ private lastInboundActivityAt;
30
32
  private options;
31
33
  constructor(options: WSClientOptions);
32
34
  get currentSessionId(): string;
35
+ isTransportOpen(): boolean;
36
+ hasPendingRpcRequests(): boolean;
37
+ hasRecentlyPendingRpcPeriod(maxAgeMs: number, now?: number): boolean;
38
+ private removePendingRpc;
39
+ /**
40
+ * Whether the current socket has delivered any frame recently. SessionDO
41
+ * broadcasts a one-way heartbeat every five seconds, so this measures the
42
+ * transport without adding an RPC that can be delayed by application work.
43
+ */
44
+ hasRecentInboundActivity(maxAgeMs: number, now?: number): boolean;
33
45
  seedDocumentSnapshot(document: Record<string, unknown> | Uint8Array | null | undefined): void;
34
46
  saveDocumentSnapshot(): Uint8Array;
35
47
  private clearTokenRefreshTimer;
@@ -310,6 +322,7 @@ declare class Session {
310
322
  type SessionFileUploadBody = Blob | ArrayBuffer | Uint8Array | string;
311
323
  type SessionDataHeaders = any;
312
324
  type EnvironmentImporterHandler = (importer: EnvironmentImporter) => Promise<void> | void;
325
+ declare function isGraphQLMutationDocument(query: unknown): boolean;
313
326
  /**
314
327
  * Environment is the sessionless handle for one resolved ontology environment.
315
328
  *
@@ -322,7 +335,9 @@ declare class Environment {
322
335
  private envData;
323
336
  private _apiKey;
324
337
  private _apiEndpoint;
325
- constructor(granular: Granular, envData: EnvironmentData, apiKey: string, apiEndpoint: string);
338
+ private _tokenProvider?;
339
+ private _beforeMutationSend?;
340
+ constructor(granular: Granular, envData: EnvironmentData, apiKey: string, apiEndpoint: string, tokenProvider?: AccessTokenProvider, beforeMutationSend?: () => void);
326
341
  /** The environment ID */
327
342
  get environmentId(): string;
328
343
  /** The sandbox ID */
@@ -355,6 +370,7 @@ declare class Environment {
355
370
  get apiEndpoint(): string;
356
371
  /** Internal auth token used for control-plane and runtime fallback requests */
357
372
  get authToken(): string;
373
+ resolveAuthToken(): string | Promise<string>;
358
374
  /** Base runtime URL derived from the GraphQL endpoint */
359
375
  get runtimeBaseUrl(): string;
360
376
  syncEnvironmentData(envData: EnvironmentData): void;
@@ -776,9 +792,9 @@ declare class EnvironmentSession extends Session {
776
792
  readonly environment: Environment;
777
793
  private readonly sessionDataRoutePrefix;
778
794
  private readonly sessionDataHeaders;
795
+ private readonly beforeMutationSend?;
779
796
  private heartbeatTimer;
780
- private heartbeatInFlight;
781
- /** The last known graph container status, updated by checkReadiness() or on heartbeat */
797
+ /** The last known graph container status, updated by checkReadiness(). */
782
798
  graphContainerStatus: {
783
799
  lastKeepAliveAt: number;
784
800
  status: "warming" | "hot" | "unknown";
@@ -791,14 +807,16 @@ declare class EnvironmentSession extends Session {
791
807
  * that initiated a user action when multiple tabs are connected.
792
808
  */
793
809
  sessionDataHeaders?: SessionDataHeaders;
810
+ /** Browser-only readiness guard checked immediately before each write. */
811
+ beforeMutationSend?: () => void;
794
812
  initialQuota?: GranularQuotaProgress | null;
795
813
  });
796
814
  /**
797
815
  * Keep the browser session transport observable from the client side.
798
- * A worker/proxy restart can leave a browser WebSocket appearing OPEN even
799
- * though the server-side Durable Object has already closed its peer. The
800
- * heartbeat gives the SDK a bounded failure signal so it can revoke that
801
- * stale socket and use WSClient's normal reconnect path.
816
+ * SessionDO already broadcasts a heartbeat every five seconds. Treat that
817
+ * and all other inbound frames as proof of life instead of issuing an RPC:
818
+ * an application RPC may be delayed by Durable Object work even while the
819
+ * underlying WebSocket is healthy.
802
820
  */
803
821
  private startHeartbeat;
804
822
  private stopHeartbeat;
@@ -1336,4 +1354,4 @@ declare class Granular {
1336
1354
  private request;
1337
1355
  }
1338
1356
 
1339
- export { Environment as E, Granular as G, OntologyHandle as O, Session as S, WSClient as W, EnvironmentSession as a };
1357
+ export { Environment as E, Granular as G, OntologyHandle as O, Session as S, WSClient as W, EnvironmentSession as a, isGraphQLMutationDocument as i };