naro-cli 0.14.0 → 0.16.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/README.md CHANGED
@@ -173,14 +173,14 @@ naro policy set posts --from-file policy.json
173
173
  Shorthand: writes the policies naro_select … naro_delete
174
174
  naro policy remove todos Drop every policy on the table: back to deny (asks first)
175
175
 
176
- naro storage Naro Storage: on or off, limits, usage, buckets
176
+ naro storage Naro Storage: on or off, limits, usage, today's operations, buckets
177
177
  naro storage enable Turn it on: the project's R2 bucket, bound to the backend
178
178
  naro storage bucket create avatars --public --file-size-limit 5MiB --allowed-mime-types image/png,image/jpeg
179
179
  naro storage bucket update avatars --private | --file-size-limit none | --allowed-mime-types any
180
180
  naro storage bucket update docs --public (asks first: anyone with an object's URL can download it)
181
181
  naro storage bucket list | empty <name> | remove <name> [--empty] (empty and remove ask first)
182
182
  naro storage ls naro://avatars/u1 [--recursive]
183
- naro storage cp ./me.png naro://avatars/u1/me.png [--upsert] Up to 50 MiB, with the service key
183
+ naro storage cp ./me.png naro://avatars/u1/me.png [--upsert] Up to the object limit, under 4 GiB (over 50 MiB in parts), with the service key
184
184
  naro storage cp naro://docs/team-1/plan.pdf ./ Replaces a local file of the same name
185
185
  naro storage rm naro://avatars/u1/old.png [--recursive] (asks first)
186
186
  naro storage rm naro://avatars/u1/ --recursive Everything under u1/; written u1, an object u1 too
@@ -793,6 +793,50 @@ An `--update-check` you give replaces the default: without
793
793
 
794
794
  ## Release notes
795
795
 
796
+ ### 0.16.0
797
+
798
+ - `naro storage cp` uploads a file up to the object limit, under 4 GiB. Over
799
+ 50 MiB it goes in parts, four at a time, and a part that fails is sent again
800
+ (up to three more times) before the upload stops. The 50 MiB refusal before
801
+ any request is gone: the worker judges the size when the upload starts, and a
802
+ file over the bucket's or the plan's limit answers `413 OBJECT_TOO_LARGE`
803
+ before a byte is sent. An upload in parts must finish within 24 hours of its
804
+ start (R2 ends unfinished multipart uploads after a day). A big upload prints its progress on stderr: one line
805
+ rewritten in place at a terminal, a line every 10% otherwise
806
+ (`↑ 312 MiB / 1.2 GiB (26%)`), and nothing with `--json`.
807
+ - Ctrl-C during an upload (or a TERM, or the terminal closing) stops it:
808
+ `Stopping the upload…` on stderr, the upload is called off — one in parts on
809
+ the backend too, so its name and the space it reserved are free at once
810
+ rather than locked for about an hour — and the run ends with the signal. A
811
+ second Ctrl-C ends it at once, without waiting for the call-off. A process
812
+ killed outright, or a network lost, still leaves the name locked until the
813
+ lock runs out.
814
+ - A file of 4 GiB or more is refused before any request
815
+ (`VALIDATION_FAILED`): Node reads its size modulo 4 GiB (4 GiB + 1 KiB reads
816
+ as 1 KiB), and the upload would send only that much and report success.
817
+ Uploading one needs a change in naro-js that is not there yet.
818
+ - A download is written to disk as it arrives, to a hidden `.naro-partial`
819
+ file that replaces the destination once it is whole, instead of being held in
820
+ memory first.
821
+ - `naro storage` says `50 MiB per request (larger files go in parts)`, and
822
+ that uploads stopped over the storage limit reopen usually within an hour and
823
+ a half: R2 reports the bucket's size 30 minutes to about an hour and a half
824
+ late.
825
+
826
+ ### 0.15.0
827
+
828
+ - `naro storage` shows the Storage meter's day between `Used:` and `Buckets:`:
829
+ `Today (UTC):` has the write operations (R2 Class A) against the daily limit
830
+ and the reads, `Measured:` the bucket size R2 reported and when R2 measured
831
+ it (R2 reports the size 30 minutes to about an hour and a half late, and the
832
+ meter reads it every 15 minutes). A `✗` line
833
+ says uploads are stopped, over the storage limit until objects are deleted
834
+ or over the day's write operations until 00:00 UTC (downloads still work);
835
+ a `⚠` line warns from 80% of the storage limit or of the day's write
836
+ operations. Before the meter has run for the UTC day nothing is added, and
837
+ `--json` prints `storage.today` and `storage.limits.dailyOps` as the
838
+ control plane answered.
839
+
796
840
  ### 0.14.0
797
841
 
798
842
  - `naro analytics` shows the project's Web Analytics from the terminal: the
@@ -858,7 +902,8 @@ An `--update-check` you give replaces the default: without
858
902
  (public or private, the largest file, the types taken), and `ls`, `cp`
859
903
  and `rm` work on `naro://<bucket>/<path>` with the project's service key
860
904
  (`NARO_SERVICE_KEY`, or fetched with a warning) — the bytes go to the
861
- project's backend, not through naro. One upload is 50 MiB at most.
905
+ project's backend, not through naro. One request carries 50 MiB at most; a
906
+ larger file goes in parts since 0.16.0 (under 4 GiB).
862
907
  `bucket update --public`, `empty`, `remove` and `rm` ask first; without a
863
908
  terminal they need `--yes` (`CONFIRMATION_REQUIRED` otherwise, nothing
864
909
  sent).
@@ -26,6 +26,7 @@ function artifactUrlKey(kind, name) {
26
26
  return `${kind}/${name}`;
27
27
  }
28
28
  var ARTIFACT_URL_BATCH_SIZE = 100;
29
+ var ARTIFACT_UPLOAD_CONCURRENCY = 8;
29
30
  function parseArtifactPutUrls(payload) {
30
31
  if (typeof payload !== "object" || payload === null) return null;
31
32
  const { urls, disabled } = payload;
@@ -37,28 +38,55 @@ function parseArtifactPutUrls(payload) {
37
38
  }
38
39
  return resolved;
39
40
  }
41
+ async function eachWithLimit(items, limit, work) {
42
+ let next = 0;
43
+ const lanes = Array.from(
44
+ { length: Math.min(limit, items.length) },
45
+ async () => {
46
+ while (next < items.length) {
47
+ const item = items[next];
48
+ next += 1;
49
+ await work(item);
50
+ }
51
+ }
52
+ );
53
+ await Promise.all(lanes);
54
+ }
40
55
  async function uploadArtifactFiles(artifact, ports) {
41
56
  const entries = artifactUploadEntries(artifact);
42
57
  let proxied = 0;
43
58
  for (let index = 0; index < entries.length; index += ARTIFACT_URL_BATCH_SIZE) {
44
59
  const batch = entries.slice(index, index + ARTIFACT_URL_BATCH_SIZE);
45
60
  const urls = await ports.resolvePutUrls(batch);
46
- for (const entry of batch) {
61
+ const viaProxy = [];
62
+ await eachWithLimit(batch, ARTIFACT_UPLOAD_CONCURRENCY, async (entry) => {
47
63
  const url2 = urls?.get(artifactUrlKey(entry.kind, entry.file.name));
48
64
  if (!url2) {
49
- proxied += 1;
50
- await ports.putProxied(entry);
51
- continue;
65
+ viaProxy.push(entry);
66
+ return;
52
67
  }
53
68
  try {
54
69
  await ports.putSigned(url2, entry);
55
70
  } catch {
56
- proxied += 1;
57
- await ports.putProxied(entry);
71
+ viaProxy.push(entry);
58
72
  }
73
+ });
74
+ for (const entry of viaProxy) {
75
+ proxied += 1;
76
+ await ports.putProxied(entry);
59
77
  }
60
78
  }
61
- return { proxied, total: entries.length };
79
+ const bytes = entries.reduce(
80
+ (total, entry) => total + entry.file.data.byteLength,
81
+ 0
82
+ );
83
+ return { proxied, total: entries.length, bytes };
84
+ }
85
+ function artifactUploadSummary(report, elapsedMs) {
86
+ const kilobytes = Math.ceil(report.bytes / 1e3);
87
+ const size = kilobytes >= 1e3 ? `${(report.bytes / 1e6).toFixed(1)} MB` : `${String(kilobytes)} kB`;
88
+ const files = report.total === 1 ? "file" : "files";
89
+ return `Uploaded ${String(report.total)} artifact ${files} (${size}) in ${(elapsedMs / 1e3).toFixed(1)} s.`;
62
90
  }
63
91
 
64
92
  // ../../packages/build/src/cache.ts
@@ -100,6 +128,7 @@ async function reportUnavailable(scope, ports, observer) {
100
128
  "Build cache is unavailable - this build runs uncached and saves nothing for the next one."
101
129
  );
102
130
  }
131
+ var UNKNOWN_PAX_KEYWORD = /^tar: Ignoring unknown extended header keyword /;
103
132
  async function restoreCache(args) {
104
133
  const { scope, key, targetDir, ports, observer } = args;
105
134
  const urls = await ports.transport.resolveUrls(scope, key);
@@ -132,7 +161,9 @@ async function restoreCache(args) {
132
161
  stage: STAGE[scope],
133
162
  errorCode: ERROR_CODE[scope]
134
163
  },
135
- observer
164
+ {
165
+ onLog: (entry) => UNKNOWN_PAX_KEYWORD.test(entry.message) ? void 0 : observer.onLog(entry)
166
+ }
136
167
  );
137
168
  await log(observer, scope, "INFO", `Restored ${scope} cache (${key}).`);
138
169
  return true;
@@ -160,7 +191,14 @@ async function saveCache(args) {
160
191
  await ports.runCommand(
161
192
  {
162
193
  command: "tar",
163
- args: ["--zstd", "-cf", archive, "-C", sourceDir, "."],
194
+ // --no-xattrs: macOS bsdtar would store extended attributes that the
195
+ // build container's GNU tar warns about per file (UNKNOWN_PAX_KEYWORD).
196
+ // GNU tar 1.27+ and bsdtar 3.x both take it.
197
+ args: ["--no-xattrs", "--zstd", "-cf", archive, "-C", sourceDir, "."],
198
+ // macOS can also write the same metadata as AppleDouble `._name` files,
199
+ // which GNU tar restores as ordinary files (PR #114 review; not seen on
200
+ // bsdtar 3.5.3 here). GNU tar ignores the variable.
201
+ env: { COPYFILE_DISABLE: "1" },
164
202
  cwd: sourceDir,
165
203
  timeoutMs: 12e4,
166
204
  stage: STAGE[scope],
@@ -257,6 +295,9 @@ var PHASES = [
257
295
  ];
258
296
  var ACTIVE_STATUSES = new Set(PHASES);
259
297
 
298
+ // ../../packages/core/src/deployment-stall.ts
299
+ var STALL_WINDOW_MS = 10 * 6e4;
300
+
260
301
  // ../../packages/core/src/deployment-strategy.ts
261
302
  var MIN_SUPPORTED_NEXT_MAJOR = 15;
262
303
 
@@ -294,9 +335,6 @@ function pathnameOf(url2) {
294
335
  }
295
336
  }
296
337
 
297
- // ../../packages/core/src/internal-auth.ts
298
- import { Buffer as Buffer2 } from "buffer";
299
-
300
338
  // ../../packages/core/src/migration-names.ts
301
339
  function normalizeMigrationName(name) {
302
340
  return name.trim().toLowerCase().replace(/\.sql$/, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
@@ -15253,6 +15291,21 @@ function classifyNextVersion(raw, now) {
15253
15291
  // ../../packages/build/src/package-manager.ts
15254
15292
  import { access, readFile as readFile2 } from "fs/promises";
15255
15293
  import { join as join2 } from "path";
15294
+ function cacheEnv(dir) {
15295
+ return {
15296
+ npm_config_store_dir: dir,
15297
+ npm_config_cache: dir,
15298
+ YARN_CACHE_FOLDER: dir,
15299
+ YARN_ENABLE_GLOBAL_CACHE: "0"
15300
+ };
15301
+ }
15302
+ function withSharedCache(spec, dir) {
15303
+ return {
15304
+ ...spec,
15305
+ addArgs: (specs) => [...spec.addArgs(specs), ...spec.cacheArgs(dir)],
15306
+ addEnv: { ...spec.addEnv, ...cacheEnv(dir) }
15307
+ };
15308
+ }
15256
15309
  async function exists(path) {
15257
15310
  try {
15258
15311
  await access(path);
@@ -15302,7 +15355,8 @@ async function detectPackageManager(root) {
15302
15355
  "install",
15303
15356
  modern ? "--immutable" : "--frozen-lockfile"
15304
15357
  ],
15305
- // Berry takes no install-time cache flag; YARN_CACHE_FOLDER covers it.
15358
+ // Berry takes no install-time cache flag; cacheEnv covers it
15359
+ // (YARN_CACHE_FOLDER with the global cache switched off).
15306
15360
  cacheArgs: (dir) => modern ? [] : ["--cache-folder", dir],
15307
15361
  addArgs: (specs) => ["yarn", "add", ...specs]
15308
15362
  };
@@ -15706,7 +15760,7 @@ export default defineCloudflareConfig({});
15706
15760
 
15707
15761
  // ../../packages/build/src/vinext.ts
15708
15762
  var VINEXT_TOOLCHAIN = [
15709
- "vinext@1.0.0-beta.9",
15763
+ "vinext@1.1.0",
15710
15764
  "@cloudflare/vite-plugin@1.54.7",
15711
15765
  "@vitejs/plugin-react@6.1.1",
15712
15766
  "@vitejs/plugin-rsc@0.5.34",
@@ -15722,7 +15776,7 @@ var VITE_CONFIG_NAMES = [
15722
15776
  "vite.config.mts",
15723
15777
  "vite.config.cts"
15724
15778
  ];
15725
- var GENERATED_VITE_CONFIG = "vite.config.mjs";
15779
+ var GENERATED_VITE_CONFIG = "vite.config.js";
15726
15780
  var OUTPUT_DIR = "dist";
15727
15781
  var SERVER_DIR = "server";
15728
15782
  var CLIENT_DIR = "client";
@@ -15787,6 +15841,8 @@ async function createVinextBuildConfig(appRoot, deploymentId, writer = async (pa
15787
15841
  }
15788
15842
  var AUTO_FIXED_ISSUES = [/Missing "type": "module"/i];
15789
15843
  var ISSUE_LINE = /^\s*✗\s+(.+)$/;
15844
+ var PARTIAL_LINE = /^\s*~\s+(.+)$/;
15845
+ var BLOCKING_PARTIALS = [/^webpack\b/i];
15790
15846
  var ANSI_ESCAPE = /\u001B\[[0-9;]*m/g;
15791
15847
  var SUMMARY_LINE = /Overall:\s*(\d+)%\s*compatible\s*\(\s*(\d+)\s+supported,\s*(\d+)\s+partial,\s*(\d+)\s+issues?\s*\)/i;
15792
15848
  function parseVinextCheckReport(output) {
@@ -15798,12 +15854,18 @@ function parseVinextCheckReport(output) {
15798
15854
  plain.split("\n").map((line) => ISSUE_LINE.exec(line)?.[1]?.trim()).filter((line) => Boolean(line))
15799
15855
  )
15800
15856
  ];
15857
+ const partialLines = [
15858
+ ...new Set(
15859
+ plain.split("\n").map((line) => PARTIAL_LINE.exec(line)?.[1]?.trim()).filter((line) => Boolean(line))
15860
+ )
15861
+ ];
15801
15862
  return {
15802
15863
  percent: Number(match[1]),
15803
15864
  supported: Number(match[2]),
15804
15865
  partial: Number(match[3]),
15805
15866
  issues: Number(match[4]),
15806
- issueLines
15867
+ issueLines,
15868
+ partialLines
15807
15869
  };
15808
15870
  }
15809
15871
  function vinextRouteFromCheck(report) {
@@ -15815,7 +15877,10 @@ function vinextRouteFromCheck(report) {
15815
15877
  const autoFixed = report.issueLines.filter(
15816
15878
  (line) => AUTO_FIXED_ISSUES.some((pattern) => pattern.test(line))
15817
15879
  );
15818
- const blocking = Math.max(0, report.issues - autoFixed.length);
15880
+ const blockingPartials = report.partialLines.filter(
15881
+ (line) => BLOCKING_PARTIALS.some((pattern) => pattern.test(line))
15882
+ );
15883
+ const blocking = Math.max(0, report.issues - autoFixed.length) + blockingPartials.length;
15819
15884
  if (blocking > 0)
15820
15885
  return {
15821
15886
  eligible: false,
@@ -17836,6 +17901,7 @@ function createVinextCloudflareStrategy(options) {
17836
17901
  // Not production: npm and classic Yarn read that as omit=dev and
17837
17902
  // would prune the project's devDependencies while adding ours.
17838
17903
  env: {
17904
+ ...context.packageManager.addEnv,
17839
17905
  NODE_ENV: "development",
17840
17906
  NPM_CONFIG_USERCONFIG: context.packageManagerConfigPath
17841
17907
  },
@@ -18217,11 +18283,7 @@ var LocalBuildExecutor = class {
18217
18283
  env: {
18218
18284
  NODE_ENV: "development",
18219
18285
  NPM_CONFIG_USERCONFIG: packageManagerConfigPath,
18220
- ...cachePorts ? {
18221
- npm_config_store_dir: pmCacheDir,
18222
- npm_config_cache: pmCacheDir,
18223
- YARN_CACHE_FOLDER: pmCacheDir
18224
- } : {}
18286
+ ...cachePorts ? cacheEnv(pmCacheDir) : {}
18225
18287
  },
18226
18288
  timeoutMs: 10 * 6e4,
18227
18289
  stage: "INSTALLING",
@@ -18305,7 +18367,9 @@ var LocalBuildExecutor = class {
18305
18367
  identity,
18306
18368
  branch: input.branch,
18307
18369
  env: input.env ?? {},
18308
- packageManager,
18370
+ // The toolchain add has to use the store the install linked
18371
+ // node_modules from (withSharedCache).
18372
+ packageManager: cachePorts ? withSharedCache(packageManager, pmCacheDir) : packageManager,
18309
18373
  packageManagerConfigPath,
18310
18374
  buildScript: resolved.buildScript,
18311
18375
  nextVersion: nextVersion ? `${String(nextVersion.major)}.${String(nextVersion.minor)}.${String(nextVersion.patch)}` : "unknown",
@@ -18425,22 +18489,31 @@ function createLogBatcher(options) {
18425
18489
  let buffer = [];
18426
18490
  let timer = null;
18427
18491
  let pending = null;
18492
+ let sending = Promise.resolve();
18428
18493
  function disarm() {
18429
18494
  if (timer === null) return;
18430
18495
  clearTimeout(timer);
18431
18496
  timer = null;
18432
18497
  }
18498
+ function surfacePending() {
18499
+ if (pending === null) return;
18500
+ const { error: error51 } = pending;
18501
+ pending = null;
18502
+ throw error51;
18503
+ }
18433
18504
  async function flush() {
18434
18505
  disarm();
18435
- if (pending !== null) {
18436
- const { error: error51 } = pending;
18437
- pending = null;
18438
- throw error51;
18506
+ surfacePending();
18507
+ if (buffer.length === 0) {
18508
+ await sending;
18509
+ surfacePending();
18510
+ return;
18439
18511
  }
18440
- if (buffer.length === 0) return;
18441
18512
  const batch = buffer;
18442
18513
  buffer = [];
18443
- await options.send(batch);
18514
+ const send = sending.then(() => options.send(batch));
18515
+ sending = send.catch(() => void 0);
18516
+ await send;
18444
18517
  }
18445
18518
  function arm() {
18446
18519
  if (timer !== null || !options.intervalMs) return;
@@ -21059,6 +21132,10 @@ var CATALOG = {
21059
21132
  status: 404,
21060
21133
  message: ({ bucket, path }) => `Object ${storageName(path)} was not found in bucket ${storageName(bucket)}.`
21061
21134
  },
21135
+ UPLOAD_NOT_FOUND: {
21136
+ status: 404,
21137
+ message: ({ upload }) => `Upload ${storageName(upload)} was not found or has expired.`
21138
+ },
21062
21139
  INVALID_CONTENT_TYPE: {
21063
21140
  status: 400,
21064
21141
  message: ({ type }) => `Content-Type ${storageName(type)} is not a media type like image/png.`
@@ -21073,7 +21150,7 @@ var CATALOG = {
21073
21150
  },
21074
21151
  OBJECT_TOO_LARGE: {
21075
21152
  status: 413,
21076
- message: (vars) => "single" in vars ? "Uploads over 50 MiB are not supported yet." : `The object is ${String(vars.size)} bytes; bucket ${storageName(vars.bucket)} takes objects up to ${String(vars.max)} bytes.`
21153
+ message: (vars) => "single" in vars ? "Uploads over 50 MiB go in parts: use the multipart upload." : `The object is ${String(vars.size)} bytes; bucket ${storageName(vars.bucket)} takes objects up to ${String(vars.max)} bytes.`
21077
21154
  },
21078
21155
  MIME_TYPE_NOT_ALLOWED: {
21079
21156
  status: 415,
@@ -21109,6 +21186,12 @@ var CATALOG = {
21109
21186
  status: 507,
21110
21187
  message: () => "This project has used all of its storage. Delete objects to free space; uploads fail until then."
21111
21188
  },
21189
+ // Storage 3-1d (design §6.4): like WRITE_QUOTA_EXCEEDED, a 429 that clears
21190
+ // by itself at midnight; downloads, deletes and listings are not refused.
21191
+ STORAGE_OPS_QUOTA_EXCEEDED: {
21192
+ status: 429,
21193
+ message: () => "This project has reached its daily storage operation limit. Downloads still work; uploads resume at 00:00 UTC."
21194
+ },
21112
21195
  // R2 or its binding did not answer; the original error is logged, never sent.
21113
21196
  STORAGE_UNAVAILABLE: {
21114
21197
  status: 503,
@@ -25813,7 +25896,7 @@ function claimUpload(i) {
25813
25896
  const nonce = nonceOf(i.nonce);
25814
25897
  return compose(
25815
25898
  [
25816
- `INSERT INTO ${STORAGE_OBJECTS_TABLE} (id, bucket_id, name, owner, owner_id, metadata, version, parent, size, status, lock_id, lock_expires_at) SELECT ?, ?, ?, ?, ?, ?, NULL, ?, ?, 'uploading', ?, ? WHERE CASE WHEN NOT EXISTS (SELECT 1 FROM ${STORAGE_BUCKETS_TABLE} WHERE id = ? AND ${LIVE_BUCKET}) THEN ${marker("bucket", nonce)} WHEN (`,
25899
+ `INSERT INTO ${STORAGE_OBJECTS_TABLE} (id, bucket_id, name, owner, owner_id, metadata, version, parent, size, status, lock_id, lock_expires_at, pending) SELECT ?, ?, ?, ?, ?, ?, NULL, ?, ?, 'uploading', ?, ?, ? WHERE CASE WHEN NOT EXISTS (SELECT 1 FROM ${STORAGE_BUCKETS_TABLE} WHERE id = ? AND ${LIVE_BUCKET}) THEN ${marker("bucket", nonce)} WHEN (`,
25817
25900
  i.check,
25818
25901
  `) <> 1 THEN 0 WHEN EXISTS (SELECT 1 FROM ${STORAGE_OBJECTS_TABLE} WHERE bucket_id = ? AND name = ? AND status = 'ready') THEN ${marker("exists", nonce)} WHEN EXISTS (SELECT 1 FROM ${STORAGE_OBJECTS_TABLE} WHERE bucket_id = ? AND name = ?) THEN ${marker("busy", nonce)} WHEN ${PROJECT_BYTES} + ? > ? THEN ${marker("quota", nonce)} ELSE 1 END RETURNING id`
25819
25902
  ],
@@ -25829,6 +25912,7 @@ function claimUpload(i) {
25829
25912
  i.size,
25830
25913
  i.lockId,
25831
25914
  i.expiresAt,
25915
+ i.pending ?? null,
25832
25916
  i.bucket
25833
25917
  ],
25834
25918
  [i.bucket, i.name, i.bucket, i.name, i.size, i.limitBytes]
@@ -25852,6 +25936,13 @@ function overwriteCheckRow(i) {
25852
25936
  }
25853
25937
  function claimOverwrite(i) {
25854
25938
  const nonce = nonceOf(i.nonce);
25939
+ const room = i.inParts ? {
25940
+ sql: `${PROJECT_BYTES} + ? > ?`,
25941
+ values: [i.size, i.limitBytes]
25942
+ } : {
25943
+ sql: `? > o.size AND ${PROJECT_BYTES} + ? - o.size > ?`,
25944
+ values: [i.size, i.size, i.limitBytes]
25945
+ };
25855
25946
  return compose(
25856
25947
  [
25857
25948
  `UPDATE ${STORAGE_OBJECTS_TABLE} AS o SET lock_id = ?, lock_expires_at = ?, pending = ? WHERE o.bucket_id = ? AND o.name = ? AND CASE WHEN NOT (`,
@@ -25859,17 +25950,14 @@ function claimOverwrite(i) {
25859
25950
  // The row's own bucket (`o.bucket_id`), so no value is bound for it.
25860
25951
  `) THEN ${marker("hidden", nonce)} WHEN NOT EXISTS (SELECT 1 FROM ${STORAGE_BUCKETS_TABLE} AS b WHERE b.id = o.bucket_id AND b.${LIVE_BUCKET}) THEN ${marker("bucket", nonce)} WHEN o.status <> 'ready' OR o.lock_id IS NOT NULL THEN ${marker("busy", nonce)} WHEN NOT (`,
25861
25952
  i.updateUsing,
25862
- `) THEN ${marker("denied", nonce)} WHEN ? > o.size AND ${PROJECT_BYTES} + ? - o.size > ? THEN ${marker("quota", nonce)} ELSE (`,
25953
+ `) THEN ${marker("denied", nonce)} WHEN ${room.sql} THEN ${marker("quota", nonce)} ELSE (`,
25863
25954
  i.check,
25864
25955
  // RETURNING names the columns bare: it does not see the UPDATE's
25865
25956
  // alias (SQLite 3.53: `no such column: o.id`), and the table is the
25866
25957
  // only one there.
25867
25958
  ") END RETURNING id, version, size, owner, owner_id, created_at"
25868
25959
  ],
25869
- [
25870
- [i.lockId, i.expiresAt, i.pending, i.bucket, i.name],
25871
- [i.size, i.size, i.limitBytes]
25872
- ]
25960
+ [[i.lockId, i.expiresAt, i.pending, i.bucket, i.name], room.values]
25873
25961
  );
25874
25962
  }
25875
25963
  function claimDelete(i) {
@@ -27048,6 +27136,17 @@ function formatRedirectEntry(entry) {
27048
27136
  return entry.path === "/" ? entry.origin : `${entry.origin}${entry.path}`;
27049
27137
  }
27050
27138
 
27139
+ // ../../packages/data-core/src/storage-multipart.ts
27140
+ var MiB = 1024 * 1024;
27141
+ var MULTIPART_PART_SIZES = [16 * MiB, 32 * MiB, 64 * MiB];
27142
+ var MAX_MULTIPART_PARTS = 1e4;
27143
+ var MAX_MULTIPART_BYTES = 64 * MiB * MAX_MULTIPART_PARTS;
27144
+ var MULTIPART_LOCK_MS = 60 * 60 * 1e3;
27145
+ var MULTIPART_PART_EXTEND_BELOW_MS = 30 * 60 * 1e3;
27146
+ var MULTIPART_COMPLETE_LOCK_MS = 15 * 60 * 1e3;
27147
+ var UUID = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
27148
+ var UPLOAD_ID = new RegExp(`^(${UUID})\\.(${UUID})$`);
27149
+
27051
27150
  // ../../packages/data-core/src/system-sql.ts
27052
27151
  async function convertLegacyPolicies(db, found) {
27053
27152
  const rows = rowsOf(await db.prepare(POLICY_SQL.list).all());
@@ -28176,7 +28275,9 @@ function manifest(artifact) {
28176
28275
  };
28177
28276
  }
28178
28277
  async function uploadArtifact(artifact, opts) {
28179
- await uploadArtifactFiles(artifact, uploadPorts(opts));
28278
+ const started = Date.now();
28279
+ const report = await uploadArtifactFiles(artifact, uploadPorts(opts));
28280
+ const uploadSummary = artifactUploadSummary(report, Date.now() - started);
28180
28281
  const response = await authed(opts, "/artifacts/complete", {
28181
28282
  method: "POST",
28182
28283
  headers: { "Content-Type": "application/json" },
@@ -28187,7 +28288,8 @@ async function uploadArtifact(artifact, opts) {
28187
28288
  throw new Error("Control plane did not return an artifact key.");
28188
28289
  return {
28189
28290
  artifactKey: payload.artifactKey,
28190
- previewUrl: typeof payload.previewUrl === "string" ? payload.previewUrl : null
28291
+ previewUrl: typeof payload.previewUrl === "string" ? payload.previewUrl : null,
28292
+ uploadSummary
28191
28293
  };
28192
28294
  }
28193
28295
 
@@ -28263,7 +28365,8 @@ function eventsObserver(cp3, deploymentId, token, fetchImpl = fetch, logIngestUr
28263
28365
  await flushLogs();
28264
28366
  await post({ type: "metadata", metadata: rest }).catch(() => {
28265
28367
  });
28266
- }
28368
+ },
28369
+ flush: flushLogs
28267
28370
  };
28268
28371
  }
28269
28372
  function slugify2(value) {
@@ -28458,12 +28561,19 @@ async function deployDirectory(sourceDir, options, log2) {
28458
28561
  { details: { deploymentId: ctx.deploymentId }, cause: error51 }
28459
28562
  );
28460
28563
  }
28461
- await uploadArtifact(artifact, {
28564
+ const { uploadSummary } = await uploadArtifact(artifact, {
28462
28565
  controlPlaneUrl: cpUrl,
28463
28566
  deploymentId: ctx.deploymentId,
28464
28567
  callbackToken: ctx.callbackToken,
28465
28568
  ...options.fetch ? { fetch: options.fetch } : {}
28466
28569
  });
28570
+ log2(`\u25B8 ${uploadSummary}`);
28571
+ await observer.onLog({
28572
+ level: "INFO",
28573
+ stage: "DEPLOYING",
28574
+ message: uploadSummary
28575
+ });
28576
+ await observer.flush();
28467
28577
  log2("\u25B8 publishing worker");
28468
28578
  const detail = await waitForFinish(client, ctx, log2);
28469
28579
  const url2 = deploymentUrl(detail, ctx, production);
@@ -28541,7 +28651,6 @@ export {
28541
28651
  parseCacheControl,
28542
28652
  STORAGE_OBJECTS_TABLE,
28543
28653
  systemTablePredicate,
28544
- dataError,
28545
28654
  SCHEMA_SQL,
28546
28655
  tableNames,
28547
28656
  snapshotFromRows,
package/dist/deploy.d.ts CHANGED
@@ -21,7 +21,10 @@ export interface DeployBuildExecutor {
21
21
  export declare function parseDotenv(text: string): Record<string, string>;
22
22
  /** Reads the first of .env.local / .env.production / .env in a directory. */
23
23
  export declare function readProjectEnv(dir: string): Record<string, string>;
24
- export declare function eventsObserver(cp: string, deploymentId: string, token: string, fetchImpl?: typeof fetch, logIngestUrl?: string): BuildObserver;
24
+ export declare function eventsObserver(cp: string, deploymentId: string, token: string, fetchImpl?: typeof fetch, logIngestUrl?: string): BuildObserver & {
25
+ /** Sends what is buffered now; never throws. */
26
+ flush(): Promise<void>;
27
+ };
25
28
  export declare function slugify(value: string): string;
26
29
  export interface DeployOptions {
27
30
  name?: string | undefined;
package/dist/deploy.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  parseDotenv,
6
6
  readProjectEnv,
7
7
  slugify
8
- } from "./chunk-U7AZSAFS.js";
8
+ } from "./chunk-XNT7SINN.js";
9
9
  import "./chunk-K3NQKI34.js";
10
10
  export {
11
11
  deployDirectory,