@hot-updater/vercel 1.2.0-rc.12 → 1.2.0-rc.13

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.
@@ -2470,6 +2470,10 @@ const nativeBuilds = defineTable({
2470
2470
  type: "string",
2471
2471
  maxLength: 255,
2472
2472
  required: false
2473
+ },
2474
+ store_build: {
2475
+ type: "boolean",
2476
+ required: false
2473
2477
  }
2474
2478
  }, {
2475
2479
  key: ["id"],
@@ -2482,6 +2486,11 @@ const nativeBuilds = defineTable({
2482
2486
  eq: ["scope_key", "build_key"],
2483
2487
  sort: ["id"]
2484
2488
  },
2489
+ byStoreBuild: {
2490
+ eq: ["scope_key", "store_build"],
2491
+ sort: [],
2492
+ unique: true
2493
+ },
2485
2494
  byChannel: {
2486
2495
  eq: ["channel_id"],
2487
2496
  sort: ["id"],
@@ -2677,10 +2686,7 @@ const LOG_TABLES = [
2677
2686
  const LEASE_TABLE = "aggregate_lease";
2678
2687
  const LEASE = { id: "compaction" };
2679
2688
  const LEASE_MS = 3e4;
2680
- const WINDOW_MS$1 = {
2681
- log: 6e4,
2682
- memory: 15e3
2683
- };
2689
+ const WINDOW_MS$1 = 6e4;
2684
2690
  const LOG_PAGE = 200;
2685
2691
  const GROUPS = {
2686
2692
  commit: 1,
@@ -2688,7 +2694,6 @@ const GROUPS = {
2688
2694
  flush: 256
2689
2695
  };
2690
2696
  const ATTEMPTS = 5;
2691
- const MAX_BUFFERED = 1e4;
2692
2697
  const log = defineTable({
2693
2698
  id: {
2694
2699
  type: "string",
@@ -2789,8 +2794,8 @@ const rewrite$1 = (change, key, current) => {
2789
2794
  previous: current
2790
2795
  };
2791
2796
  };
2792
- const compile = async (adapter, changes, shard) => {
2793
- const keys = changes.map(({ model, key }) => [...key.slice(0, -1), shard(model)]);
2797
+ const compile = async (adapter, changes) => {
2798
+ const keys = changes.map(({ key }) => [...key.slice(0, -1), 0]);
2794
2799
  const reads = /* @__PURE__ */ new Map();
2795
2800
  changes.forEach((change, at) => {
2796
2801
  if (rewrites(change)) reads.set(change.model, [...reads.get(change.model) ?? [], at]);
@@ -2836,7 +2841,7 @@ const encode = async (changes) => {
2836
2841
  const zipped = await deflate(json);
2837
2842
  return zipped !== void 0 && zipped.length < json.length ? `z${zipped}` : json;
2838
2843
  };
2839
- const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "log", windowMs = WINDOW_MS$1[mode] }, now }) => {
2844
+ const createAggregateBatches = ({ engine, adapter, schema, batching: { windowMs = WINDOW_MS$1 }, now }) => {
2840
2845
  if (!schema.models.has(LEASE_TABLE)) throw new DatabaseTransactionError("An engine that batches aggregates needs aggregateBatchingModule in its schema.");
2841
2846
  const holder = crypto.randomUUID().replaceAll("-", "");
2842
2847
  const table = (name) => schema.models.get(name).table;
@@ -2863,61 +2868,6 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
2863
2868
  }
2864
2869
  return changes;
2865
2870
  };
2866
- const apply = async (changes, shard) => {
2867
- let rest = changes;
2868
- let ops = [];
2869
- let read = true;
2870
- for (let failures = 0; rest.length > 0 && failures < ATTEMPTS;) try {
2871
- if (read) ops = await compile(adapter, rest, shard);
2872
- let size = rest.length;
2873
- const chunk = () => ops.slice(0, size).filter(isOp);
2874
- while (size > 1 && !adapter.fits(chunk())) size = Math.ceil(size / 2);
2875
- let result = { ok: true };
2876
- if (chunk().length > 0) result = await adapter.write(chunk()).catch((error) => {
2877
- warn("flush")(error);
2878
- return { ok: true };
2879
- });
2880
- read = !result.ok && !("retry" in result);
2881
- if (result.ok) {
2882
- rest = rest.slice(size);
2883
- ops = ops.slice(size);
2884
- failures = 0;
2885
- } else {
2886
- failures += 1;
2887
- if (!read) await sleep(failures);
2888
- }
2889
- } catch (error) {
2890
- warn("flush")(error);
2891
- return rest;
2892
- }
2893
- return rest;
2894
- };
2895
- let buffer = /* @__PURE__ */ new Map();
2896
- let timer;
2897
- let flushing;
2898
- const shards = /* @__PURE__ */ new Map();
2899
- const processShard = (model) => {
2900
- if (!shards.has(model)) shards.set(model, Math.floor(Math.random() * shapeOf(model).shards));
2901
- return shards.get(model);
2902
- };
2903
- const schedule = () => {
2904
- if (timer !== void 0 || buffer.size === 0) return;
2905
- timer = setTimeout(() => {
2906
- timer = void 0;
2907
- flushBuffer();
2908
- }, windowMs);
2909
- timer.unref?.();
2910
- };
2911
- const flushBuffer = () => flushing ??= (async () => {
2912
- clearTimeout(timer);
2913
- timer = void 0;
2914
- const pending = [...buffer.values()];
2915
- buffer = /* @__PURE__ */ new Map();
2916
- mergeAll(buffer, await apply(pending, processShard));
2917
- })().catch(warn("flush")).finally(() => {
2918
- flushing = void 0;
2919
- schedule();
2920
- });
2921
2871
  let nextDue = 0;
2922
2872
  let compacting;
2923
2873
  const known = /* @__PURE__ */ new Map();
@@ -3039,7 +2989,7 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
3039
2989
  more
3040
2990
  };
3041
2991
  };
3042
- const rowsFor = async (changes) => (await compile(adapter, changes, () => 0)).filter(isOp);
2992
+ const rowsFor = async (changes) => (await compile(adapter, changes)).filter(isOp);
3043
2993
  const applyGroup = async (held, logs) => {
3044
2994
  for (let count = logs.length; count > 0; count = Math.floor(count / 2)) {
3045
2995
  const batch = logs.slice(0, count);
@@ -3119,19 +3069,6 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
3119
3069
  }))).then((pages) => pages.some((page) => page.rows.length > 0)).finally(() => {
3120
3070
  checking = void 0;
3121
3071
  });
3122
- const committed = async (changes) => {
3123
- if (mode === "log") {
3124
- if (now() >= nextDue) await compact(false, GROUPS.commit);
3125
- return;
3126
- }
3127
- mergeAll(buffer, changes.values());
3128
- schedule();
3129
- if (buffer.size >= MAX_BUFFERED) await flushBuffer();
3130
- };
3131
- const flush = async () => {
3132
- await flushBuffer();
3133
- if (mode === "log" && await hasPending()) await compact(true, GROUPS.flush);
3134
- };
3135
3072
  return {
3136
3073
  async transaction(fn) {
3137
3074
  let changes = /* @__PURE__ */ new Map();
@@ -3157,7 +3094,7 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
3157
3094
  aggregate
3158
3095
  });
3159
3096
  if (failure) throw failure.error;
3160
- if (mode === "log" && attempt.size > 0) {
3097
+ if (attempt.size > 0) {
3161
3098
  const id = `${now().toString(36).padStart(9, "0")}${holder.slice(0, 8)}${crypto.randomUUID().slice(0, 8)}`;
3162
3099
  const shard = Math.floor(Math.random() * LOG_TABLES.length);
3163
3100
  const merged = mergeAll(/* @__PURE__ */ new Map(), attempt.values());
@@ -3171,24 +3108,18 @@ const createAggregateBatches = ({ engine, adapter, schema, batching: { mode = "l
3171
3108
  open = false;
3172
3109
  }
3173
3110
  });
3174
- if (changes.size > 0) await committed(changes);
3111
+ if (changes.size > 0 && now() >= nextDue) await compact(false, GROUPS.commit);
3175
3112
  return result;
3176
3113
  },
3177
3114
  async beforeRead(name) {
3178
3115
  if (!isBatched(schema.models.get(name))) return;
3179
- if (buffer.size > 0 || flushing !== void 0) await flushBuffer();
3180
- if (mode === "log") {
3181
- if (await hasPending().catch((error) => {
3182
- warn("read")(error);
3183
- return false;
3184
- })) await compact(true, GROUPS.read);
3185
- }
3116
+ if (await hasPending().catch((error) => {
3117
+ warn("read")(error);
3118
+ return false;
3119
+ })) await compact(true, GROUPS.read);
3186
3120
  },
3187
- flush,
3188
- async dispose() {
3189
- clearTimeout(timer);
3190
- timer = void 0;
3191
- await flush();
3121
+ async flush() {
3122
+ if (await hasPending()) await compact(true, GROUPS.flush);
3192
3123
  }
3193
3124
  };
3194
3125
  };
@@ -4147,9 +4078,6 @@ const createDatabaseEngine = ({ batching, now = Date.now, meter, ...options }) =
4147
4078
  flush: async () => {
4148
4079
  await batches?.flush();
4149
4080
  },
4150
- dispose: async () => {
4151
- await batches?.dispose();
4152
- },
4153
4081
  database(module) {
4154
4082
  const name = (model) => module.namespace ? `${module.namespace}_${model}` : model;
4155
4083
  return {
@@ -4217,8 +4145,7 @@ const createEngine = (database, { plugins = [], now = Date.now } = {}) => {
4217
4145
  if (module === void 0) throw new HotUpdaterConfigError(`Plugin "${plugin.id}" is not one of this engine's plugins.`);
4218
4146
  return engine.database(module);
4219
4147
  },
4220
- flush: () => engine.flush(),
4221
- dispose: () => engine.dispose()
4148
+ flush: () => engine.flush()
4222
4149
  };
4223
4150
  };
4224
4151
  //#endregion
@@ -17698,7 +17625,7 @@ const postgres = (config) => {
17698
17625
  };
17699
17626
  //#endregion
17700
17627
  //#region ../../packages/server/dist/version.mjs
17701
- const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.12";
17628
+ const HOT_UPDATER_SERVER_VERSION = "1.2.0-rc.13";
17702
17629
  //#endregion
17703
17630
  //#region ../../packages/server/dist/handlerVersionRoutes.mjs
17704
17631
  const createVersionRouteHandlers = (plugins = []) => {
@@ -18806,8 +18733,12 @@ const pick = (row) => ({
18806
18733
  build_number: row.build_number ?? null,
18807
18734
  submitted_to: row.submitted_to ?? null,
18808
18735
  submitted_at_ms: row.submitted_at_ms ?? null,
18809
- submitted_version: row.submitted_version ?? null
18736
+ submitted_version: row.submitted_version ?? null,
18737
+ store_build: row.store_build === true ? true : null
18810
18738
  });
18739
+ const SANDBOX_CHANNEL_PREFIX = "sandbox-";
18740
+ const OLDER_BUILDS_PAGE = 20;
18741
+ const MARK_ATTEMPTS = 8;
18811
18742
  const SUBMISSION_DESTINATIONS = {
18812
18743
  android: "play-internal",
18813
18744
  ios: "testflight"
@@ -18873,7 +18804,8 @@ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
18873
18804
  build_number: registration.buildNumber ?? null,
18874
18805
  submitted_to: null,
18875
18806
  submitted_at_ms: null,
18876
- submitted_version: null
18807
+ submitted_version: null,
18808
+ store_build: null
18877
18809
  };
18878
18810
  for (let attempt = 1;; attempt += 1) {
18879
18811
  await insertChannel(db, {
@@ -18911,21 +18843,45 @@ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
18911
18843
  const scopeKey = nativeBuildScopeKey(scope);
18912
18844
  const buildKey = options?.buildKey;
18913
18845
  if (buildKey !== void 0 && !BUILD_KEY.test(buildKey)) throw new NativeBuildInputError("Invalid native build key: it must be nbk1: and a SHA-256.");
18914
- const { rows } = buildKey === void 0 ? await db.findMany("native_builds", {
18915
- index: "byScope",
18916
- where: { scope_key: scopeKey },
18917
- order: "desc",
18918
- limit: 1
18919
- }) : await db.findMany("native_builds", {
18920
- index: "byBuildKey",
18921
- where: {
18846
+ if (options?.storeBuild) {
18847
+ const marked = await db.findOne("native_builds", {
18922
18848
  scope_key: scopeKey,
18923
- build_key: buildKey
18924
- },
18925
- order: "desc",
18926
- limit: 1
18927
- });
18928
- return rows[0] === void 0 ? null : pick(rows[0]);
18849
+ store_build: true
18850
+ });
18851
+ return marked === null ? null : pick(marked);
18852
+ }
18853
+ const read = (limit, before) => {
18854
+ const page = {
18855
+ order: "desc",
18856
+ limit,
18857
+ ...before === void 0 ? {} : { range: { lt: before } }
18858
+ };
18859
+ return buildKey === void 0 ? db.findMany("native_builds", {
18860
+ index: "byScope",
18861
+ where: { scope_key: scopeKey },
18862
+ ...page
18863
+ }) : db.findMany("native_builds", {
18864
+ index: "byBuildKey",
18865
+ where: {
18866
+ scope_key: scopeKey,
18867
+ build_key: buildKey
18868
+ },
18869
+ ...page
18870
+ });
18871
+ };
18872
+ const { rows } = await read(1);
18873
+ const newest = rows[0];
18874
+ if (newest === void 0) return null;
18875
+ const bundleId = options?.builtInOlderThan;
18876
+ if (bundleId === void 0 || newest.min_bundle_id < bundleId) return pick(newest);
18877
+ for (let before = newest.id;;) {
18878
+ const { rows: older } = await read(OLDER_BUILDS_PAGE, before);
18879
+ const usable = older.find(({ min_bundle_id }) => min_bundle_id < bundleId);
18880
+ if (usable !== void 0) return pick(usable);
18881
+ const last = older.at(-1);
18882
+ if (older.length < OLDER_BUILDS_PAGE || last === void 0) return null;
18883
+ before = last.id;
18884
+ }
18929
18885
  },
18930
18886
  async recordNativeBuildSubmission(id, { destination, version }) {
18931
18887
  if (version.trim().length === 0 || version.length > SUBMITTED_VERSION_MAX_LENGTH) throw new NativeBuildInputError(`Invalid native build submission: version must be the store's label of the build, up to ${SUBMITTED_VERSION_MAX_LENGTH} characters.`);
@@ -18946,6 +18902,49 @@ const createNativeBuildApi = (db, { now = Date.now } = {}) => ({
18946
18902
  };
18947
18903
  });
18948
18904
  },
18905
+ async markNativeBuild(id, { storeBuild }) {
18906
+ for (let attempt = 1;; attempt += 1) try {
18907
+ return await db.transaction(async (tx) => {
18908
+ const row = await tx.findOne("native_builds", { id });
18909
+ if (row === null) return null;
18910
+ const build = pick(row);
18911
+ if (!storeBuild) {
18912
+ if (build.store_build !== null) tx.update("native_builds", row, { store_build: null });
18913
+ return {
18914
+ nativeBuild: {
18915
+ ...build,
18916
+ store_build: null
18917
+ },
18918
+ previous: null
18919
+ };
18920
+ }
18921
+ if (decodeChannelKey(build.channel_id.slice(8)).startsWith(SANDBOX_CHANNEL_PREFIX)) throw new NativeBuildInputError("A build on a sandbox channel can't be the store build.");
18922
+ if (build.store_build !== null) return {
18923
+ nativeBuild: build,
18924
+ previous: null
18925
+ };
18926
+ const holder = await tx.findOne("native_builds", {
18927
+ scope_key: build.scope_key,
18928
+ store_build: true
18929
+ });
18930
+ if (holder !== null) tx.update("native_builds", holder, { store_build: null });
18931
+ tx.update("native_builds", row, { store_build: true });
18932
+ return {
18933
+ nativeBuild: {
18934
+ ...build,
18935
+ store_build: true
18936
+ },
18937
+ previous: holder === null ? null : {
18938
+ ...pick(holder),
18939
+ store_build: null
18940
+ }
18941
+ };
18942
+ });
18943
+ } catch (error) {
18944
+ if (attempt < MARK_ATTEMPTS && error instanceof DatabaseConstraintError && error.reason === "unique") continue;
18945
+ throw error;
18946
+ }
18947
+ },
18949
18948
  async deleteNativeBuild(id) {
18950
18949
  return db.transaction(async (tx) => {
18951
18950
  const row = await tx.findOne("native_builds", { id });
@@ -19972,7 +19971,13 @@ const createAdminRouteHandlers = () => ({
19972
19971
  const url = new URL(request.url);
19973
19972
  const scope = nativeBuildScopeOf(url);
19974
19973
  const buildKey = url.searchParams.get("buildKey");
19975
- return json$2({ data: await refusingInvalid(() => buildKey === null ? core.findNativeBuild(scope) : core.findNativeBuild(scope, { buildKey })) });
19974
+ const builtInOlderThan = url.searchParams.get("builtInOlderThan");
19975
+ const options = {
19976
+ ...buildKey === null ? {} : { buildKey },
19977
+ ...url.searchParams.get("storeBuild") === "true" ? { storeBuild: true } : {},
19978
+ ...builtInOlderThan === null ? {} : { builtInOlderThan }
19979
+ };
19980
+ return json$2({ data: await refusingInvalid(() => Object.keys(options).length === 0 ? core.findNativeBuild(scope) : core.findNativeBuild(scope, options)) });
19976
19981
  }),
19977
19982
  getNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.getNativeBuild(requireRouteParam(params, "id")) })),
19978
19983
  listNativeBuilds: onCore(async (core, request) => {
@@ -19993,6 +19998,12 @@ const createAdminRouteHandlers = () => ({
19993
19998
  };
19994
19999
  return json$2({ data: await refusingInvalid(() => core.recordNativeBuildSubmission(requireRouteParam(params, "id"), submission)) });
19995
20000
  }),
20001
+ markNativeBuild: onCore(async (core, request, params) => {
20002
+ const input = await body(request);
20003
+ if (!isRecord$5(input) || typeof input.storeBuild !== "boolean") throw new HandlerBadRequestError("A native build's store build mark needs storeBuild, true or false.");
20004
+ const mark = { storeBuild: input.storeBuild };
20005
+ return json$2({ data: await refusingInvalid(() => core.markNativeBuild(requireRouteParam(params, "id"), mark)) });
20006
+ }),
19996
20007
  deleteNativeBuild: onCore(async (core, _request, params) => json$2({ data: await core.deleteNativeBuild(requireRouteParam(params, "id")) })),
19997
20008
  deleteBundles: onCore(async (core, request) => {
19998
20009
  const { ids } = await body(request);
@@ -20137,6 +20148,11 @@ const ADMIN_ROUTES = [
20137
20148
  path: "/native-builds/:id/submission",
20138
20149
  handler: "recordNativeBuildSubmission"
20139
20150
  },
20151
+ {
20152
+ method: "POST",
20153
+ path: "/native-builds/:id/store-build",
20154
+ handler: "markNativeBuild"
20155
+ },
20140
20156
  {
20141
20157
  method: "DELETE",
20142
20158
  path: "/native-builds/:id",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hot-updater/vercel",
3
3
  "type": "module",
4
- "version": "1.2.0-rc.12",
4
+ "version": "1.2.0-rc.13",
5
5
  "description": "React Native OTA solution for self-hosted",
6
6
  "main": "dist/index.cjs",
7
7
  "module": "dist/index.mjs",
@@ -41,11 +41,11 @@
41
41
  "@vercel/cli-auth": "0.3.10",
42
42
  "@vercel/cli-config": "0.3.1",
43
43
  "pg": "8.13.1",
44
- "@hot-updater/cli-tools": "1.2.0-rc.12",
45
- "@hot-updater/plugin-core": "1.2.0-rc.12",
46
- "@hot-updater/postgres": "1.2.0-rc.12",
47
- "@hot-updater/server": "1.2.0-rc.12",
48
- "@hot-updater/protocol": "1.2.0-rc.12"
44
+ "@hot-updater/cli-tools": "1.2.0-rc.13",
45
+ "@hot-updater/protocol": "1.2.0-rc.13",
46
+ "@hot-updater/postgres": "1.2.0-rc.13",
47
+ "@hot-updater/server": "1.2.0-rc.13",
48
+ "@hot-updater/plugin-core": "1.2.0-rc.13"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^20",
@@ -54,7 +54,7 @@
54
54
  "execa": "9.5.2",
55
55
  "kysely": "0.28.17",
56
56
  "vitest": "4.1.4",
57
- "@hot-updater/test-utils": "1.2.0-rc.12"
57
+ "@hot-updater/test-utils": "1.2.0-rc.13"
58
58
  },
59
59
  "scripts": {
60
60
  "build": "tsdown",