|
| 1 | +import { WorkerOptions } from "./plugins"; |
| 2 | + |
| 3 | +// https://github.com/Rich-Harris/devalue/blob/50af63e2b2c648f6e6ea29904a14faac25a581fc/src/utils.js#L31-L51 |
| 4 | +const objectProtoNames = Object.getOwnPropertyNames(Object.prototype) |
| 5 | + .sort() |
| 6 | + .join("\0"); |
| 7 | +function isPlainObject(value: unknown): value is Record<string, unknown> { |
| 8 | + const proto = Object.getPrototypeOf(value); |
| 9 | + return ( |
| 10 | + proto === Object.prototype || |
| 11 | + proto === null || |
| 12 | + Object.getOwnPropertyNames(proto).sort().join("\0") === objectProtoNames |
| 13 | + ); |
| 14 | +} |
| 15 | + |
| 16 | +// Get all the keys in `WorkerOptions` whose values can be either an array or |
| 17 | +// a record (e.g. `kvNamespaces` which can either be a `string[]` of namespaces |
| 18 | +// or a `Record<string, string>` mapping binding name to namespace ID) |
| 19 | +type ArrayRecordKeys<O extends object, K extends keyof O> = K extends unknown |
| 20 | + ? Extract<O[K], unknown[]> extends never |
| 21 | + ? never |
| 22 | + : Extract<O[K], Record<string, unknown>> extends never |
| 23 | + ? never |
| 24 | + : K |
| 25 | + : never; |
| 26 | +// "kvNamespaces" | "r2Buckets" | "queueProducers" | "queueConsumers" | ... |
| 27 | +type WorkerOptionsArrayRecordKeys = ArrayRecordKeys< |
| 28 | + WorkerOptions, |
| 29 | + keyof WorkerOptions |
| 30 | +>; |
| 31 | +// Get the record type that can be used for key `K` in `WorkerOptions` |
| 32 | +type WorkerOptionsRecord<K extends WorkerOptionsArrayRecordKeys> = Extract< |
| 33 | + WorkerOptions[K], |
| 34 | + Record<string, unknown> |
| 35 | +>; |
| 36 | +/** Converts the array-form of key `K` in `WorkerOptions` to its object form */ |
| 37 | +function convertWorkerOptionsArrayToObject< |
| 38 | + K extends WorkerOptionsArrayRecordKeys |
| 39 | +>(key: K, array: Extract<WorkerOptions[K], unknown[]>): WorkerOptionsRecord<K> { |
| 40 | + // eslint-disable-next-line @typescript-eslint/no-unused-vars |
| 41 | + const _: string[] = array; // Static assert that `array` is a `string[]` |
| 42 | + if (key === "queueConsumers") { |
| 43 | + // Unfortunately, we can't just `return Object.fromEntries(...)` here, as |
| 44 | + // TypeScript isn't smart enough to substitute "queueConsumers" as `K` in |
| 45 | + // the return type. We'd still like to verify correct types, so try assign |
| 46 | + // it to that first, then return by casting. |
| 47 | + const object: WorkerOptionsRecord<"queueConsumers"> = Object.fromEntries( |
| 48 | + array.map((item) => [item, {}]) |
| 49 | + ); |
| 50 | + return object as WorkerOptionsRecord<K>; |
| 51 | + } else { |
| 52 | + const object: WorkerOptionsRecord< |
| 53 | + // `Exclude` encodes the `else` here |
| 54 | + Exclude<WorkerOptionsArrayRecordKeys, "queueConsumers"> |
| 55 | + > = Object.fromEntries(array.map((item) => [item, item])); |
| 56 | + return object as WorkerOptionsRecord<K>; |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +/** |
| 61 | + * Merges all of `b`'s properties into `a`. Only merges 1 level deep, i.e. |
| 62 | + * `kvNamespaces` will be fully-merged, but `durableObject` object-designators |
| 63 | + * will be overwritten. |
| 64 | + */ |
| 65 | +export function mergeWorkerOptions( |
| 66 | + /* mut */ a: Partial<WorkerOptions>, |
| 67 | + b: Partial<WorkerOptions> |
| 68 | +): Partial<WorkerOptions> { |
| 69 | + const aRecord = a as Record<string, unknown>; |
| 70 | + for (const [key, bValue] of Object.entries(b)) { |
| 71 | + const aValue = aRecord[key]; |
| 72 | + if (aValue === undefined) { |
| 73 | + // Simple case: if `key` only exists in `b`, copy it over to `a` |
| 74 | + aRecord[key] = bValue; |
| 75 | + continue; |
| 76 | + } |
| 77 | + |
| 78 | + const aIsArray = Array.isArray(aValue); |
| 79 | + const bIsArray = Array.isArray(bValue); |
| 80 | + const aIsObject = isPlainObject(aValue); |
| 81 | + const bIsObject = isPlainObject(bValue); |
| 82 | + if (aIsArray && bIsArray) { |
| 83 | + // Merge arrays by joining them together, de-duplicating primitives |
| 84 | + aRecord[key] = Array.from(new Set(aValue.concat(bValue))); |
| 85 | + } else if (aIsArray && bIsObject) { |
| 86 | + // Merge arrays and objects by converting the array into object form, |
| 87 | + // then assigning `b` to `a`. |
| 88 | + const aNewValue = convertWorkerOptionsArrayToObject( |
| 89 | + // Must be an array/record key if `aValue` & `bValue` are array/record |
| 90 | + key as WorkerOptionsArrayRecordKeys, |
| 91 | + aValue |
| 92 | + ); |
| 93 | + Object.assign(aNewValue, bValue); |
| 94 | + aRecord[key] = aNewValue; |
| 95 | + } else if (aIsObject && bIsArray) { |
| 96 | + const bNewValue = convertWorkerOptionsArrayToObject( |
| 97 | + // Must be an array/record key if `aValue` & `bValue` are array/record |
| 98 | + key as WorkerOptionsArrayRecordKeys, |
| 99 | + bValue |
| 100 | + ); |
| 101 | + Object.assign(aValue, bNewValue); |
| 102 | + } else if (aIsObject && bIsObject) { |
| 103 | + // Merge objects by assigning `b` to `a` |
| 104 | + Object.assign(aValue, bValue); |
| 105 | + } else { |
| 106 | + // Merge primitives/complex objects by just using `b`'s value |
| 107 | + aRecord[key] = bValue; |
| 108 | + } |
| 109 | + } |
| 110 | + return a; |
| 111 | +} |
0 commit comments