@granular-software/sdk 0.4.16 → 0.4.18

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
@@ -3928,6 +3928,12 @@ var READY_STATE_OPEN = 1;
3928
3928
  var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
3929
3929
  var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
3930
3930
  var MAX_TIMER_DELAY_MS = 2147483647;
3931
+ var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
3932
+ function debugWs(...args) {
3933
+ if (DEBUG_WS) {
3934
+ console.log(...args);
3935
+ }
3936
+ }
3931
3937
  var WSClient = class {
3932
3938
  ws = null;
3933
3939
  url;
@@ -4227,7 +4233,7 @@ var WSClient = class {
4227
4233
  }
4228
4234
  handleMessage(message) {
4229
4235
  if (typeof message !== "object" || message === null) return;
4230
- console.log("[Granular DEBUG] Received message:", JSON.stringify(message).slice(0, 500));
4236
+ debugWs("[Granular DEBUG] Received message:", JSON.stringify(message).slice(0, 500));
4231
4237
  if ("type" in message && message.type === "sync") {
4232
4238
  const syncMessage = message;
4233
4239
  let bytes;
@@ -4247,7 +4253,7 @@ var WSClient = class {
4247
4253
  } else {
4248
4254
  return;
4249
4255
  }
4250
- console.log("[Granular DEBUG] Applying sync bytes:", bytes.length);
4256
+ debugWs("[Granular DEBUG] Applying sync bytes:", bytes.length);
4251
4257
  const [newDoc, newSyncState] = Automerge.receiveSyncMessage(
4252
4258
  this.doc,
4253
4259
  this.syncState,
@@ -4257,19 +4263,19 @@ var WSClient = class {
4257
4263
  this.syncState = newSyncState;
4258
4264
  const docAny = this.doc;
4259
4265
  if (docAny.catalog) {
4260
- console.log("[Granular DEBUG] Doc catalog sync applied. Keys in catalog:", Object.keys(docAny.catalog || {}));
4261
- console.log("[Granular DEBUG] RawToolCatalogs:", Object.keys(docAny.catalog.rawToolCatalogs || {}));
4266
+ debugWs("[Granular DEBUG] Doc catalog sync applied. Keys in catalog:", Object.keys(docAny.catalog || {}));
4267
+ debugWs("[Granular DEBUG] RawToolCatalogs:", Object.keys(docAny.catalog.rawToolCatalogs || {}));
4262
4268
  } else {
4263
- console.log("[Granular DEBUG] Doc synced but no catalog yet. Keys in doc:", Object.keys(docAny));
4269
+ debugWs("[Granular DEBUG] Doc synced but no catalog yet. Keys in doc:", Object.keys(docAny));
4264
4270
  }
4265
4271
  this.emit("sync", this.doc);
4266
4272
  } catch (e) {
4267
4273
  try {
4268
- console.log("[Granular DEBUG] receiveSyncMessage failed, trying applyChanges...");
4274
+ debugWs("[Granular DEBUG] receiveSyncMessage failed, trying applyChanges...");
4269
4275
  const [newDoc] = Automerge.applyChanges(this.doc, [bytes]);
4270
4276
  this.doc = newDoc;
4271
4277
  this.emit("sync", this.doc);
4272
- console.log("[Granular DEBUG] applyChanges succeeded. Doc:", JSON.stringify(Automerge.toJS(this.doc)));
4278
+ debugWs("[Granular DEBUG] applyChanges succeeded. Doc:", JSON.stringify(Automerge.toJS(this.doc)));
4273
4279
  } catch (applyError) {
4274
4280
  console.warn("[Granular] Failed to apply sync message (both sync & applyChanges)", e, applyError);
4275
4281
  }
@@ -4280,10 +4286,10 @@ var WSClient = class {
4280
4286
  const snapshotMessage = message;
4281
4287
  try {
4282
4288
  const bytes = new Uint8Array(snapshotMessage.data);
4283
- console.log("[Granular DEBUG] Loading Automerge session snapshot bytes:", bytes.length);
4289
+ debugWs("[Granular DEBUG] Loading Automerge session snapshot bytes:", bytes.length);
4284
4290
  this.doc = Automerge.load(bytes);
4285
4291
  this.emit("sync", this.doc);
4286
- console.log("[Granular DEBUG] Automerge session snapshot loaded. Doc:", JSON.stringify(Automerge.toJS(this.doc)));
4292
+ debugWs("[Granular DEBUG] Automerge session snapshot loaded. Doc:", JSON.stringify(Automerge.toJS(this.doc)));
4287
4293
  } catch (e) {
4288
4294
  console.warn("[Granular] Failed to load snapshot message", e);
4289
4295
  }
@@ -11321,6 +11327,7 @@ var Environment = class extends Session {
11321
11327
  headers: {
11322
11328
  "Authorization": `Bearer ${this._apiKey}`,
11323
11329
  "Content-Type": "application/json",
11330
+ "Connection": "close",
11324
11331
  ...options.headers
11325
11332
  }
11326
11333
  });
@@ -11355,7 +11362,8 @@ var Environment = class extends Session {
11355
11362
  method: "POST",
11356
11363
  headers: {
11357
11364
  "Content-Type": "application/json",
11358
- "Authorization": `Bearer ${this._apiKey}`
11365
+ "Authorization": `Bearer ${this._apiKey}`,
11366
+ "Connection": "close"
11359
11367
  },
11360
11368
  body: JSON.stringify({
11361
11369
  reason: "sdk_disconnect_http_fallback",
@@ -11815,6 +11823,70 @@ var Environment = class extends Session {
11815
11823
  await this._runGraphql(mutation.query, mutation.label);
11816
11824
  }
11817
11825
  }
11826
+ async _ensureWorkspaceStreamsRoot() {
11827
+ await this._runGraphql(
11828
+ `mutation { create_model(path: "workspace", label: "workspace") { model { path } } }`,
11829
+ "ensure workspace"
11830
+ ).catch((error) => {
11831
+ if (!error.message.includes("already exists")) throw error;
11832
+ });
11833
+ await this._runGraphql(
11834
+ `mutation { at(path: "workspace") { create_submodel(subpath: "streams", label: "Streams") { model { path } } } }`,
11835
+ "ensure workspace:streams"
11836
+ ).catch((error) => {
11837
+ if (!error.message.includes("already exists")) throw error;
11838
+ });
11839
+ }
11840
+ async _applyEventStreamDeclaration(stream) {
11841
+ await this._ensureWorkspaceStreamsRoot();
11842
+ const streamPath = `workspace:streams:${stream.name}`;
11843
+ await this._runGraphql(
11844
+ `mutation { at(path: "workspace:streams") { create_submodel(subpath: ${JSON.stringify(stream.name)}, label: ${JSON.stringify(stream.name)}) { model { path } } } }`,
11845
+ `create stream ${stream.name}`
11846
+ ).catch((error) => {
11847
+ if (!error.message.includes("already exists")) throw error;
11848
+ });
11849
+ if (stream.description) {
11850
+ await this._runGraphql(
11851
+ `mutation { at(path: ${JSON.stringify(streamPath)}) { set_description(description: ${JSON.stringify(stream.description)}) { done } } }`,
11852
+ `set stream description on ${streamPath}`
11853
+ );
11854
+ }
11855
+ for (const eventType of stream.eventTypes) {
11856
+ const typePath = `${streamPath}:${eventType.name}`;
11857
+ await this._runGraphql(
11858
+ `mutation { at(path: ${JSON.stringify(streamPath)}) { create_submodel(subpath: ${JSON.stringify(eventType.name)}, label: ${JSON.stringify(eventType.name)}) { model { path } } } }`,
11859
+ `create event type ${eventType.name} on ${streamPath}`
11860
+ ).catch((error) => {
11861
+ if (!error.message.includes("already exists")) throw error;
11862
+ });
11863
+ if (eventType.description) {
11864
+ await this._runGraphql(
11865
+ `mutation { at(path: ${JSON.stringify(typePath)}) { set_description(description: ${JSON.stringify(eventType.description)}) { done } } }`,
11866
+ `set event type description on ${typePath}`
11867
+ );
11868
+ }
11869
+ if (eventType.payloadSchema?.properties) {
11870
+ const fieldSpecs = {};
11871
+ for (const [propName, propSchema] of Object.entries(eventType.payloadSchema.properties)) {
11872
+ const schema = propSchema;
11873
+ fieldSpecs[propName] = {
11874
+ type: schema.type ?? "string",
11875
+ description: schema.description
11876
+ };
11877
+ }
11878
+ await this._applyFields(typePath, fieldSpecs);
11879
+ }
11880
+ if (eventType.payloadSchema?.required?.length) {
11881
+ await this._runGraphql(
11882
+ `mutation { at(path: ${JSON.stringify(typePath)}) { create_submodel(subpath: "required", label: "required") { set_string_value(value: ${JSON.stringify(JSON.stringify(eventType.payloadSchema.required))}) { done } } } }`,
11883
+ `store required fields on ${typePath}`
11884
+ ).catch((error) => {
11885
+ if (!error.message.includes("already exists")) throw error;
11886
+ });
11887
+ }
11888
+ }
11889
+ }
11818
11890
  async _applyEffectDeclaration(effect, aliasMap) {
11819
11891
  let containerPath = "workspace:tools:declared";
11820
11892
  if (effect.attachedClass) {
@@ -11902,6 +11974,9 @@ var Environment = class extends Session {
11902
11974
  if (op.withEffect) {
11903
11975
  await this._applyEffectDeclaration(op.withEffect, aliasMap);
11904
11976
  }
11977
+ if (op.defineEventStream) {
11978
+ await this._applyEventStreamDeclaration(op.defineEventStream);
11979
+ }
11905
11980
  }
11906
11981
  /**
11907
11982
  * Apply field definitions (has) to a model via GraphQL
@@ -12151,6 +12226,7 @@ var Granular = class _Granular {
12151
12226
  WebSocketCtor;
12152
12227
  onUnexpectedClose;
12153
12228
  onReconnectError;
12229
+ debugHttp = process.env.GRANULAR_DEBUG_HTTP === "1";
12154
12230
  /** Sandbox-level effect registry: sandboxId → (effectKey → ToolWithHandler) */
12155
12231
  sandboxEffects = /* @__PURE__ */ new Map();
12156
12232
  /** Live sandbox-scoped effect hosts keyed by sandboxId */
@@ -12874,6 +12950,107 @@ var Granular = class _Granular {
12874
12950
  }
12875
12951
  };
12876
12952
  }
12953
+ /**
12954
+ * Event stream operations: query, subscribe, and acknowledge stream events
12955
+ */
12956
+ get streams() {
12957
+ return {
12958
+ getEvents: async (params) => {
12959
+ const sandbox = await this._resolveSandboxId(params.ontology);
12960
+ const query = new URLSearchParams({ sandboxId: sandbox });
12961
+ if (params.environment) query.set("environmentId", params.environment);
12962
+ if (params.session) query.set("sessionId", params.session);
12963
+ if (params.stream) query.set("streamName", params.stream);
12964
+ if (params.eventTypes && params.eventTypes.length > 0) {
12965
+ query.set("eventTypes", params.eventTypes.join(","));
12966
+ }
12967
+ if (params.since) query.set("since", params.since.toISOString());
12968
+ if (params.until) query.set("until", params.until.toISOString());
12969
+ if (params.isAcked !== void 0) query.set("isAcked", params.isAcked ? "1" : "0");
12970
+ if (params.limit) query.set("limit", String(params.limit));
12971
+ if (params.offset) query.set("offset", String(params.offset));
12972
+ const result = await this.request(`/control/stream-events?${query.toString()}`);
12973
+ return (result.items || []).map((row) => ({
12974
+ eventId: row.event_id,
12975
+ streamName: row.stream_name,
12976
+ eventType: row.event_type,
12977
+ payload: typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload,
12978
+ environmentId: row.environment_id,
12979
+ sessionId: row.session_id,
12980
+ subjectId: row.subject_id,
12981
+ source: row.source,
12982
+ isAcked: Boolean(row.is_acked),
12983
+ createdAt: row.created_at
12984
+ }));
12985
+ },
12986
+ subscribe: (params) => {
12987
+ const interval = params.pollIntervalMs ?? 5e3;
12988
+ let cursor = params.since || /* @__PURE__ */ new Date();
12989
+ let running = true;
12990
+ const seenEventIds = /* @__PURE__ */ new Set();
12991
+ const poll = async () => {
12992
+ while (running) {
12993
+ try {
12994
+ const events = await this.streams.getEvents({
12995
+ ontology: params.ontology,
12996
+ stream: params.stream,
12997
+ environment: params.environment,
12998
+ session: params.session,
12999
+ eventTypes: params.eventTypes,
13000
+ since: cursor,
13001
+ limit: 100
13002
+ });
13003
+ const orderedEvents = [...events].sort((a, b) => a.createdAt - b.createdAt);
13004
+ for (const event of orderedEvents) {
13005
+ if (seenEventIds.has(event.eventId)) {
13006
+ continue;
13007
+ }
13008
+ seenEventIds.add(event.eventId);
13009
+ const eventTime = new Date(event.createdAt * 1e3);
13010
+ if (eventTime > cursor) {
13011
+ cursor = eventTime;
13012
+ }
13013
+ params.onEvent(event);
13014
+ }
13015
+ } catch (err) {
13016
+ params.onError?.(err instanceof Error ? err : new Error(String(err)));
13017
+ }
13018
+ await new Promise((resolve) => setTimeout(resolve, interval));
13019
+ }
13020
+ };
13021
+ poll();
13022
+ return { unsubscribe: () => {
13023
+ running = false;
13024
+ } };
13025
+ },
13026
+ ack: async (eventId) => {
13027
+ await this.request("/control/stream-events/ack", {
13028
+ method: "POST",
13029
+ body: JSON.stringify({ eventIds: [eventId] })
13030
+ });
13031
+ },
13032
+ ackBatch: async (eventIds) => {
13033
+ await this.request("/control/stream-events/ack", {
13034
+ method: "POST",
13035
+ body: JSON.stringify({ eventIds })
13036
+ });
13037
+ },
13038
+ getStats: async (params) => {
13039
+ const sandbox = await this._resolveSandboxId(params.ontology);
13040
+ const query = new URLSearchParams({ sandboxId: sandbox });
13041
+ if (params.environment) query.set("environmentId", params.environment);
13042
+ const result = await this.request(`/control/stream-events/stats?${query.toString()}`);
13043
+ return (result.items || []).map((row) => ({
13044
+ streamName: row.stream_name,
13045
+ eventType: row.event_type,
13046
+ total: Number(row.total),
13047
+ last1h: Number(row.last_1h),
13048
+ last24h: Number(row.last_24h),
13049
+ unacked: Number(row.unacked)
13050
+ }));
13051
+ }
13052
+ };
13053
+ }
12877
13054
  /**
12878
13055
  * Subject management
12879
13056
  */
@@ -12907,17 +13084,26 @@ var Granular = class _Granular {
12907
13084
  }
12908
13085
  };
12909
13086
  }
13087
+ async _resolveSandboxId(ontologyNameOrId) {
13088
+ if (ontologyNameOrId.startsWith("sbx_")) return ontologyNameOrId;
13089
+ const result = await this.request(`/control/sandboxes?name=${encodeURIComponent(ontologyNameOrId)}`);
13090
+ if (result.items.length === 0) throw new Error(`Ontology not found: ${ontologyNameOrId}`);
13091
+ return result.items[0].sandboxId;
13092
+ }
12910
13093
  /**
12911
13094
  * Make an authenticated API request
12912
13095
  */
12913
13096
  async request(path, options = {}) {
12914
13097
  const url = `${this.httpUrl}${path}`;
12915
- console.log(`[SDK] Requesting: ${url}`);
13098
+ if (this.debugHttp) {
13099
+ console.log(`[SDK] Requesting: ${url}`);
13100
+ }
12916
13101
  const response = await fetch(url, {
12917
13102
  ...options,
12918
13103
  headers: {
12919
13104
  "Authorization": `Bearer ${this.apiKey}`,
12920
13105
  "Content-Type": "application/json",
13106
+ "Connection": "close",
12921
13107
  ...options.headers
12922
13108
  }
12923
13109
  });