@hot-updater/azure 1.1.4 → 1.2.0-rc.0

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.cjs CHANGED
@@ -20,8 +20,9 @@ const serviceOf = (config) => "connectionString" in config ? _azure_storage_blob
20
20
  */
21
21
  function azureBlobStorage(config) {
22
22
  const { containerName, basePath, getDownloadUrl } = config;
23
- const service = serviceOf(config);
24
- const container = service.getContainerClient(containerName);
23
+ let client;
24
+ const service = () => client ??= serviceOf(config);
25
+ const container = () => service().getContainerClient(containerName);
25
26
  const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
26
27
  const getStorageKey = (0, _hot_updater_plugin_core.createStorageKeyBuilder)(normalizedBasePath);
27
28
  const getListPrefix = (prefix = "") => {
@@ -53,7 +54,7 @@ function azureBlobStorage(config) {
53
54
  const delegationKey = () => {
54
55
  const now = Date.now();
55
56
  if (delegation === void 0 || delegation.renewAt <= now) {
56
- const key = service.getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
57
+ const key = service().getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
57
58
  key.catch(() => {
58
59
  delegation = void 0;
59
60
  });
@@ -65,7 +66,7 @@ function azureBlobStorage(config) {
65
66
  return delegation.key;
66
67
  };
67
68
  const signedUrl = async (key) => {
68
- const blob = container.getBlobClient(key);
69
+ const blob = container().getBlobClient(key);
69
70
  const expiresOn = new Date(Date.now() + SAS_EXPIRES_IN_MS);
70
71
  const permissions = _azure_storage_blob.BlobSASPermissions.parse("r");
71
72
  if (blob.credential instanceof _azure_storage_blob.StorageSharedKeyCredential) return blob.generateSasUrl({
@@ -86,7 +87,7 @@ function azureBlobStorage(config) {
86
87
  protocol: PROTOCOL,
87
88
  async listObjects(prefix) {
88
89
  const objects = [];
89
- for await (const blob of container.listBlobsFlat({ prefix: getListPrefix(prefix) })) {
90
+ for await (const blob of container().listBlobsFlat({ prefix: getListPrefix(prefix) })) {
90
91
  const storageUri = getListedStorageUri(blob.name);
91
92
  if (storageUri === null) continue;
92
93
  objects.push({
@@ -103,17 +104,36 @@ function azureBlobStorage(config) {
103
104
  for (let offset = 0; offset < keys.length; offset += 256) {
104
105
  const batch = keys.slice(offset, offset + 256);
105
106
  if (batch.length === 0) continue;
106
- const failed = (await container.getBlobBatchClient().deleteBlobs(batch.map((key) => container.getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
107
+ const failed = (await container().getBlobBatchClient().deleteBlobs(batch.map((key) => container().getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
107
108
  if (failed.length > 0) throw new Error(`Failed to delete ${failed.length} Azure blob(s).`);
108
109
  }
109
110
  },
110
- async put({ key, body, contentType }) {
111
+ async put({ key, body, contentLength, contentType }) {
111
112
  const storageKey = getStorageKey(key);
112
- const bytes = new Uint8Array(await new Response(body).arrayBuffer());
113
- await container.getBlockBlobClient(storageKey).uploadData(bytes, { blobHTTPHeaders: {
113
+ const options = { blobHTTPHeaders: {
114
114
  blobContentType: contentType,
115
115
  blobCacheControl: "max-age=31536000"
116
- } });
116
+ } };
117
+ const blob = container().getBlockBlobClient(storageKey);
118
+ if (contentLength === void 0) await blob.uploadData(new Uint8Array(await new Response(body).arrayBuffer()), options);
119
+ else {
120
+ const { Readable } = await import("node:stream");
121
+ const source = Readable.fromWeb(body);
122
+ const aborted = new AbortController();
123
+ let failure;
124
+ source.once("error", (error) => {
125
+ failure = error;
126
+ aborted.abort(error);
127
+ });
128
+ try {
129
+ await blob.upload(source, contentLength, {
130
+ ...options,
131
+ abortSignal: aborted.signal
132
+ });
133
+ } catch (error) {
134
+ throw failure ?? error;
135
+ }
136
+ }
117
137
  return { storageUri: (0, _hot_updater_plugin_core.createStorageUri)({
118
138
  bucket: containerName,
119
139
  key: storageKey,
@@ -123,7 +143,7 @@ function azureBlobStorage(config) {
123
143
  async get({ storageUri }) {
124
144
  const { key } = parseAndValidate(storageUri);
125
145
  try {
126
- const download = await container.getBlobClient(key).download();
146
+ const download = await container().getBlobClient(key).download();
127
147
  const body = download.readableStreamBody;
128
148
  if (body === void 0) return { response: null };
129
149
  const headers = new Headers();
@@ -143,11 +163,11 @@ function azureBlobStorage(config) {
143
163
  },
144
164
  async exists({ storageUri }) {
145
165
  const { key } = parseAndValidate(storageUri);
146
- return { exists: await container.getBlobClient(key).exists() };
166
+ return { exists: await container().getBlobClient(key).exists() };
147
167
  },
148
168
  async delete({ storageUri }) {
149
169
  const { key } = parseAndValidate(storageUri);
150
- await container.getBlobClient(key).deleteIfExists();
170
+ await container().getBlobClient(key).deleteIfExists();
151
171
  return { deleted: true };
152
172
  }
153
173
  });
@@ -364,7 +384,15 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
364
384
  * replaces the row it read.
365
385
  */
366
386
  const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
367
- const container = client.database(databaseId).container(containerId);
387
+ let opened;
388
+ const open = () => opened ??= (() => {
389
+ const account = typeof client === "function" ? client() : client;
390
+ return {
391
+ client: account,
392
+ container: account.database(databaseId).container(containerId)
393
+ };
394
+ })();
395
+ const container = () => open().container;
368
396
  /**
369
397
  * The state of the items `ids` names, by id, with the rows of `rowIds`;
370
398
  * a missing item has none.
@@ -372,7 +400,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
372
400
  const readStates = async (ids, rowIds) => {
373
401
  const states = /* @__PURE__ */ new Map();
374
402
  for (const [group, projection] of [[ids.filter((id) => !rowIds.has(id)), ""], [ids.filter((id) => rowIds.has(id)), ", c.row"]]) for (let at = 0; at < group.length; at += GET_BATCH) {
375
- const { resources } = await container.items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
403
+ const { resources } = await container().items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
376
404
  for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
377
405
  etag,
378
406
  v,
@@ -510,7 +538,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
510
538
  const found = /* @__PURE__ */ new Map();
511
539
  const ids = [...new Set(keys.map(cosmosItemId))];
512
540
  for (let at = 0; at < ids.length; at += GET_BATCH) {
513
- const { resources } = await container.items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
541
+ const { resources } = await container().items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
514
542
  for (const resource of resources) found.set(resource.id, toRow(resource.row));
515
543
  }
516
544
  return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
@@ -546,7 +574,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
546
574
  value: after
547
575
  });
548
576
  }
549
- const { resources } = await container.items.query({
577
+ const { resources } = await container().items.query({
550
578
  query: `SELECT TOP @size c.sk, c.row FROM c WHERE ${filters.join(" AND ")} ORDER BY c.pk ${order === "asc" ? "ASC" : "DESC"}, c.sk ${order === "asc" ? "ASC" : "DESC"}`,
551
579
  parameters
552
580
  }, {
@@ -596,7 +624,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
596
624
  if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
597
625
  let results;
598
626
  try {
599
- const response = await container.items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
627
+ const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
600
628
  if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
601
629
  results = response.result;
602
630
  } catch (error) {
@@ -630,7 +658,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
630
658
  async deleteConsumed(keys) {
631
659
  let remaining = [...new Set(keys.map(cosmosItemId))];
632
660
  for (let attempt = 0; remaining.length > 0; attempt += 1) {
633
- const failed = (await container.items.executeBulkOperations(remaining.map((id) => ({
661
+ const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
634
662
  operationType: "Delete",
635
663
  id,
636
664
  partitionKey: COSMOS_PARTITION
@@ -651,8 +679,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
651
679
  migrations: {
652
680
  /** Creates the database and the container when they are missing. */
653
681
  async apply() {
654
- if (engine !== void 0) await assertCosmosContainerReusable(container, engine);
655
- const { database } = await client.databases.createIfNotExists({ id: databaseId });
682
+ if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
683
+ const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
656
684
  const { resource } = await database.containers.createIfNotExists({
657
685
  id: containerId,
658
686
  partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
@@ -688,15 +716,15 @@ async apply() {
688
716
  const adapterOf = (config) => {
689
717
  const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
690
718
  if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
691
- const client = new _azure_cosmos.CosmosClient(connection);
719
+ let client;
692
720
  const store = createCosmosStore({
693
- client,
721
+ client: () => client ??= new _azure_cosmos.CosmosClient(connection),
694
722
  databaseId,
695
723
  containerId,
696
724
  engine: currentCosmosEngine()
697
725
  });
698
726
  return {
699
- client,
727
+ dispose: () => client?.dispose(),
700
728
  adapter: (0, _hot_updater_plugin_core.createKvAdapter)({ store })
701
729
  };
702
730
  };
@@ -708,8 +736,23 @@ const adapterOf = (config) => {
708
736
  * it with the managed server's plugins.
709
737
  */
710
738
  const migrateCosmosDatabase = (config, plugins = []) => {
711
- const { client, adapter } = adapterOf(config);
712
- return (0, _hot_updater_plugin_core.migrateCoreSchema)(adapter, "cosmosDatabase", plugins).finally(() => client.dispose());
739
+ const { dispose, adapter } = adapterOf(config);
740
+ return (0, _hot_updater_plugin_core.migrateCoreSchema)(adapter, "cosmosDatabase", plugins).finally(dispose);
741
+ };
742
+ /**
743
+ * The resource this database reaches, without credentials, under the key
744
+ * `hot-updater doctor` reads to tell the database `hot-updater init` saved
745
+ * from another (a convention CLAUDE.md documents).
746
+ */
747
+ const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
748
+ /** The account host a client reaches: its endpoint, or the connection string's. */
749
+ const accountHostOf = ({ endpoint, connectionString }) => {
750
+ const url = endpoint ?? connectionString?.match(/AccountEndpoint=([^;]+)/iu)?.[1];
751
+ try {
752
+ return url === void 0 ? void 0 : new URL(url).hostname;
753
+ } catch {
754
+ return;
755
+ }
713
756
  };
714
757
  /**
715
758
  * Hot Updater's database in one Azure Cosmos DB for NoSQL container,
@@ -718,16 +761,18 @@ const migrateCosmosDatabase = (config, plugins = []) => {
718
761
  * 20 GB.
719
762
  */
720
763
  const cosmosDatabase = (config) => {
721
- const { client, adapter } = adapterOf(config);
764
+ const { dispose, adapter } = adapterOf(config);
765
+ const host = accountHostOf(config);
722
766
  return {
723
767
  ...(0, _hot_updater_plugin_core.createEngineDatabase)({
724
768
  name: "cosmosDatabase",
725
769
  adapter,
726
770
  aggregateBatching: config.aggregateBatching ?? {}
727
771
  }),
772
+ ...host === void 0 ? {} : { [DATABASE_TARGET]: `cosmos/${host}` },
728
773
  async dispose() {
729
774
  await adapter.dispose?.();
730
- client.dispose();
775
+ dispose();
731
776
  }
732
777
  };
733
778
  };
package/dist/index.mjs CHANGED
@@ -19,8 +19,9 @@ const serviceOf = (config) => "connectionString" in config ? BlobServiceClient.f
19
19
  */
20
20
  function azureBlobStorage(config) {
21
21
  const { containerName, basePath, getDownloadUrl } = config;
22
- const service = serviceOf(config);
23
- const container = service.getContainerClient(containerName);
22
+ let client;
23
+ const service = () => client ??= serviceOf(config);
24
+ const container = () => service().getContainerClient(containerName);
24
25
  const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "") ?? "";
25
26
  const getStorageKey = createStorageKeyBuilder(normalizedBasePath);
26
27
  const getListPrefix = (prefix = "") => {
@@ -52,7 +53,7 @@ function azureBlobStorage(config) {
52
53
  const delegationKey = () => {
53
54
  const now = Date.now();
54
55
  if (delegation === void 0 || delegation.renewAt <= now) {
55
- const key = service.getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
56
+ const key = service().getUserDelegationKey(/* @__PURE__ */ new Date(now - 3e5), new Date(now + DELEGATION_KEY_LIFETIME_MS));
56
57
  key.catch(() => {
57
58
  delegation = void 0;
58
59
  });
@@ -64,7 +65,7 @@ function azureBlobStorage(config) {
64
65
  return delegation.key;
65
66
  };
66
67
  const signedUrl = async (key) => {
67
- const blob = container.getBlobClient(key);
68
+ const blob = container().getBlobClient(key);
68
69
  const expiresOn = new Date(Date.now() + SAS_EXPIRES_IN_MS);
69
70
  const permissions = BlobSASPermissions.parse("r");
70
71
  if (blob.credential instanceof StorageSharedKeyCredential) return blob.generateSasUrl({
@@ -85,7 +86,7 @@ function azureBlobStorage(config) {
85
86
  protocol: PROTOCOL,
86
87
  async listObjects(prefix) {
87
88
  const objects = [];
88
- for await (const blob of container.listBlobsFlat({ prefix: getListPrefix(prefix) })) {
89
+ for await (const blob of container().listBlobsFlat({ prefix: getListPrefix(prefix) })) {
89
90
  const storageUri = getListedStorageUri(blob.name);
90
91
  if (storageUri === null) continue;
91
92
  objects.push({
@@ -102,17 +103,36 @@ function azureBlobStorage(config) {
102
103
  for (let offset = 0; offset < keys.length; offset += 256) {
103
104
  const batch = keys.slice(offset, offset + 256);
104
105
  if (batch.length === 0) continue;
105
- const failed = (await container.getBlobBatchClient().deleteBlobs(batch.map((key) => container.getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
106
+ const failed = (await container().getBlobBatchClient().deleteBlobs(batch.map((key) => container().getBlobClient(key)))).subResponses.filter(({ status }) => status >= 300 && status !== 404);
106
107
  if (failed.length > 0) throw new Error(`Failed to delete ${failed.length} Azure blob(s).`);
107
108
  }
108
109
  },
109
- async put({ key, body, contentType }) {
110
+ async put({ key, body, contentLength, contentType }) {
110
111
  const storageKey = getStorageKey(key);
111
- const bytes = new Uint8Array(await new Response(body).arrayBuffer());
112
- await container.getBlockBlobClient(storageKey).uploadData(bytes, { blobHTTPHeaders: {
112
+ const options = { blobHTTPHeaders: {
113
113
  blobContentType: contentType,
114
114
  blobCacheControl: "max-age=31536000"
115
- } });
115
+ } };
116
+ const blob = container().getBlockBlobClient(storageKey);
117
+ if (contentLength === void 0) await blob.uploadData(new Uint8Array(await new Response(body).arrayBuffer()), options);
118
+ else {
119
+ const { Readable } = await import("node:stream");
120
+ const source = Readable.fromWeb(body);
121
+ const aborted = new AbortController();
122
+ let failure;
123
+ source.once("error", (error) => {
124
+ failure = error;
125
+ aborted.abort(error);
126
+ });
127
+ try {
128
+ await blob.upload(source, contentLength, {
129
+ ...options,
130
+ abortSignal: aborted.signal
131
+ });
132
+ } catch (error) {
133
+ throw failure ?? error;
134
+ }
135
+ }
116
136
  return { storageUri: createStorageUri({
117
137
  bucket: containerName,
118
138
  key: storageKey,
@@ -122,7 +142,7 @@ function azureBlobStorage(config) {
122
142
  async get({ storageUri }) {
123
143
  const { key } = parseAndValidate(storageUri);
124
144
  try {
125
- const download = await container.getBlobClient(key).download();
145
+ const download = await container().getBlobClient(key).download();
126
146
  const body = download.readableStreamBody;
127
147
  if (body === void 0) return { response: null };
128
148
  const headers = new Headers();
@@ -142,11 +162,11 @@ function azureBlobStorage(config) {
142
162
  },
143
163
  async exists({ storageUri }) {
144
164
  const { key } = parseAndValidate(storageUri);
145
- return { exists: await container.getBlobClient(key).exists() };
165
+ return { exists: await container().getBlobClient(key).exists() };
146
166
  },
147
167
  async delete({ storageUri }) {
148
168
  const { key } = parseAndValidate(storageUri);
149
- await container.getBlobClient(key).deleteIfExists();
169
+ await container().getBlobClient(key).deleteIfExists();
150
170
  return { deleted: true };
151
171
  }
152
172
  });
@@ -363,7 +383,15 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
363
383
  * replaces the row it read.
364
384
  */
365
385
  const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
366
- const container = client.database(databaseId).container(containerId);
386
+ let opened;
387
+ const open = () => opened ??= (() => {
388
+ const account = typeof client === "function" ? client() : client;
389
+ return {
390
+ client: account,
391
+ container: account.database(databaseId).container(containerId)
392
+ };
393
+ })();
394
+ const container = () => open().container;
367
395
  /**
368
396
  * The state of the items `ids` names, by id, with the rows of `rowIds`;
369
397
  * a missing item has none.
@@ -371,7 +399,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
371
399
  const readStates = async (ids, rowIds) => {
372
400
  const states = /* @__PURE__ */ new Map();
373
401
  for (const [group, projection] of [[ids.filter((id) => !rowIds.has(id)), ""], [ids.filter((id) => rowIds.has(id)), ", c.row"]]) for (let at = 0; at < group.length; at += GET_BATCH) {
374
- const { resources } = await container.items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
402
+ const { resources } = await container().items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
375
403
  for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
376
404
  etag,
377
405
  v,
@@ -509,7 +537,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
509
537
  const found = /* @__PURE__ */ new Map();
510
538
  const ids = [...new Set(keys.map(cosmosItemId))];
511
539
  for (let at = 0; at < ids.length; at += GET_BATCH) {
512
- const { resources } = await container.items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
540
+ const { resources } = await container().items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
513
541
  for (const resource of resources) found.set(resource.id, toRow(resource.row));
514
542
  }
515
543
  return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
@@ -545,7 +573,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
545
573
  value: after
546
574
  });
547
575
  }
548
- const { resources } = await container.items.query({
576
+ const { resources } = await container().items.query({
549
577
  query: `SELECT TOP @size c.sk, c.row FROM c WHERE ${filters.join(" AND ")} ORDER BY c.pk ${order === "asc" ? "ASC" : "DESC"}, c.sk ${order === "asc" ? "ASC" : "DESC"}`,
550
578
  parameters
551
579
  }, {
@@ -595,7 +623,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
595
623
  if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
596
624
  let results;
597
625
  try {
598
- const response = await container.items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
626
+ const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
599
627
  if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
600
628
  results = response.result;
601
629
  } catch (error) {
@@ -629,7 +657,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
629
657
  async deleteConsumed(keys) {
630
658
  let remaining = [...new Set(keys.map(cosmosItemId))];
631
659
  for (let attempt = 0; remaining.length > 0; attempt += 1) {
632
- const failed = (await container.items.executeBulkOperations(remaining.map((id) => ({
660
+ const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
633
661
  operationType: "Delete",
634
662
  id,
635
663
  partitionKey: COSMOS_PARTITION
@@ -650,8 +678,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
650
678
  migrations: {
651
679
  /** Creates the database and the container when they are missing. */
652
680
  async apply() {
653
- if (engine !== void 0) await assertCosmosContainerReusable(container, engine);
654
- const { database } = await client.databases.createIfNotExists({ id: databaseId });
681
+ if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
682
+ const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
655
683
  const { resource } = await database.containers.createIfNotExists({
656
684
  id: containerId,
657
685
  partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
@@ -687,15 +715,15 @@ async apply() {
687
715
  const adapterOf = (config) => {
688
716
  const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
689
717
  if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
690
- const client = new CosmosClient(connection);
718
+ let client;
691
719
  const store = createCosmosStore({
692
- client,
720
+ client: () => client ??= new CosmosClient(connection),
693
721
  databaseId,
694
722
  containerId,
695
723
  engine: currentCosmosEngine()
696
724
  });
697
725
  return {
698
- client,
726
+ dispose: () => client?.dispose(),
699
727
  adapter: createKvAdapter({ store })
700
728
  };
701
729
  };
@@ -707,8 +735,23 @@ const adapterOf = (config) => {
707
735
  * it with the managed server's plugins.
708
736
  */
709
737
  const migrateCosmosDatabase = (config, plugins = []) => {
710
- const { client, adapter } = adapterOf(config);
711
- return migrateCoreSchema(adapter, "cosmosDatabase", plugins).finally(() => client.dispose());
738
+ const { dispose, adapter } = adapterOf(config);
739
+ return migrateCoreSchema(adapter, "cosmosDatabase", plugins).finally(dispose);
740
+ };
741
+ /**
742
+ * The resource this database reaches, without credentials, under the key
743
+ * `hot-updater doctor` reads to tell the database `hot-updater init` saved
744
+ * from another (a convention CLAUDE.md documents).
745
+ */
746
+ const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
747
+ /** The account host a client reaches: its endpoint, or the connection string's. */
748
+ const accountHostOf = ({ endpoint, connectionString }) => {
749
+ const url = endpoint ?? connectionString?.match(/AccountEndpoint=([^;]+)/iu)?.[1];
750
+ try {
751
+ return url === void 0 ? void 0 : new URL(url).hostname;
752
+ } catch {
753
+ return;
754
+ }
712
755
  };
713
756
  /**
714
757
  * Hot Updater's database in one Azure Cosmos DB for NoSQL container,
@@ -717,16 +760,18 @@ const migrateCosmosDatabase = (config, plugins = []) => {
717
760
  * 20 GB.
718
761
  */
719
762
  const cosmosDatabase = (config) => {
720
- const { client, adapter } = adapterOf(config);
763
+ const { dispose, adapter } = adapterOf(config);
764
+ const host = accountHostOf(config);
721
765
  return {
722
766
  ...createEngineDatabase({
723
767
  name: "cosmosDatabase",
724
768
  adapter,
725
769
  aggregateBatching: config.aggregateBatching ?? {}
726
770
  }),
771
+ ...host === void 0 ? {} : { [DATABASE_TARGET]: `cosmos/${host}` },
727
772
  async dispose() {
728
773
  await adapter.dispose?.();
729
- client.dispose();
774
+ dispose();
730
775
  }
731
776
  };
732
777
  };
@@ -266,7 +266,15 @@ const emulatorAborted = (error) => statusOf(error) === 500 && /PostgresError\(Sq
266
266
  * replaces the row it read.
267
267
  */
268
268
  const createCosmosStore = ({ client, databaseId, containerId, engine, nativePageSize }) => {
269
- const container = client.database(databaseId).container(containerId);
269
+ let opened;
270
+ const open = () => opened ??= (() => {
271
+ const account = typeof client === "function" ? client() : client;
272
+ return {
273
+ client: account,
274
+ container: account.database(databaseId).container(containerId)
275
+ };
276
+ })();
277
+ const container = () => open().container;
270
278
  /**
271
279
  * The state of the items `ids` names, by id, with the rows of `rowIds`;
272
280
  * a missing item has none.
@@ -274,7 +282,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
274
282
  const readStates = async (ids, rowIds) => {
275
283
  const states = /* @__PURE__ */ new Map();
276
284
  for (const [group, projection] of [[ids.filter((id) => !rowIds.has(id)), ""], [ids.filter((id) => rowIds.has(id)), ", c.row"]]) for (let at = 0; at < group.length; at += GET_BATCH) {
277
- const { resources } = await container.items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
285
+ const { resources } = await container().items.query(`SELECT c.id, c._etag AS etag, c.row._v AS v, c.expireAt${projection} FROM c WHERE c.id IN (${idList(group.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
278
286
  for (const { id, etag, v, expireAt, row } of resources) states.set(id, {
279
287
  etag,
280
288
  v,
@@ -412,7 +420,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
412
420
  const found = /* @__PURE__ */ new Map();
413
421
  const ids = [...new Set(keys.map(cosmosItemId))];
414
422
  for (let at = 0; at < ids.length; at += GET_BATCH) {
415
- const { resources } = await container.items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
423
+ const { resources } = await container().items.query(`SELECT c.id, c.row FROM c WHERE c.id IN (${idList(ids.slice(at, at + GET_BATCH))})`, { partitionKey: COSMOS_PARTITION }).fetchAll().catch(missingContainer);
416
424
  for (const resource of resources) found.set(resource.id, toRow(resource.row));
417
425
  }
418
426
  return keys.map((key) => found.get(cosmosItemId(key)) ?? null);
@@ -448,7 +456,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
448
456
  value: after
449
457
  });
450
458
  }
451
- const { resources } = await container.items.query({
459
+ const { resources } = await container().items.query({
452
460
  query: `SELECT TOP @size c.sk, c.row FROM c WHERE ${filters.join(" AND ")} ORDER BY c.pk ${order === "asc" ? "ASC" : "DESC"}, c.sk ${order === "asc" ? "ASC" : "DESC"}`,
453
461
  parameters
454
462
  }, {
@@ -498,7 +506,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
498
506
  if (planned.length > BATCH_OPERATIONS) throw new Error(`A Cosmos DB batch takes ${BATCH_OPERATIONS} operations; this write needs ${planned.length}.`);
499
507
  let results;
500
508
  try {
501
- const response = await container.items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
509
+ const response = await container().items.batch(planned.map(({ operation }) => operation), COSMOS_PARTITION);
502
510
  if (response.result === void 0) throw Object.assign(/* @__PURE__ */ new Error(`Cosmos DB refused the batch with status ${response.code}`), { code: response.code });
503
511
  results = response.result;
504
512
  } catch (error) {
@@ -532,7 +540,7 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
532
540
  async deleteConsumed(keys) {
533
541
  let remaining = [...new Set(keys.map(cosmosItemId))];
534
542
  for (let attempt = 0; remaining.length > 0; attempt += 1) {
535
- const failed = (await container.items.executeBulkOperations(remaining.map((id) => ({
543
+ const failed = (await container().items.executeBulkOperations(remaining.map((id) => ({
536
544
  operationType: "Delete",
537
545
  id,
538
546
  partitionKey: COSMOS_PARTITION
@@ -553,8 +561,8 @@ const createCosmosStore = ({ client, databaseId, containerId, engine, nativePage
553
561
  migrations: {
554
562
  /** Creates the database and the container when they are missing. */
555
563
  async apply() {
556
- if (engine !== void 0) await assertCosmosContainerReusable(container, engine);
557
- const { database } = await client.databases.createIfNotExists({ id: databaseId });
564
+ if (engine !== void 0) await assertCosmosContainerReusable(container(), engine);
565
+ const { database } = await open().client.databases.createIfNotExists({ id: databaseId });
558
566
  const { resource } = await database.containers.createIfNotExists({
559
567
  id: containerId,
560
568
  partitionKey: { paths: [COSMOS_PARTITION_KEY_PATH] },
@@ -590,15 +598,15 @@ async apply() {
590
598
  const adapterOf = (config) => {
591
599
  const { databaseId = AZURE_COSMOS_V1_DATABASE, containerId = AZURE_COSMOS_V1_CONTAINER, aggregateBatching: _, ...connection } = config;
592
600
  if (connection.consistencyLevel !== void 0 && connection.consistencyLevel !== "Strong") throw new Error(`cosmosDatabase reads with strong consistency; consistencyLevel "${String(connection.consistencyLevel)}" is weaker.`);
593
- const client = new _azure_cosmos.CosmosClient(connection);
601
+ let client;
594
602
  const store = createCosmosStore({
595
- client,
603
+ client: () => client ??= new _azure_cosmos.CosmosClient(connection),
596
604
  databaseId,
597
605
  containerId,
598
606
  engine: currentCosmosEngine()
599
607
  });
600
608
  return {
601
- client,
609
+ dispose: () => client?.dispose(),
602
610
  adapter: (0, _hot_updater_plugin_core.createKvAdapter)({ store })
603
611
  };
604
612
  };
@@ -610,8 +618,23 @@ const adapterOf = (config) => {
610
618
  * it with the managed server's plugins.
611
619
  */
612
620
  const migrateCosmosDatabase = (config, plugins = []) => {
613
- const { client, adapter } = adapterOf(config);
614
- return (0, _hot_updater_plugin_core.migrateCoreSchema)(adapter, "cosmosDatabase", plugins).finally(() => client.dispose());
621
+ const { dispose, adapter } = adapterOf(config);
622
+ return (0, _hot_updater_plugin_core.migrateCoreSchema)(adapter, "cosmosDatabase", plugins).finally(dispose);
623
+ };
624
+ /**
625
+ * The resource this database reaches, without credentials, under the key
626
+ * `hot-updater doctor` reads to tell the database `hot-updater init` saved
627
+ * from another (a convention CLAUDE.md documents).
628
+ */
629
+ const DATABASE_TARGET = Symbol.for("@hot-updater/database-target");
630
+ /** The account host a client reaches: its endpoint, or the connection string's. */
631
+ const accountHostOf = ({ endpoint, connectionString }) => {
632
+ const url = endpoint ?? connectionString?.match(/AccountEndpoint=([^;]+)/iu)?.[1];
633
+ try {
634
+ return url === void 0 ? void 0 : new URL(url).hostname;
635
+ } catch {
636
+ return;
637
+ }
615
638
  };
616
639
  /**
617
640
  * Hot Updater's database in one Azure Cosmos DB for NoSQL container,
@@ -620,16 +643,18 @@ const migrateCosmosDatabase = (config, plugins = []) => {
620
643
  * 20 GB.
621
644
  */
622
645
  const cosmosDatabase = (config) => {
623
- const { client, adapter } = adapterOf(config);
646
+ const { dispose, adapter } = adapterOf(config);
647
+ const host = accountHostOf(config);
624
648
  return {
625
649
  ...(0, _hot_updater_plugin_core.createEngineDatabase)({
626
650
  name: "cosmosDatabase",
627
651
  adapter,
628
652
  aggregateBatching: config.aggregateBatching ?? {}
629
653
  }),
654
+ ...host === void 0 ? {} : { [DATABASE_TARGET]: `cosmos/${host}` },
630
655
  async dispose() {
631
656
  await adapter.dispose?.();
632
- client.dispose();
657
+ dispose();
633
658
  }
634
659
  };
635
660
  };