@hot-updater/plugin-core 0.21.6 → 0.21.8

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.
Files changed (46) hide show
  1. package/dist/_virtual/rolldown_runtime.cjs +25 -0
  2. package/dist/calculatePagination.cjs +27 -0
  3. package/dist/calculatePagination.d.cts +17 -0
  4. package/dist/calculatePagination.d.ts +17 -0
  5. package/dist/calculatePagination.js +26 -0
  6. package/dist/compressionFormat.cjs +59 -0
  7. package/dist/compressionFormat.d.cts +33 -0
  8. package/dist/compressionFormat.d.ts +33 -0
  9. package/dist/compressionFormat.js +54 -0
  10. package/dist/createBlobDatabasePlugin.cjs +312 -0
  11. package/dist/createBlobDatabasePlugin.d.cts +38 -0
  12. package/dist/createBlobDatabasePlugin.d.ts +38 -0
  13. package/dist/createBlobDatabasePlugin.js +309 -0
  14. package/dist/createDatabasePlugin.cjs +97 -0
  15. package/dist/createDatabasePlugin.d.cts +68 -0
  16. package/dist/createDatabasePlugin.d.ts +68 -0
  17. package/dist/createDatabasePlugin.js +95 -0
  18. package/dist/createStorageKeyBuilder.cjs +8 -0
  19. package/dist/createStorageKeyBuilder.d.cts +4 -0
  20. package/dist/createStorageKeyBuilder.d.ts +4 -0
  21. package/dist/createStorageKeyBuilder.js +7 -0
  22. package/dist/filterCompatibleAppVersions.cjs +17 -0
  23. package/dist/filterCompatibleAppVersions.d.cts +12 -0
  24. package/dist/filterCompatibleAppVersions.d.ts +12 -0
  25. package/dist/filterCompatibleAppVersions.js +17 -0
  26. package/dist/generateMinBundleId.cjs +11 -0
  27. package/dist/generateMinBundleId.d.cts +4 -0
  28. package/dist/generateMinBundleId.d.ts +4 -0
  29. package/dist/generateMinBundleId.js +10 -0
  30. package/dist/index.cjs +21 -27391
  31. package/dist/index.d.cts +12 -560
  32. package/dist/index.d.ts +12 -560
  33. package/dist/index.js +11 -27351
  34. package/dist/parseStorageUri.cjs +35 -0
  35. package/dist/parseStorageUri.d.cts +24 -0
  36. package/dist/parseStorageUri.d.ts +24 -0
  37. package/dist/parseStorageUri.js +34 -0
  38. package/dist/semverSatisfies.cjs +13 -0
  39. package/dist/semverSatisfies.d.cts +4 -0
  40. package/dist/semverSatisfies.d.ts +4 -0
  41. package/dist/semverSatisfies.js +11 -0
  42. package/dist/types/index.d.cts +201 -0
  43. package/dist/types/index.d.ts +201 -0
  44. package/dist/types/utils.d.cts +12 -0
  45. package/dist/types/utils.d.ts +12 -0
  46. package/package.json +5 -14
@@ -0,0 +1,38 @@
1
+ import { BasePluginArgs, DatabasePlugin, DatabasePluginHooks } from "./types/index.js";
2
+
3
+ //#region src/createBlobDatabasePlugin.d.ts
4
+
5
+ /**
6
+ *
7
+ * @param name - The name of the database plugin
8
+ * @param listObjects - Function to list objects in the storage
9
+ * @param loadObject - Function to load an JSON object from the storage
10
+ * @param uploadObject - Function to upload an JSON object to the storage
11
+ * @param deleteObject - Function to delete an object from the storage
12
+ * @param invalidatePaths - Function to invalidate paths in the CDN
13
+ * @param hooks - Optional hooks for additional functionality - see createDatabasePlugin
14
+ * @returns
15
+ */
16
+ declare const createBlobDatabasePlugin: <TContext = object>({
17
+ name,
18
+ getContext,
19
+ listObjects,
20
+ loadObject,
21
+ uploadObject,
22
+ deleteObject,
23
+ invalidatePaths,
24
+ hooks,
25
+ apiBasePath
26
+ }: {
27
+ name: string;
28
+ getContext: () => TContext;
29
+ listObjects: (context: TContext, prefix: string) => Promise<string[]>;
30
+ loadObject: <T>(context: TContext, key: string) => Promise<T | null>;
31
+ uploadObject: <T>(context: TContext, key: string, data: T) => Promise<void>;
32
+ deleteObject: (context: TContext, key: string) => Promise<void>;
33
+ invalidatePaths: (context: TContext, paths: string[]) => Promise<void>;
34
+ hooks?: DatabasePluginHooks;
35
+ apiBasePath: string;
36
+ }) => (options: BasePluginArgs) => DatabasePlugin;
37
+ //#endregion
38
+ export { createBlobDatabasePlugin };
@@ -0,0 +1,309 @@
1
+ import { calculatePagination } from "./calculatePagination.js";
2
+ import { createDatabasePlugin } from "./createDatabasePlugin.js";
3
+ import { orderBy } from "es-toolkit";
4
+ import semver from "semver";
5
+
6
+ //#region src/createBlobDatabasePlugin.ts
7
+ function removeBundleInternalKeys(bundle) {
8
+ const { _updateJsonKey, _oldUpdateJsonKey,...pureBundle } = bundle;
9
+ return pureBundle;
10
+ }
11
+ function normalizeTargetAppVersion(version) {
12
+ if (!version) return null;
13
+ let normalized = version.replace(/\s+/g, " ").trim();
14
+ normalized = normalized.replace(/([><=~^]+)\s+(\d)/g, (_match, operator, digit) => `${operator}${digit}`);
15
+ return normalized;
16
+ }
17
+ function isExactVersion(version) {
18
+ if (!version) return false;
19
+ const normalized = normalizeTargetAppVersion(version);
20
+ if (!normalized) return false;
21
+ return semver.valid(normalized) !== null;
22
+ }
23
+ /**
24
+ * Get all normalized semver versions for a version string.
25
+ * This handles the case where clients may request with different normalized forms.
26
+ *
27
+ * Examples:
28
+ * - "1.0.0" generates ["1.0.0", "1.0", "1"]
29
+ * - "2.1.0" generates ["2.1.0", "2.1"]
30
+ * - "1.2.3" generates ["1.2.3"]
31
+ */
32
+ function getSemverNormalizedVersions(version) {
33
+ const normalized = normalizeTargetAppVersion(version) || version;
34
+ const coerced = semver.coerce(normalized);
35
+ if (!coerced) return [normalized];
36
+ const versions = /* @__PURE__ */ new Set();
37
+ versions.add(coerced.version);
38
+ if (coerced.patch === 0) versions.add(`${coerced.major}.${coerced.minor}`);
39
+ if (coerced.minor === 0 && coerced.patch === 0) versions.add(`${coerced.major}`);
40
+ return Array.from(versions);
41
+ }
42
+ /**
43
+ *
44
+ * @param name - The name of the database plugin
45
+ * @param listObjects - Function to list objects in the storage
46
+ * @param loadObject - Function to load an JSON object from the storage
47
+ * @param uploadObject - Function to upload an JSON object to the storage
48
+ * @param deleteObject - Function to delete an object from the storage
49
+ * @param invalidatePaths - Function to invalidate paths in the CDN
50
+ * @param hooks - Optional hooks for additional functionality - see createDatabasePlugin
51
+ * @returns
52
+ */
53
+ const createBlobDatabasePlugin = ({ name, getContext, listObjects, loadObject, uploadObject, deleteObject, invalidatePaths, hooks, apiBasePath }) => {
54
+ const bundlesMap = /* @__PURE__ */ new Map();
55
+ const pendingBundlesMap = /* @__PURE__ */ new Map();
56
+ const PLATFORMS = ["ios", "android"];
57
+ async function reloadBundles(context) {
58
+ bundlesMap.clear();
59
+ const platformPromises = PLATFORMS.map(async (platform) => {
60
+ const filePromises = (await listUpdateJsonKeys(context, platform)).map(async (key) => {
61
+ return (await loadObject(context, key) ?? []).map((bundle) => ({
62
+ ...bundle,
63
+ _updateJsonKey: key
64
+ }));
65
+ });
66
+ return (await Promise.all(filePromises)).flat();
67
+ });
68
+ const allBundles = (await Promise.all(platformPromises)).flat();
69
+ for (const bundle of allBundles) bundlesMap.set(bundle.id, bundle);
70
+ for (const [id, bundle] of pendingBundlesMap.entries()) bundlesMap.set(id, bundle);
71
+ return orderBy(allBundles, [(v) => v.id], ["desc"]);
72
+ }
73
+ /**
74
+ * Updates target-app-versions.json for each channel on the given platform.
75
+ * Returns true if the file was updated, false if no changes were made.
76
+ */
77
+ async function updateTargetVersionsForPlatform(context, platform) {
78
+ const pattern = /* @__PURE__ */ new RegExp(`^[^/]+/${platform}/[^/]+/update\\.json$`);
79
+ const keysByChannel = (await listObjects(context, "")).filter((key) => pattern.test(key)).reduce((acc, key) => {
80
+ const channel = key.split("/")[0];
81
+ acc[channel] = acc[channel] || [];
82
+ acc[channel].push(key);
83
+ return acc;
84
+ }, {});
85
+ const updatedTargetFiles = /* @__PURE__ */ new Set();
86
+ for (const channel of Object.keys(keysByChannel)) {
87
+ const updateKeys = keysByChannel[channel];
88
+ const targetKey = `${channel}/${platform}/target-app-versions.json`;
89
+ const currentVersions = updateKeys.map((key) => key.split("/")[2]);
90
+ const oldTargetVersions = await loadObject(context, targetKey) ?? [];
91
+ const newTargetVersions = oldTargetVersions.filter((v) => currentVersions.includes(v));
92
+ for (const v of currentVersions) if (!newTargetVersions.includes(v)) newTargetVersions.push(v);
93
+ if (JSON.stringify(oldTargetVersions) !== JSON.stringify(newTargetVersions)) {
94
+ await uploadObject(context, targetKey, newTargetVersions);
95
+ updatedTargetFiles.add(`/${targetKey}`);
96
+ }
97
+ }
98
+ return updatedTargetFiles;
99
+ }
100
+ /**
101
+ * Lists update.json keys for a given platform.
102
+ *
103
+ * - If a channel is provided, only that channel's update.json files are listed.
104
+ * - Otherwise, all channels for the given platform are returned.
105
+ */
106
+ async function listUpdateJsonKeys(context, platform, channel) {
107
+ const prefix = channel ? platform ? `${channel}/${platform}/` : `${channel}/` : "";
108
+ const pattern = channel ? platform ? /* @__PURE__ */ new RegExp(`^${channel}/${platform}/[^/]+/update\\.json$`) : /* @__PURE__ */ new RegExp(`^${channel}/[^/]+/[^/]+/update\\.json$`) : platform ? /* @__PURE__ */ new RegExp(`^[^/]+/${platform}/[^/]+/update\\.json$`) : /^[^/]+\/[^/]+\/[^/]+\/update\.json$/;
109
+ return listObjects(context, prefix).then((keys) => keys.filter((key) => pattern.test(key)));
110
+ }
111
+ return createDatabasePlugin(name, {
112
+ getContext,
113
+ async getBundleById(context, bundleId) {
114
+ const pendingBundle = pendingBundlesMap.get(bundleId);
115
+ if (pendingBundle) return removeBundleInternalKeys(pendingBundle);
116
+ const bundle = bundlesMap.get(bundleId);
117
+ if (bundle) return removeBundleInternalKeys(bundle);
118
+ return (await reloadBundles(context)).find((bundle$1) => bundle$1.id === bundleId) ?? null;
119
+ },
120
+ async getBundles(context, options) {
121
+ let allBundles = await reloadBundles(context);
122
+ const { where, limit, offset } = options;
123
+ if (where) allBundles = allBundles.filter((bundle) => {
124
+ return Object.entries(where).every(([key, value]) => value === void 0 || value === null || bundle[key] === value);
125
+ });
126
+ const total = allBundles.length;
127
+ let paginatedData = allBundles.map(removeBundleInternalKeys);
128
+ if (offset > 0) paginatedData = paginatedData.slice(offset);
129
+ if (limit) paginatedData = paginatedData.slice(0, limit);
130
+ return {
131
+ data: paginatedData,
132
+ pagination: calculatePagination(total, {
133
+ limit,
134
+ offset
135
+ })
136
+ };
137
+ },
138
+ async getChannels(context) {
139
+ const total = (await reloadBundles(context)).length;
140
+ const result = await this.getBundles(context, {
141
+ limit: total,
142
+ offset: 0
143
+ });
144
+ return [...new Set(result.data.map((bundle) => bundle.channel))];
145
+ },
146
+ async commitBundle(context, { changedSets }) {
147
+ if (changedSets.length === 0) return;
148
+ const changedBundlesByKey = {};
149
+ const removalsByKey = {};
150
+ const pathsToInvalidate = /* @__PURE__ */ new Set();
151
+ let isTargetAppVersionChanged = false;
152
+ for (const { operation, data } of changedSets) {
153
+ if (data.targetAppVersion !== void 0) isTargetAppVersionChanged = true;
154
+ if (operation === "insert") {
155
+ const target = normalizeTargetAppVersion(data.targetAppVersion) ?? data.fingerprintHash;
156
+ if (!target) throw new Error("target not found");
157
+ const key = `${data.channel}/${data.platform}/${target}/update.json`;
158
+ const bundleWithKey = {
159
+ ...data,
160
+ _updateJsonKey: key
161
+ };
162
+ bundlesMap.set(data.id, bundleWithKey);
163
+ pendingBundlesMap.set(data.id, bundleWithKey);
164
+ changedBundlesByKey[key] = changedBundlesByKey[key] || [];
165
+ changedBundlesByKey[key].push(removeBundleInternalKeys(bundleWithKey));
166
+ pathsToInvalidate.add(`/${key}`);
167
+ if (data.fingerprintHash) pathsToInvalidate.add(`${apiBasePath}/fingerprint/${data.platform}/${data.fingerprintHash}/${data.channel}/*`);
168
+ else if (data.targetAppVersion) if (!isExactVersion(data.targetAppVersion)) pathsToInvalidate.add(`${apiBasePath}/app-version/${data.platform}/*`);
169
+ else {
170
+ const normalizedVersions = getSemverNormalizedVersions(data.targetAppVersion);
171
+ for (const version of normalizedVersions) pathsToInvalidate.add(`${apiBasePath}/app-version/${data.platform}/${version}/${data.channel}/*`);
172
+ }
173
+ continue;
174
+ }
175
+ if (operation === "delete") {
176
+ let bundle$1 = pendingBundlesMap.get(data.id);
177
+ if (!bundle$1) bundle$1 = bundlesMap.get(data.id);
178
+ if (!bundle$1) throw new Error("Bundle to delete not found");
179
+ bundlesMap.delete(data.id);
180
+ pendingBundlesMap.delete(data.id);
181
+ const key = bundle$1._updateJsonKey;
182
+ removalsByKey[key] = removalsByKey[key] || [];
183
+ removalsByKey[key].push(bundle$1.id);
184
+ pathsToInvalidate.add(`/${key}`);
185
+ if (bundle$1.fingerprintHash) pathsToInvalidate.add(`${apiBasePath}/fingerprint/${bundle$1.platform}/${bundle$1.fingerprintHash}/${bundle$1.channel}/*`);
186
+ else if (bundle$1.targetAppVersion) if (!isExactVersion(bundle$1.targetAppVersion)) pathsToInvalidate.add(`${apiBasePath}/app-version/${bundle$1.platform}/*`);
187
+ else {
188
+ const normalizedVersions = getSemverNormalizedVersions(bundle$1.targetAppVersion);
189
+ for (const version of normalizedVersions) pathsToInvalidate.add(`${apiBasePath}/app-version/${bundle$1.platform}/${version}/${bundle$1.channel}/*`);
190
+ }
191
+ continue;
192
+ }
193
+ let bundle = pendingBundlesMap.get(data.id);
194
+ if (!bundle) bundle = bundlesMap.get(data.id);
195
+ if (!bundle) throw new Error("targetBundleId not found");
196
+ if (operation === "update") {
197
+ const newChannel = data.channel !== void 0 ? data.channel : bundle.channel;
198
+ const newPlatform = data.platform !== void 0 ? data.platform : bundle.platform;
199
+ const target = data.fingerprintHash ?? bundle.fingerprintHash ?? normalizeTargetAppVersion(data.targetAppVersion) ?? normalizeTargetAppVersion(bundle.targetAppVersion);
200
+ if (!target) throw new Error("target not found");
201
+ const newKey = `${newChannel}/${newPlatform}/${target}/update.json`;
202
+ if (newKey !== bundle._updateJsonKey) {
203
+ const oldKey = bundle._updateJsonKey;
204
+ removalsByKey[oldKey] = removalsByKey[oldKey] || [];
205
+ removalsByKey[oldKey].push(bundle.id);
206
+ changedBundlesByKey[newKey] = changedBundlesByKey[newKey] || [];
207
+ const updatedBundle$1 = {
208
+ ...bundle,
209
+ ...data
210
+ };
211
+ updatedBundle$1._oldUpdateJsonKey = oldKey;
212
+ updatedBundle$1._updateJsonKey = newKey;
213
+ bundlesMap.set(data.id, updatedBundle$1);
214
+ pendingBundlesMap.set(data.id, updatedBundle$1);
215
+ changedBundlesByKey[newKey].push(removeBundleInternalKeys(updatedBundle$1));
216
+ pathsToInvalidate.add(`/${oldKey}`);
217
+ pathsToInvalidate.add(`/${newKey}`);
218
+ const oldChannel = bundle.channel;
219
+ const newChannel$1 = data.channel;
220
+ if (oldChannel !== newChannel$1) {
221
+ pathsToInvalidate.add(`/${oldChannel}/${bundle.platform}/target-app-versions.json`);
222
+ pathsToInvalidate.add(`/${newChannel$1}/${bundle.platform}/target-app-versions.json`);
223
+ if (bundle.fingerprintHash) {
224
+ pathsToInvalidate.add(`${apiBasePath}/fingerprint/${bundle.platform}/${bundle.fingerprintHash}/${oldChannel}/*`);
225
+ pathsToInvalidate.add(`${apiBasePath}/fingerprint/${bundle.platform}/${bundle.fingerprintHash}/${newChannel$1}/*`);
226
+ }
227
+ if (bundle.targetAppVersion) if (!isExactVersion(bundle.targetAppVersion)) pathsToInvalidate.add(`${apiBasePath}/app-version/${bundle.platform}/*`);
228
+ else {
229
+ const normalizedVersions = getSemverNormalizedVersions(bundle.targetAppVersion);
230
+ for (const version of normalizedVersions) {
231
+ pathsToInvalidate.add(`${apiBasePath}/app-version/${bundle.platform}/${version}/${oldChannel}/*`);
232
+ pathsToInvalidate.add(`${apiBasePath}/app-version/${bundle.platform}/${version}/${newChannel$1}/*`);
233
+ }
234
+ }
235
+ }
236
+ if (updatedBundle$1.fingerprintHash) pathsToInvalidate.add(`${apiBasePath}/fingerprint/${bundle.platform}/${updatedBundle$1.fingerprintHash}/${updatedBundle$1.channel}/*`);
237
+ else if (updatedBundle$1.targetAppVersion) {
238
+ if (!isExactVersion(updatedBundle$1.targetAppVersion)) pathsToInvalidate.add(`${apiBasePath}/app-version/${updatedBundle$1.platform}/*`);
239
+ else {
240
+ const normalizedVersions = getSemverNormalizedVersions(updatedBundle$1.targetAppVersion);
241
+ for (const version of normalizedVersions) pathsToInvalidate.add(`${apiBasePath}/app-version/${updatedBundle$1.platform}/${version}/${updatedBundle$1.channel}/*`);
242
+ }
243
+ if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle$1.targetAppVersion) if (!isExactVersion(bundle.targetAppVersion)) pathsToInvalidate.add(`${apiBasePath}/app-version/${bundle.platform}/*`);
244
+ else {
245
+ const oldNormalizedVersions = getSemverNormalizedVersions(bundle.targetAppVersion);
246
+ for (const version of oldNormalizedVersions) pathsToInvalidate.add(`${apiBasePath}/app-version/${bundle.platform}/${version}/${bundle.channel}/*`);
247
+ }
248
+ }
249
+ continue;
250
+ }
251
+ const currentKey = bundle._updateJsonKey;
252
+ const updatedBundle = {
253
+ ...bundle,
254
+ ...data
255
+ };
256
+ bundlesMap.set(data.id, updatedBundle);
257
+ pendingBundlesMap.set(data.id, updatedBundle);
258
+ changedBundlesByKey[currentKey] = changedBundlesByKey[currentKey] || [];
259
+ changedBundlesByKey[currentKey].push(removeBundleInternalKeys(updatedBundle));
260
+ pathsToInvalidate.add(`/${currentKey}`);
261
+ if (updatedBundle.fingerprintHash) pathsToInvalidate.add(`${apiBasePath}/fingerprint/${updatedBundle.platform}/${updatedBundle.fingerprintHash}/${updatedBundle.channel}/*`);
262
+ else if (updatedBundle.targetAppVersion) {
263
+ if (!isExactVersion(updatedBundle.targetAppVersion)) pathsToInvalidate.add(`${apiBasePath}/app-version/${updatedBundle.platform}/*`);
264
+ else {
265
+ const normalizedVersions = getSemverNormalizedVersions(updatedBundle.targetAppVersion);
266
+ for (const version of normalizedVersions) pathsToInvalidate.add(`${apiBasePath}/app-version/${updatedBundle.platform}/${version}/${updatedBundle.channel}/*`);
267
+ }
268
+ if (bundle.targetAppVersion && bundle.targetAppVersion !== updatedBundle.targetAppVersion) if (!isExactVersion(bundle.targetAppVersion)) pathsToInvalidate.add(`${apiBasePath}/app-version/${bundle.platform}/*`);
269
+ else {
270
+ const oldNormalizedVersions = getSemverNormalizedVersions(bundle.targetAppVersion);
271
+ for (const version of oldNormalizedVersions) pathsToInvalidate.add(`${apiBasePath}/app-version/${bundle.platform}/${version}/${bundle.channel}/*`);
272
+ }
273
+ }
274
+ }
275
+ }
276
+ for (const oldKey of Object.keys(removalsByKey)) await (async () => {
277
+ const updatedBundles = (await loadObject(context, oldKey) ?? []).filter((b) => !removalsByKey[oldKey].includes(b.id));
278
+ updatedBundles.sort((a, b) => b.id.localeCompare(a.id));
279
+ if (updatedBundles.length === 0) await deleteObject(context, oldKey);
280
+ else await uploadObject(context, oldKey, updatedBundles);
281
+ })();
282
+ for (const key of Object.keys(changedBundlesByKey)) await (async () => {
283
+ const currentBundles = await loadObject(context, key) ?? [];
284
+ const pureBundles = changedBundlesByKey[key].map((bundle) => bundle);
285
+ for (const changedBundle of pureBundles) {
286
+ const index = currentBundles.findIndex((b) => b.id === changedBundle.id);
287
+ if (index >= 0) currentBundles[index] = changedBundle;
288
+ else currentBundles.push(changedBundle);
289
+ }
290
+ currentBundles.sort((a, b) => b.id.localeCompare(a.id));
291
+ await uploadObject(context, key, currentBundles);
292
+ })();
293
+ const updatedTargetFilePaths = /* @__PURE__ */ new Set();
294
+ if (isTargetAppVersionChanged) for (const platform of PLATFORMS) {
295
+ const updatedPaths = await updateTargetVersionsForPlatform(context, platform);
296
+ for (const path of updatedPaths) updatedTargetFilePaths.add(path);
297
+ }
298
+ for (const path of updatedTargetFilePaths) pathsToInvalidate.add(path);
299
+ const encondedPaths = /* @__PURE__ */ new Set();
300
+ for (const path of pathsToInvalidate) encondedPaths.add(encodeURI(path));
301
+ await invalidatePaths(context, Array.from(encondedPaths));
302
+ pendingBundlesMap.clear();
303
+ hooks?.onDatabaseUpdated?.();
304
+ }
305
+ }, hooks);
306
+ };
307
+
308
+ //#endregion
309
+ export { createBlobDatabasePlugin };
@@ -0,0 +1,97 @@
1
+ const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs');
2
+ let es_toolkit = require("es-toolkit");
3
+ es_toolkit = require_rolldown_runtime.__toESM(es_toolkit);
4
+
5
+ //#region src/createDatabasePlugin.ts
6
+ /**
7
+ * Creates a database plugin with the given implementation.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const myDatabasePlugin = createDatabasePlugin("myDatabase", {
12
+ * getContext: () => ({
13
+ * // Your database client or connection
14
+ * dbClient: createDbClient()
15
+ * }),
16
+ * async getBundleById(context, bundleId) {
17
+ * // Implementation to get a bundle by ID using context.dbClient
18
+ * return bundle;
19
+ * },
20
+ * async getBundles(context, options) {
21
+ * // Implementation to get bundles with options using context.dbClient
22
+ * return bundles;
23
+ * },
24
+ * async getChannels(context) {
25
+ * // Implementation to get available channels using context.dbClient
26
+ * return channels;
27
+ * },
28
+ * async commitBundle(context, { changedSets }) {
29
+ * // Implementation to commit changed bundles using context.dbClient
30
+ * }
31
+ * });
32
+ * ```
33
+ *
34
+ * @param name - The name of the database plugin
35
+ * @param abstractPlugin - A plugin implementation with context support
36
+ * @param hooks - Optional hooks for plugin lifecycle events
37
+ * @returns A function that creates a database plugin instance
38
+ */
39
+ function createDatabasePlugin(name, abstractPlugin, hooks) {
40
+ const changedMap = /* @__PURE__ */ new Map();
41
+ const markChanged = (operation, data) => {
42
+ changedMap.set(data.id, {
43
+ operation,
44
+ data
45
+ });
46
+ };
47
+ const memoizedContext = (0, es_toolkit.memoize)(abstractPlugin?.getContext ?? (() => {}));
48
+ return (_) => ({
49
+ name,
50
+ async getBundleById(bundleId) {
51
+ const context = memoizedContext();
52
+ return abstractPlugin.getBundleById(context, bundleId);
53
+ },
54
+ async getBundles(options) {
55
+ const context = memoizedContext();
56
+ return abstractPlugin.getBundles(context, options);
57
+ },
58
+ async getChannels() {
59
+ const context = memoizedContext();
60
+ return abstractPlugin.getChannels(context);
61
+ },
62
+ async commitBundle() {
63
+ if (!abstractPlugin.commitBundle) throw new Error("commitBundle is not implemented");
64
+ const context = memoizedContext();
65
+ await abstractPlugin.commitBundle(context, { changedSets: Array.from(changedMap.values()) });
66
+ changedMap.clear();
67
+ hooks?.onDatabaseUpdated?.();
68
+ },
69
+ async updateBundle(targetBundleId, newBundle) {
70
+ const pendingChange = changedMap.get(targetBundleId);
71
+ if (pendingChange) {
72
+ const updatedData = (0, es_toolkit.merge)(pendingChange.data, newBundle);
73
+ changedMap.set(targetBundleId, {
74
+ operation: pendingChange.operation,
75
+ data: updatedData
76
+ });
77
+ return;
78
+ }
79
+ const currentBundle = await this.getBundleById(targetBundleId);
80
+ if (!currentBundle) throw new Error("targetBundleId not found");
81
+ markChanged("update", (0, es_toolkit.merge)(currentBundle, newBundle));
82
+ },
83
+ async appendBundle(inputBundle) {
84
+ markChanged("insert", inputBundle);
85
+ },
86
+ onUnmount: abstractPlugin.onUnmount ? async () => {
87
+ const context = memoizedContext();
88
+ await abstractPlugin.onUnmount?.(context);
89
+ } : void 0,
90
+ async deleteBundle(deleteBundle) {
91
+ markChanged("delete", deleteBundle);
92
+ }
93
+ });
94
+ }
95
+
96
+ //#endregion
97
+ exports.createDatabasePlugin = createDatabasePlugin;
@@ -0,0 +1,68 @@
1
+ import { BasePluginArgs, DatabasePlugin, DatabasePluginHooks, PaginationInfo } from "./types/index.cjs";
2
+ import { Bundle } from "@hot-updater/core";
3
+
4
+ //#region src/createDatabasePlugin.d.ts
5
+ interface BaseDatabaseUtils {
6
+ cwd: string;
7
+ }
8
+ interface AbstractDatabasePlugin<TContext = object> {
9
+ getContext?: () => TContext;
10
+ getBundleById: (context: TContext, bundleId: string) => Promise<Bundle | null>;
11
+ getBundles: (context: TContext, options: {
12
+ where?: {
13
+ channel?: string;
14
+ platform?: string;
15
+ };
16
+ limit: number;
17
+ offset: number;
18
+ }) => Promise<{
19
+ data: Bundle[];
20
+ pagination: PaginationInfo;
21
+ }>;
22
+ getChannels: (context: TContext) => Promise<string[]>;
23
+ onUnmount?: (context: TContext) => void;
24
+ commitBundle: (context: TContext, {
25
+ changedSets
26
+ }: {
27
+ changedSets: {
28
+ operation: "insert" | "update" | "delete";
29
+ data: Bundle;
30
+ }[];
31
+ }) => Promise<void>;
32
+ }
33
+ /**
34
+ * Creates a database plugin with the given implementation.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const myDatabasePlugin = createDatabasePlugin("myDatabase", {
39
+ * getContext: () => ({
40
+ * // Your database client or connection
41
+ * dbClient: createDbClient()
42
+ * }),
43
+ * async getBundleById(context, bundleId) {
44
+ * // Implementation to get a bundle by ID using context.dbClient
45
+ * return bundle;
46
+ * },
47
+ * async getBundles(context, options) {
48
+ * // Implementation to get bundles with options using context.dbClient
49
+ * return bundles;
50
+ * },
51
+ * async getChannels(context) {
52
+ * // Implementation to get available channels using context.dbClient
53
+ * return channels;
54
+ * },
55
+ * async commitBundle(context, { changedSets }) {
56
+ * // Implementation to commit changed bundles using context.dbClient
57
+ * }
58
+ * });
59
+ * ```
60
+ *
61
+ * @param name - The name of the database plugin
62
+ * @param abstractPlugin - A plugin implementation with context support
63
+ * @param hooks - Optional hooks for plugin lifecycle events
64
+ * @returns A function that creates a database plugin instance
65
+ */
66
+ declare function createDatabasePlugin<TContext = object>(name: string, abstractPlugin: AbstractDatabasePlugin<TContext>, hooks?: DatabasePluginHooks): (options: BasePluginArgs) => DatabasePlugin;
67
+ //#endregion
68
+ export { AbstractDatabasePlugin, BaseDatabaseUtils, createDatabasePlugin };
@@ -0,0 +1,68 @@
1
+ import { BasePluginArgs, DatabasePlugin, DatabasePluginHooks, PaginationInfo } from "./types/index.js";
2
+ import { Bundle } from "@hot-updater/core";
3
+
4
+ //#region src/createDatabasePlugin.d.ts
5
+ interface BaseDatabaseUtils {
6
+ cwd: string;
7
+ }
8
+ interface AbstractDatabasePlugin<TContext = object> {
9
+ getContext?: () => TContext;
10
+ getBundleById: (context: TContext, bundleId: string) => Promise<Bundle | null>;
11
+ getBundles: (context: TContext, options: {
12
+ where?: {
13
+ channel?: string;
14
+ platform?: string;
15
+ };
16
+ limit: number;
17
+ offset: number;
18
+ }) => Promise<{
19
+ data: Bundle[];
20
+ pagination: PaginationInfo;
21
+ }>;
22
+ getChannels: (context: TContext) => Promise<string[]>;
23
+ onUnmount?: (context: TContext) => void;
24
+ commitBundle: (context: TContext, {
25
+ changedSets
26
+ }: {
27
+ changedSets: {
28
+ operation: "insert" | "update" | "delete";
29
+ data: Bundle;
30
+ }[];
31
+ }) => Promise<void>;
32
+ }
33
+ /**
34
+ * Creates a database plugin with the given implementation.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const myDatabasePlugin = createDatabasePlugin("myDatabase", {
39
+ * getContext: () => ({
40
+ * // Your database client or connection
41
+ * dbClient: createDbClient()
42
+ * }),
43
+ * async getBundleById(context, bundleId) {
44
+ * // Implementation to get a bundle by ID using context.dbClient
45
+ * return bundle;
46
+ * },
47
+ * async getBundles(context, options) {
48
+ * // Implementation to get bundles with options using context.dbClient
49
+ * return bundles;
50
+ * },
51
+ * async getChannels(context) {
52
+ * // Implementation to get available channels using context.dbClient
53
+ * return channels;
54
+ * },
55
+ * async commitBundle(context, { changedSets }) {
56
+ * // Implementation to commit changed bundles using context.dbClient
57
+ * }
58
+ * });
59
+ * ```
60
+ *
61
+ * @param name - The name of the database plugin
62
+ * @param abstractPlugin - A plugin implementation with context support
63
+ * @param hooks - Optional hooks for plugin lifecycle events
64
+ * @returns A function that creates a database plugin instance
65
+ */
66
+ declare function createDatabasePlugin<TContext = object>(name: string, abstractPlugin: AbstractDatabasePlugin<TContext>, hooks?: DatabasePluginHooks): (options: BasePluginArgs) => DatabasePlugin;
67
+ //#endregion
68
+ export { AbstractDatabasePlugin, BaseDatabaseUtils, createDatabasePlugin };
@@ -0,0 +1,95 @@
1
+ import { memoize, merge } from "es-toolkit";
2
+
3
+ //#region src/createDatabasePlugin.ts
4
+ /**
5
+ * Creates a database plugin with the given implementation.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const myDatabasePlugin = createDatabasePlugin("myDatabase", {
10
+ * getContext: () => ({
11
+ * // Your database client or connection
12
+ * dbClient: createDbClient()
13
+ * }),
14
+ * async getBundleById(context, bundleId) {
15
+ * // Implementation to get a bundle by ID using context.dbClient
16
+ * return bundle;
17
+ * },
18
+ * async getBundles(context, options) {
19
+ * // Implementation to get bundles with options using context.dbClient
20
+ * return bundles;
21
+ * },
22
+ * async getChannels(context) {
23
+ * // Implementation to get available channels using context.dbClient
24
+ * return channels;
25
+ * },
26
+ * async commitBundle(context, { changedSets }) {
27
+ * // Implementation to commit changed bundles using context.dbClient
28
+ * }
29
+ * });
30
+ * ```
31
+ *
32
+ * @param name - The name of the database plugin
33
+ * @param abstractPlugin - A plugin implementation with context support
34
+ * @param hooks - Optional hooks for plugin lifecycle events
35
+ * @returns A function that creates a database plugin instance
36
+ */
37
+ function createDatabasePlugin(name, abstractPlugin, hooks) {
38
+ const changedMap = /* @__PURE__ */ new Map();
39
+ const markChanged = (operation, data) => {
40
+ changedMap.set(data.id, {
41
+ operation,
42
+ data
43
+ });
44
+ };
45
+ const memoizedContext = memoize(abstractPlugin?.getContext ?? (() => {}));
46
+ return (_) => ({
47
+ name,
48
+ async getBundleById(bundleId) {
49
+ const context = memoizedContext();
50
+ return abstractPlugin.getBundleById(context, bundleId);
51
+ },
52
+ async getBundles(options) {
53
+ const context = memoizedContext();
54
+ return abstractPlugin.getBundles(context, options);
55
+ },
56
+ async getChannels() {
57
+ const context = memoizedContext();
58
+ return abstractPlugin.getChannels(context);
59
+ },
60
+ async commitBundle() {
61
+ if (!abstractPlugin.commitBundle) throw new Error("commitBundle is not implemented");
62
+ const context = memoizedContext();
63
+ await abstractPlugin.commitBundle(context, { changedSets: Array.from(changedMap.values()) });
64
+ changedMap.clear();
65
+ hooks?.onDatabaseUpdated?.();
66
+ },
67
+ async updateBundle(targetBundleId, newBundle) {
68
+ const pendingChange = changedMap.get(targetBundleId);
69
+ if (pendingChange) {
70
+ const updatedData = merge(pendingChange.data, newBundle);
71
+ changedMap.set(targetBundleId, {
72
+ operation: pendingChange.operation,
73
+ data: updatedData
74
+ });
75
+ return;
76
+ }
77
+ const currentBundle = await this.getBundleById(targetBundleId);
78
+ if (!currentBundle) throw new Error("targetBundleId not found");
79
+ markChanged("update", merge(currentBundle, newBundle));
80
+ },
81
+ async appendBundle(inputBundle) {
82
+ markChanged("insert", inputBundle);
83
+ },
84
+ onUnmount: abstractPlugin.onUnmount ? async () => {
85
+ const context = memoizedContext();
86
+ await abstractPlugin.onUnmount?.(context);
87
+ } : void 0,
88
+ async deleteBundle(deleteBundle) {
89
+ markChanged("delete", deleteBundle);
90
+ }
91
+ });
92
+ }
93
+
94
+ //#endregion
95
+ export { createDatabasePlugin };
@@ -0,0 +1,8 @@
1
+
2
+ //#region src/createStorageKeyBuilder.ts
3
+ const createStorageKeyBuilder = (basePath) => (...args) => {
4
+ return [basePath || "", ...args].filter(Boolean).join("/");
5
+ };
6
+
7
+ //#endregion
8
+ exports.createStorageKeyBuilder = createStorageKeyBuilder;