From e03f0bc5b9a5bc7d1af4ff1fa5e69c9c0fd593c6 Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sat, 28 Feb 2026 13:38:46 +0100 Subject: [PATCH 01/12] API version update and package created. --- .../batch/BatchableSchedulable.cls-meta.xml | 2 +- .../classes/schedule/CronBuilder.cls-meta.xml | 2 +- sfdx-project.json | 15 +++++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/force-app/main/default/classes/batch/BatchableSchedulable.cls-meta.xml b/force-app/main/default/classes/batch/BatchableSchedulable.cls-meta.xml index 40d6793..82775b9 100644 --- a/force-app/main/default/classes/batch/BatchableSchedulable.cls-meta.xml +++ b/force-app/main/default/classes/batch/BatchableSchedulable.cls-meta.xml @@ -1,5 +1,5 @@ - 54.0 + 65.0 Active diff --git a/force-app/main/default/classes/schedule/CronBuilder.cls-meta.xml b/force-app/main/default/classes/schedule/CronBuilder.cls-meta.xml index 40d6793..82775b9 100644 --- a/force-app/main/default/classes/schedule/CronBuilder.cls-meta.xml +++ b/force-app/main/default/classes/schedule/CronBuilder.cls-meta.xml @@ -1,5 +1,5 @@ - 54.0 + 65.0 Active diff --git a/sfdx-project.json b/sfdx-project.json index 8e14a8d..e093523 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -1,12 +1,19 @@ { "packageDirectories": [ { + "versionName": "Async Lib 2.4.0", + "versionNumber": "2.4.0.NEXT", "path": "force-app", - "default": true + "default": false, + "package": "Async Lib", + "versionDescription": "" } ], - "name": "BeyondTheCloud", - "namespace": "", + "name": "async-lib", + "namespace": "btcdev", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "65.0" + "sourceApiVersion": "65.0", + "packageAliases": { + "Async Lib": "0HoP600000002JZKAY" + } } From abd8356bc90cabf14204d4bf77ec90e9dff2db53 Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sat, 28 Feb 2026 13:59:17 +0100 Subject: [PATCH 02/12] Script to create unlocked package. --- scripts/create-unlocked-package-version.sh | 41 ++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 scripts/create-unlocked-package-version.sh diff --git a/scripts/create-unlocked-package-version.sh b/scripts/create-unlocked-package-version.sh new file mode 100755 index 0000000..ccc6142 --- /dev/null +++ b/scripts/create-unlocked-package-version.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -e + +if ! git diff --quiet || ! git diff --cached --quiet; then + echo "Error: Working tree is not clean. Commit or stash changes before running this script." + exit 1 +fi + +trap 'echo "Reverting Apex changes..."; git checkout -- .' EXIT + +TARGET_FILES=( + "force-app/main/default/classes/Async.cls" + "force-app/main/default/classes/queue/QueueableJob.cls" + "force-app/main/default/classes/queue/QueueableBuilder.cls" + "force-app/main/default/classes/batch/BatchableBuilder.cls" + "force-app/main/default/classes/schedule/SchedulableBuilder.cls" + "force-app/main/default/classes/schedule/CronBuilder.cls" + "force-app/main/default/classes/mocks/AsyncMock.cls" +) + +echo "Adding global modifiers to API surface classes..." +for file in "${TARGET_FILES[@]}"; do + sed -i '' 's/public /global /g' "$file" +done + +echo "Reverting internal-type references back to public..." +sed -i '' 's/global QueueableManager\.EnqueueType/public QueueableManager.EnqueueType/g' \ + "force-app/main/default/classes/Async.cls" +sed -i '' 's/global QueueableChainState setEnqueueType/public QueueableChainState setEnqueueType/g' \ + "force-app/main/default/classes/Async.cls" +sed -i '' 's/global void enqueue(QueueableChain chain)/public void enqueue(QueueableChain chain)/g' \ + "force-app/main/default/classes/queue/QueueableJob.cls" + +echo "Creating unlocked package version..." +sf package version create \ + --code-coverage \ + --installation-key-bypass \ + --wait 30 \ + "$@" + +echo "Package version created successfully." From 261befaa81f418feb6d849a8ddae0dfe4a200e7d Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sat, 28 Feb 2026 14:01:07 +0100 Subject: [PATCH 03/12] Fix project json def. --- sfdx-project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfdx-project.json b/sfdx-project.json index e093523..3746f8a 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -4,7 +4,7 @@ "versionName": "Async Lib 2.4.0", "versionNumber": "2.4.0.NEXT", "path": "force-app", - "default": false, + "default": true, "package": "Async Lib", "versionDescription": "" } From 8d2c5c5c1db85c7365bd268a89cf5db6be3d8e90 Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sat, 28 Feb 2026 14:09:40 +0100 Subject: [PATCH 04/12] Script updates. --- scripts/create-unlocked-package-version.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/create-unlocked-package-version.sh b/scripts/create-unlocked-package-version.sh index ccc6142..dbe41fd 100755 --- a/scripts/create-unlocked-package-version.sh +++ b/scripts/create-unlocked-package-version.sh @@ -33,9 +33,12 @@ sed -i '' 's/global void enqueue(QueueableChain chain)/public void enqueue(Queue echo "Creating unlocked package version..." sf package version create \ - --code-coverage \ + --package "Async Lib" \ + --definition-file ./config/project-scratch-def.json \ --installation-key-bypass \ - --wait 30 \ + --code-coverage \ + --wait 50 \ + --json \ "$@" echo "Package version created successfully." From 4c22c6fa0e76fac04cae10f55211e6ddd3f9f67a Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sat, 28 Feb 2026 14:17:48 +0100 Subject: [PATCH 05/12] package version created. --- scripts/create-unlocked-package-version.sh | 15 ++++++++++++++- sfdx-project.json | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/scripts/create-unlocked-package-version.sh b/scripts/create-unlocked-package-version.sh index dbe41fd..0088e7d 100755 --- a/scripts/create-unlocked-package-version.sh +++ b/scripts/create-unlocked-package-version.sh @@ -6,7 +6,19 @@ if ! git diff --quiet || ! git diff --cached --quiet; then exit 1 fi -trap 'echo "Reverting Apex changes..."; git checkout -- .' EXIT +PACKAGE_CREATED=false +cleanup() { + if [ "$PACKAGE_CREATED" = true ]; then + echo "Staging sfdx-project.json with new version alias..." + git add sfdx-project.json + echo "Reverting Apex changes..." + git checkout -- . + else + echo "Package creation failed. Reverting all changes..." + git checkout -- . + fi +} +trap cleanup EXIT TARGET_FILES=( "force-app/main/default/classes/Async.cls" @@ -41,4 +53,5 @@ sf package version create \ --json \ "$@" +PACKAGE_CREATED=true echo "Package version created successfully." diff --git a/sfdx-project.json b/sfdx-project.json index 3746f8a..1109663 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -14,6 +14,7 @@ "sfdcLoginUrl": "https://login.salesforce.com", "sourceApiVersion": "65.0", "packageAliases": { - "Async Lib": "0HoP600000002JZKAY" + "Async Lib": "0HoP600000002JZKAY", + "Async Lib@2.4.0-1": "04tP6000002cJEzIAM" } } From ec1b93c88acef0d74abfd5ccef9a825fcaa00891 Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sun, 1 Mar 2026 09:48:34 +0100 Subject: [PATCH 06/12] Add namespace for unlocked package. --- force-app/main/default/classes/queue/QueueableJob.cls | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/force-app/main/default/classes/queue/QueueableJob.cls b/force-app/main/default/classes/queue/QueueableJob.cls index 8c7a8e4..cb4398d 100644 --- a/force-app/main/default/classes/queue/QueueableJob.cls +++ b/force-app/main/default/classes/queue/QueueableJob.cls @@ -86,9 +86,18 @@ public abstract class QueueableJob implements Queueable, Comparable { public QueueableJob cloneJob() { if (deepClone) { + String fullName = this.className; + String namespace = null; + String typeName = fullName; + + if (fullName.contains('.')) { + namespace = fullName.substringBefore('.'); + typeName = fullName.substringAfter('.'); + } + return (QueueableJob) JSON.deserialize( JSON.serialize(this), - Type.forName(this.className) + Type.forName(namespace, typeName) ); } else { return this.clone(); From 2b5e76defb448463ae60abf281badaeca06a634d Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sun, 1 Mar 2026 09:51:48 +0100 Subject: [PATCH 07/12] New package version. --- sfdx-project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfdx-project.json b/sfdx-project.json index 1109663..52313be 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -15,6 +15,6 @@ "sourceApiVersion": "65.0", "packageAliases": { "Async Lib": "0HoP600000002JZKAY", - "Async Lib@2.4.0-1": "04tP6000002cJEzIAM" + "Async Lib@2.4.0-2": "04tP6000002cdyTIAQ" } } From 67a37d763c3f66c95b1cc056c1a6aa0ed64a3cfb Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sun, 1 Mar 2026 11:38:17 +0100 Subject: [PATCH 08/12] Workaround JSON exception from packages. --- .../default/classes/queue/QueueableJob.cls | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/force-app/main/default/classes/queue/QueueableJob.cls b/force-app/main/default/classes/queue/QueueableJob.cls index cb4398d..fbc5114 100644 --- a/force-app/main/default/classes/queue/QueueableJob.cls +++ b/force-app/main/default/classes/queue/QueueableJob.cls @@ -86,22 +86,37 @@ public abstract class QueueableJob implements Queueable, Comparable { public QueueableJob cloneJob() { if (deepClone) { - String fullName = this.className; - String namespace = null; - String typeName = fullName; - - if (fullName.contains('.')) { - namespace = fullName.substringBefore('.'); - typeName = fullName.substringAfter('.'); + try { + return deepCloneJob(toJSON()); + } catch (JSONException e) { + throw new IllegalArgumentException( + 'deepClone() failed to serialize the job "' + className + '". ' + + 'When using a namespaced package, override toJSON() in your QueueableJob subclass: ' + + 'public override String toJSON() { return JSON.serialize(this); }' + ); } + } + return this.clone(); + } - return (QueueableJob) JSON.deserialize( - JSON.serialize(this), - Type.forName(namespace, typeName) - ); - } else { - return this.clone(); + public virtual String toJSON() { + return JSON.serialize(this); + } + + public QueueableJob deepCloneJob(String serialized) { + String fullName = this.className; + String namespace = null; + String typeName = fullName; + + if (fullName.contains('.')) { + namespace = fullName.substringBefore('.'); + typeName = fullName.substringAfter('.'); } + + return (QueueableJob) JSON.deserialize( + serialized, + Type.forName(namespace, typeName) + ); } private String getFullClassName(Object job) { From 41b18de751758036a59b54c7ece9f41e3204d353 Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sun, 1 Mar 2026 11:41:55 +0100 Subject: [PATCH 09/12] Package update. --- sfdx-project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfdx-project.json b/sfdx-project.json index 52313be..eecd814 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -15,6 +15,6 @@ "sourceApiVersion": "65.0", "packageAliases": { "Async Lib": "0HoP600000002JZKAY", - "Async Lib@2.4.0-2": "04tP6000002cdyTIAQ" + "Async Lib@2.4.0-3": "04tP6000002cfqbIAA" } } From 38fe7c58dff2766366506490efaca2a6ae98d3f7 Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sun, 1 Mar 2026 12:09:59 +0100 Subject: [PATCH 10/12] Update deep clone override definition. --- .../main/default/classes/queue/QueueableJob.cls | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/force-app/main/default/classes/queue/QueueableJob.cls b/force-app/main/default/classes/queue/QueueableJob.cls index fbc5114..8420d3c 100644 --- a/force-app/main/default/classes/queue/QueueableJob.cls +++ b/force-app/main/default/classes/queue/QueueableJob.cls @@ -87,23 +87,20 @@ public abstract class QueueableJob implements Queueable, Comparable { public QueueableJob cloneJob() { if (deepClone) { try { - return deepCloneJob(toJSON()); + return cloneForDeepCopy(); } catch (JSONException e) { throw new IllegalArgumentException( - 'deepClone() failed to serialize the job "' + className + '". ' + - 'When using a namespaced package, override toJSON() in your QueueableJob subclass: ' + - 'public override String toJSON() { return JSON.serialize(this); }' + 'deepClone() failed for the job "' + className + '". ' + + 'When using a namespaced package, override cloneForDeepCopy() in your QueueableJob subclass: ' + + 'public override QueueableJob cloneForDeepCopy() { ' + + 'return (QueueableJob) JSON.deserialize(JSON.serialize(this), YourClassName.class); }' ); } } return this.clone(); } - public virtual String toJSON() { - return JSON.serialize(this); - } - - public QueueableJob deepCloneJob(String serialized) { + public virtual QueueableJob cloneForDeepCopy() { String fullName = this.className; String namespace = null; String typeName = fullName; @@ -114,7 +111,7 @@ public abstract class QueueableJob implements Queueable, Comparable { } return (QueueableJob) JSON.deserialize( - serialized, + JSON.serialize(this), Type.forName(namespace, typeName) ); } From 5a218f4935f2a7c90800efa48367148d532590dd Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sun, 1 Mar 2026 12:15:54 +0100 Subject: [PATCH 11/12] Paackage update. --- sfdx-project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sfdx-project.json b/sfdx-project.json index eecd814..2932643 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -15,6 +15,6 @@ "sourceApiVersion": "65.0", "packageAliases": { "Async Lib": "0HoP600000002JZKAY", - "Async Lib@2.4.0-3": "04tP6000002cfqbIAA" + "Async Lib@2.4.0-4": "04tP6000002cgEnIAI" } } From c48bdb25028af4225d1b2b1e834ffffa2170e9fb Mon Sep 17 00:00:00 2001 From: Mateusz7410 Date: Sun, 1 Mar 2026 12:34:59 +0100 Subject: [PATCH 12/12] Add new updates for unlocked package. --- website/.vitepress/config.mts | 4 + website/api/queueable.md | 6 +- .../explanations/deep-clone-in-packages.md | 113 ++++++++++++++++++ website/explanations/job-cloning.md | 4 + website/introduction/installation.md | 16 +++ 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 website/explanations/deep-clone-in-packages.md diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index c2077da..668414c 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -61,6 +61,10 @@ export default defineConfig({ link: '/explanations/initial-scheduled-queuable-batch-job' }, { text: 'Job Cloning', link: '/explanations/job-cloning' }, + { + text: 'Deep Clone in Packages', + link: '/explanations/deep-clone-in-packages' + }, { text: 'Testing Async Jobs', link: '/explanations/testing-async-jobs' } ] } diff --git a/website/api/queueable.md b/website/api/queueable.md index 4f65ad7..160682a 100644 --- a/website/api/queueable.md +++ b/website/api/queueable.md @@ -200,7 +200,11 @@ Async.queueable(new MyQueueableJob()) Clones provided QueueableJob by value for all the member variables. By default only primitive member variables (String, Boolean, ...) are cloned by value. -Deeper explanation is [here](/explanations/job-cloning.md). +Deeper explanation is [here](/explanations/job-cloning). + +::: warning Package Usage +When using Async Lib as a package (`btcdev` namespace), deep clone requires overriding `cloneForDeepCopy()` in your subclass. See [Deep Clone in Packages](/explanations/deep-clone-in-packages). +::: **Signature** diff --git a/website/explanations/deep-clone-in-packages.md b/website/explanations/deep-clone-in-packages.md new file mode 100644 index 0000000..b1cce19 --- /dev/null +++ b/website/explanations/deep-clone-in-packages.md @@ -0,0 +1,113 @@ +--- +outline: deep +--- + +# Deep Clone in Packages + +## TL;DR + +When using Async Lib as a **managed or unlocked package** (with the `btcdev` namespace), `.deepClone()` requires you to override `cloneForDeepCopy()` in your `QueueableJob` subclass. Without this override, the framework cannot serialize and deserialize your job across namespace boundaries. + +```apex +public class MyJob extends btcdev.QueueableJob { + + public override btcdev.QueueableJob cloneForDeepCopy() { + return (btcdev.QueueableJob) JSON.deserialize(JSON.serialize(this), MyJob.class); + } + + public override void work() { + // your logic + } +} +``` + +If you deploy Async Lib **without a namespace** (e.g., via the Deploy button or `sf project deploy`), no override is needed. + +## Why Is This Needed? + +Deep cloning uses `JSON.serialize()` and `JSON.deserialize()` to create a complete copy of a job instance. Two Salesforce platform behaviors make this fail across namespace boundaries: + +### 1. Serialization Context + +`JSON.serialize(this)` behaves differently depending on **where** it executes. When called from inside the `btcdev` package code, Salesforce attaches internal platform metadata to `Queueable` implementors that cannot be serialized. The same object serializes fine from subscriber code. + +**From package code (fails):** +```apex +// Inside btcdev.QueueableJob.cloneForDeepCopy() +JSON.serialize(this); // System.JSONException: Type cannot be serialized +``` + +**From subscriber code (works):** +```apex +// Inside your class that extends btcdev.QueueableJob +JSON.serialize(this); // works fine +``` + +### 2. Type Resolution Context + +`Type.forName()` resolves types relative to the **calling code's namespace**. When the package code tries to find your subscriber class, it looks in the `btcdev` namespace where your class doesn't exist. + +**From package code:** +```apex +// Inside btcdev.QueueableJob +Type.forName('MyJob'); // returns null (looks for btcdev.MyJob) +``` + +**From subscriber code:** +```apex +// Inside your class +Type.forName('MyJob'); // returns MyJob.class +``` + +## The Solution + +The `cloneForDeepCopy()` method is `virtual`, allowing you to override it in your subclass. Your override runs in your namespace context, where both serialization and type resolution work correctly. + +```apex +public class AccountProcessorJob extends btcdev.QueueableJob { + public List accounts; + public Map config; + + public override btcdev.QueueableJob cloneForDeepCopy() { + return (btcdev.QueueableJob) JSON.deserialize( + JSON.serialize(this), AccountProcessorJob.class + ); + } + + public override void work() { + // process accounts + } +} +``` + +Then use `.deepClone()` as normal: + +```apex +btcdev.Async.queueable(new AccountProcessorJob()) + .deepClone() + .enqueue(); +``` + +## When Do I Need This? + +| Scenario | Override needed? | +|----------|:---:| +| Deployed without namespace (Deploy button / `sf deploy`) | No | +| Installed as package, using `.deepClone()` | **Yes** | +| Installed as package, NOT using `.deepClone()` | No | + +## Error Message + +If you forget the override, the framework throws a descriptive error: + +``` +deepClone() failed for the job "MyJob". +When using a namespaced package, override cloneForDeepCopy() in your QueueableJob subclass: +public override QueueableJob cloneForDeepCopy() { + return (QueueableJob) JSON.deserialize(JSON.serialize(this), YourClassName.class); +} +``` + +## Soft Clone vs Deep Clone Recap + +Not sure if you need `.deepClone()` at all? See [Job Cloning](/explanations/job-cloning) for when soft clone (default) is sufficient vs when deep clone is required. diff --git a/website/explanations/job-cloning.md b/website/explanations/job-cloning.md index 644eea7..07f93cd 100644 --- a/website/explanations/job-cloning.md +++ b/website/explanations/job-cloning.md @@ -192,6 +192,10 @@ Async.queueable(myJob) - **CPU**: More intensive - **Recommended for**: Jobs with complex object relationships +## Package Usage + +When Async Lib is installed as a package (with the `btcdev` namespace), deep clone requires an additional override due to Salesforce cross-namespace serialization limitations. See [Deep Clone in Packages](/explanations/deep-clone-in-packages) for details. + ## Summary Job cloning is a critical feature that prevents reference corruption in diff --git a/website/introduction/installation.md b/website/introduction/installation.md index b9ada1c..aab5795 100644 --- a/website/introduction/installation.md +++ b/website/introduction/installation.md @@ -4,6 +4,22 @@ outline: deep # Installation +## Install as Unlocked Package + +Install the latest version of Async Lib as an unlocked package: + + + Install Unlocked Package + + +``` +https://login.salesforce.com/packaging/installPackage.apexp?p0=04tP6000002cgEnIAI +``` + +::: tip +When installed as a package, all classes use the `btcdev` namespace prefix (e.g., `btcdev.QueueableJob`, `btcdev.Async`). If you use [`.deepClone()`](/api/queueable#deepclone), see [Deep Clone in Packages](/explanations/deep-clone-in-packages) for a required override. +::: + ## Deploy via Button Deploy to your Salesforce org using the deploy button: