@ductape/sdk 0.2.5 → 0.2.7

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.js CHANGED
@@ -530,6 +530,8 @@ class Ductape {
530
530
  this.runtimePollIntervalMs = 30000;
531
531
  this.runtimePollJitter = 0.2;
532
532
  this.runtimeMaxBackoffMs = 300000;
533
+ this.runtimeWarmupEnabled = true;
534
+ this.runtimeCredentialState = { status: 'idle', referenced: 0, cached: 0, missing: 0 };
533
535
  this.runtimeClosed = false;
534
536
  /** Register and inspect portable application functions used by Feature JSON. */
535
537
  this.functions = {
@@ -5453,6 +5455,7 @@ class Ductape {
5453
5455
  this.runtimePollIntervalMs = Math.max(5000, (_a = runtime_sync.interval_ms) !== null && _a !== void 0 ? _a : 30000);
5454
5456
  this.runtimePollJitter = Math.max(0, Math.min(1, (_b = runtime_sync.jitter) !== null && _b !== void 0 ? _b : 0.2));
5455
5457
  this.runtimeMaxBackoffMs = Math.max(this.runtimePollIntervalMs, (_c = runtime_sync.max_backoff_ms) !== null && _c !== void 0 ? _c : 300000);
5458
+ this.runtimeWarmupEnabled = runtime_sync.warm_connections !== false;
5456
5459
  }
5457
5460
  // Services are lazily initialized via factory methods (createNewDatabaseService, etc.)
5458
5461
  // This ensures initUserAuth() is called before service creation
@@ -5796,21 +5799,31 @@ class Ductape {
5796
5799
  };
5797
5800
  }
5798
5801
  applyRuntimeSnapshot(response) {
5799
- var _a, _b, _c, _d, _e, _f, _g;
5802
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
5800
5803
  if (!(response === null || response === void 0 ? void 0 : response.snapshot))
5801
5804
  return;
5805
+ const previousManifest = this.runtimeManifest;
5802
5806
  const scope = `${this.workspace_id}:${response.snapshot.product_tag}:${response.snapshot.env_slug}`;
5803
5807
  (0, bootstrap_cache_1.primeRuntimeBootstrapCache)(this.workspace_id, scope, response.snapshot.entries);
5808
+ this.runtimeManifest = response.manifest;
5809
+ this.runtimeCredentialBundle = (_a = response.snapshot.credentials) !== null && _a !== void 0 ? _a : { referenced_keys: [], encrypted_secrets: [] };
5810
+ const referenced = (_c = (_b = this.runtimeCredentialBundle.referenced_keys) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : this.runtimeCredentialBundle.encrypted_secrets.length;
5811
+ const cached = this.runtimeCredentialBundle.encrypted_secrets.length;
5812
+ this.runtimeCredentialState = {
5813
+ status: 'loading', referenced, cached, missing: Math.max(0, referenced - cached),
5814
+ };
5815
+ if (this._secretsService)
5816
+ this.applyRuntimeCredentialBundle(this._secretsService);
5804
5817
  const runtimeCredentials = new Map();
5805
5818
  for (const entry of response.snapshot.entries) {
5806
5819
  if ((entry === null || entry === void 0 ? void 0 : entry.domain) !== 'action')
5807
5820
  continue;
5808
- const app = String(((_a = entry.parts) === null || _a === void 0 ? void 0 : _a[1]) || '');
5809
- const mapping = (_b = entry.data) === null || _b === void 0 ? void 0 : _b.product_env_mapping;
5821
+ const app = String(((_d = entry.parts) === null || _d === void 0 ? void 0 : _d[1]) || '');
5822
+ const mapping = (_e = entry.data) === null || _e === void 0 ? void 0 : _e.product_env_mapping;
5810
5823
  if (!app || !mapping)
5811
5824
  continue;
5812
- const credentials = (_c = runtimeCredentials.get(app)) !== null && _c !== void 0 ? _c : {};
5813
- for (const variable of (_d = mapping.variables) !== null && _d !== void 0 ? _d : []) {
5825
+ const credentials = (_f = runtimeCredentials.get(app)) !== null && _f !== void 0 ? _f : {};
5826
+ for (const variable of (_g = mapping.variables) !== null && _g !== void 0 ? _g : []) {
5814
5827
  if ((variable === null || variable === void 0 ? void 0 : variable.key) && variable.value !== undefined)
5815
5828
  credentials[String(variable.key)] = variable.value;
5816
5829
  }
@@ -5819,13 +5832,13 @@ class Ductape {
5819
5832
  }
5820
5833
  // token_access data is the reusable request credential itself. credential_access
5821
5834
  // data contains login secrets and must only be sent to its dedicated auth action.
5822
- if (((_f = (_e = entry.data) === null || _e === void 0 ? void 0 : _e.app_auth) === null || _f === void 0 ? void 0 : _f.setup_type) === 'token_access' && ((_g = mapping.auth) === null || _g === void 0 ? void 0 : _g.data)) {
5835
+ if (((_j = (_h = entry.data) === null || _h === void 0 ? void 0 : _h.app_auth) === null || _j === void 0 ? void 0 : _j.setup_type) === 'token_access' && ((_k = mapping.auth) === null || _k === void 0 ? void 0 : _k.data)) {
5823
5836
  let authData = mapping.auth.data;
5824
5837
  if (typeof authData === 'string') {
5825
5838
  try {
5826
5839
  authData = JSON.parse((0, processor_utils_1.decrypt)(authData, String(entry.data.private_key)));
5827
5840
  }
5828
- catch (_h) {
5841
+ catch (_l) {
5829
5842
  authData = undefined;
5830
5843
  }
5831
5844
  }
@@ -5843,9 +5856,51 @@ class Ductape {
5843
5856
  runtimeCredentials.set(app, credentials);
5844
5857
  }
5845
5858
  credential_manager_1.credentialManager.replaceRuntimeScope(response.snapshot.product_tag, response.snapshot.env_slug, [...runtimeCredentials].map(([app, credentials]) => ({ app, credentials })));
5846
- this.runtimeManifest = response.manifest;
5847
5859
  this.runtimeLastRefreshAt = Date.now();
5848
5860
  this.runtimeLastError = undefined;
5861
+ const databaseTags = [...new Set(response.snapshot.entries
5862
+ .filter((entry) => (entry === null || entry === void 0 ? void 0 : entry.domain) === 'database')
5863
+ .map((entry) => { var _a, _b, _c; return String(((_a = entry === null || entry === void 0 ? void 0 : entry.parts) === null || _a === void 0 ? void 0 : _a[2]) || ((_c = (_b = entry === null || entry === void 0 ? void 0 : entry.data) === null || _b === void 0 ? void 0 : _b.database) === null || _c === void 0 ? void 0 : _c.tag) || '').trim(); })
5864
+ .filter(Boolean))];
5865
+ this.startRuntimeWarmup(databaseTags, Boolean(previousManifest && previousManifest.connectionRevision !== response.manifest.connectionRevision));
5866
+ }
5867
+ startRuntimeWarmup(databaseTags, recycleConnections) {
5868
+ var _a, _b;
5869
+ if (!this.runtimeWarmupEnabled || this.runtimeClosed || !this.defaultProduct || !this.defaultEnv || !databaseTags.length)
5870
+ return;
5871
+ const previous = (_b = (_a = this.runtimeWarmupInFlight) === null || _a === void 0 ? void 0 : _a.catch(() => undefined)) !== null && _b !== void 0 ? _b : Promise.resolve();
5872
+ const warmup = previous.then(async () => {
5873
+ const startedAt = Date.now();
5874
+ this.runtimeWarmupState = { startedAt, databases: [] };
5875
+ const service = await this.createNewDatabaseService();
5876
+ if (recycleConnections)
5877
+ await service.closeAll();
5878
+ const databases = await Promise.all(databaseTags.map(async (tag) => {
5879
+ const start = Date.now();
5880
+ try {
5881
+ await service.connect({ product: this.defaultProduct, env: this.defaultEnv, database: tag });
5882
+ return { tag, status: 'ready', durationMs: Date.now() - start };
5883
+ }
5884
+ catch (error) {
5885
+ return {
5886
+ tag,
5887
+ status: 'failed',
5888
+ durationMs: Date.now() - start,
5889
+ error: error instanceof Error ? error.message : String(error),
5890
+ };
5891
+ }
5892
+ }));
5893
+ const completedAt = Date.now();
5894
+ this.runtimeWarmupState = { startedAt, completedAt, durationMs: completedAt - startedAt, databases };
5895
+ });
5896
+ this.runtimeWarmupInFlight = warmup;
5897
+ warmup.finally(() => {
5898
+ if (this.runtimeWarmupInFlight === warmup)
5899
+ this.runtimeWarmupInFlight = undefined;
5900
+ }).catch(() => undefined);
5901
+ warmup.catch((error) => {
5902
+ this.runtimeLastError = `Runtime connection warm-up failed: ${error instanceof Error ? error.message : String(error)}`;
5903
+ });
5849
5904
  }
5850
5905
  scheduleRuntimePoll(failureCount = 0) {
5851
5906
  var _a, _b;
@@ -5893,8 +5948,11 @@ class Ductape {
5893
5948
  this.runtimeRefreshInFlight = (async () => {
5894
5949
  var _a;
5895
5950
  const response = await this.productsApi.bootstrapRuntime(Object.assign({ product_tag: this.defaultProduct, env_slug: this.defaultEnv }, (force ? {} : { etag: (_a = this.runtimeManifest) === null || _a === void 0 ? void 0 : _a.runtimeRevision })), this.runtimeAuth());
5896
- if (response)
5951
+ if (response) {
5897
5952
  this.applyRuntimeSnapshot(response);
5953
+ const secrets = await this.createNewSecretsService();
5954
+ this.applyRuntimeCredentialBundle(secrets);
5955
+ }
5898
5956
  this.scheduleRuntimePoll(0);
5899
5957
  })().finally(() => { this.runtimeRefreshInFlight = undefined; });
5900
5958
  return await this.runtimeRefreshInFlight;
@@ -5902,14 +5960,34 @@ class Ductape {
5902
5960
  /** Force a full product runtime refresh and atomically replace bootstrap cache entries. */
5903
5961
  async ready() {
5904
5962
  await this.initUserAuth(false);
5963
+ await this.runtimeWarmupInFlight;
5964
+ this.assertRuntimeReady();
5905
5965
  }
5906
5966
  /** Force a full product runtime refresh and atomically replace bootstrap cache entries. */
5907
5967
  async refreshRuntime() {
5908
5968
  await this.initUserAuth(false);
5909
5969
  await this.refreshRuntimeSnapshot(true);
5970
+ await this.runtimeWarmupInFlight;
5971
+ this.assertRuntimeReady();
5972
+ }
5973
+ assertRuntimeReady() {
5974
+ var _a, _b;
5975
+ if (!this.runtimeSyncEnabled)
5976
+ return;
5977
+ if (this.runtimeCredentialState.status === 'incomplete') {
5978
+ throw new Error(`Runtime credentials are incomplete (${this.runtimeCredentialState.missing} referenced secret(s) unavailable)`);
5979
+ }
5980
+ if (this.runtimeCredentialState.status === 'failed') {
5981
+ throw new Error(`Runtime credential preparation failed: ${this.runtimeCredentialState.error || 'unknown error'}`);
5982
+ }
5983
+ const failedConnections = (_b = (_a = this.runtimeWarmupState) === null || _a === void 0 ? void 0 : _a.databases.filter((database) => database.status === 'failed')) !== null && _b !== void 0 ? _b : [];
5984
+ if (this.runtimeWarmupEnabled && failedConnections.length) {
5985
+ throw new Error(`Runtime connection warm-up failed for: ${failedConnections.map((database) => database.tag).join(', ')}`);
5986
+ }
5910
5987
  }
5911
5988
  /** Observe the local product runtime snapshot without exposing cached configuration or secrets. */
5912
5989
  runtimeSnapshotStatus() {
5990
+ var _a;
5913
5991
  return {
5914
5992
  enabled: this.runtimeSyncEnabled,
5915
5993
  product: this.defaultProduct,
@@ -5918,16 +5996,28 @@ class Ductape {
5918
5996
  lastRefreshAt: this.runtimeLastRefreshAt,
5919
5997
  lastError: this.runtimeLastError,
5920
5998
  refreshing: Boolean(this.runtimeRefreshInFlight),
5999
+ readiness: {
6000
+ configuration: this.runtimeManifest ? 'ready' : 'loading',
6001
+ credentials: Object.assign({}, this.runtimeCredentialState),
6002
+ connections: this.runtimeWarmupInFlight
6003
+ ? 'warming'
6004
+ : ((_a = this.runtimeWarmupState) === null || _a === void 0 ? void 0 : _a.databases.some((database) => database.status === 'failed'))
6005
+ ? 'degraded'
6006
+ : 'ready',
6007
+ },
6008
+ warmup: this.runtimeWarmupState ? Object.assign(Object.assign({}, this.runtimeWarmupState), { databases: this.runtimeWarmupState.databases.map((database) => (Object.assign({}, database))), warming: Boolean(this.runtimeWarmupInFlight) }) : { warming: Boolean(this.runtimeWarmupInFlight), databases: [] },
5921
6009
  };
5922
6010
  }
5923
6011
  /** Stop background synchronization and release SDK-owned background resources. */
5924
6012
  async close() {
5925
- var _a;
6013
+ var _a, _b, _c;
5926
6014
  this.runtimeClosed = true;
5927
6015
  if (this.runtimePollTimer)
5928
6016
  clearTimeout(this.runtimePollTimer);
5929
6017
  this.runtimePollTimer = undefined;
5930
6018
  await ((_a = this.runtimeRefreshInFlight) === null || _a === void 0 ? void 0 : _a.catch(() => undefined));
6019
+ await ((_b = this.runtimeWarmupInFlight) === null || _b === void 0 ? void 0 : _b.catch(() => undefined));
6020
+ await ((_c = this._databaseService) === null || _c === void 0 ? void 0 : _c.closeAll().catch(() => undefined));
5931
6021
  const resources = [this.jobsWorker, this.healthCheckWorker, this.healthCheckUpdaterWorker,
5932
6022
  this.jobsQueue, this.healthCheckQueue, this.healthCheckUpdaterQueue];
5933
6023
  await Promise.all(resources.filter(Boolean).map((resource) => { var _a; return Promise.resolve((_a = resource.close) === null || _a === void 0 ? void 0 : _a.call(resource)).catch(() => undefined); }));
@@ -5935,11 +6025,11 @@ class Ductape {
5935
6025
  try {
5936
6026
  await this.redisClient.quit();
5937
6027
  }
5938
- catch (_b) {
6028
+ catch (_d) {
5939
6029
  try {
5940
6030
  this.redisClient.disconnect(false);
5941
6031
  }
5942
- catch ( /* best effort */_c) { /* best effort */ }
6032
+ catch ( /* best effort */_e) { /* best effort */ }
5943
6033
  }
5944
6034
  this.redisClient = undefined;
5945
6035
  }
@@ -6183,8 +6273,32 @@ class Ductape {
6183
6273
  // Set the global instance for use by other services (e.g., database, storage)
6184
6274
  (0, secrets_1.setSecretsService)(this._secretsService);
6185
6275
  }
6276
+ this.applyRuntimeCredentialBundle(this._secretsService);
6186
6277
  return this._secretsService;
6187
6278
  }
6279
+ applyRuntimeCredentialBundle(service) {
6280
+ var _a, _b, _c;
6281
+ if (!this.runtimeCredentialBundle)
6282
+ return;
6283
+ const revision = (_a = this.runtimeManifest) === null || _a === void 0 ? void 0 : _a.secretRevision;
6284
+ if (this.runtimeCredentialAppliedService === service && this.runtimeCredentialAppliedRevision === revision)
6285
+ return;
6286
+ try {
6287
+ service.replaceRuntimeBundle(this.runtimeCredentialBundle.encrypted_secrets);
6288
+ this.runtimeCredentialAppliedService = service;
6289
+ this.runtimeCredentialAppliedRevision = revision;
6290
+ const referenced = (_c = (_b = this.runtimeCredentialBundle.referenced_keys) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : this.runtimeCredentialBundle.encrypted_secrets.length;
6291
+ const cached = this.runtimeCredentialBundle.encrypted_secrets.length;
6292
+ const missing = Math.max(0, referenced - cached);
6293
+ this.runtimeCredentialState = {
6294
+ status: missing ? 'incomplete' : 'ready', referenced, cached, missing, refreshedAt: Date.now(),
6295
+ };
6296
+ }
6297
+ catch (error) {
6298
+ this.runtimeCredentialState = Object.assign(Object.assign({}, this.runtimeCredentialState), { status: 'failed', error: error instanceof Error ? error.message : String(error) });
6299
+ throw error;
6300
+ }
6301
+ }
6188
6302
  async createNewCloudService() {
6189
6303
  const subCheck = false;
6190
6304
  if (!this.token || !this.public_key) {