diff --git a/Models/Queue.js b/Models/Queue.js index 99e2ec7..b714723 100644 --- a/Models/Queue.js +++ b/Models/Queue.js @@ -21,16 +21,19 @@ export class Queue { this.realm = null; this.worker = new Worker(); this.status = 'inactive'; + this.statusChangeObserver = null; } /** * * Initializes the queue by connecting to Realm database. * + * Specify an optional options object to override the default realmPath. + * */ - async init() { + async init(options = {}) { if (this.realm === null) { - this.realm = await Database.getRealmInstance(); + this.realm = await Database.getRealmInstance(options); } } @@ -67,6 +70,83 @@ export class Queue { this.worker.removeWorker(jobName); } + /** + * Get all of the registered workers. + */ + getWorkersAsArray() { + return this.worker.getWorkersAsArray(); + } + + /** + * Listen for changes in the queue status (starting and stopping). This method + * returns a unsubscribe function to stop listening to events. Always ensure you + * unsubscribe from the listener when no longer needed to prevent updates to + * components no longer in use. + * + * #### Example + * + * ```js + * const unsubscribe = queue.onQueueStateChanged((state) => { + * console.log(`Queue state changed to ${state}`); + * }); + * + * // Unsubscribe from further state changes + * unsubscribe(); + * ``` + * + * @param listener A listener function which triggers when queue status changed (for example starting). + */ + onQueueStateChanged(listener) { + this.statusChangeObserver = listener; + return () => {this.statusChangeObserver = null}; + } + + /** + * Listen for changes in the jobs collection such as jobs changing status. This method + * returns a unsubscribe function to stop listening to events. Always ensure you + * unsubscribe from the listener when no longer needed to prevent updates to + * components no longer in use. + * + * #### Example + * + * ```js + * const unsubscribe = queue.onQueueJobChanged(() => { + * console.log(`A job changed!`); + * }); + * + * // Unsubscribe from further state changes + * unsubscribe(); + * ``` + * + * @param listener A listener function which triggers when jobs collection changed. + */ + onQueueJobChanged(listener) { + // Add the listener callback to the realm + try { + this.realm.addListener("change", listener); + } catch (error) { + console.error( + `An exception was thrown within the react native queue change listener: ${error}` + ); + } + + return () => { this.realm.removeListener("change", listener); }; + } + + /** + * A simple wrapper for setting the status of the queue and notifying any + * listeners of the change. + * + * @private + * @param {string} status + */ + changeStatus(status) { + this.status = status; + if (this.statusChangeObserver) { + this.statusChangeObserver(status); + } + } + /** * * Creates a new job and adds it to queue. @@ -100,6 +180,7 @@ export class Queue { active: false, timeout: (options.timeout >= 0) ? options.timeout : 25000, created: new Date(), + lastFailed: null, failed: null }); }); @@ -139,7 +220,7 @@ export class Queue { return false; } - this.status = 'active'; + this.changeStatus('active'); // Get jobs to process const startTime = Date.now(); @@ -174,7 +255,7 @@ export class Queue { } } - this.status = 'inactive'; + this.changeStatus('inactive'); } /** @@ -186,7 +267,27 @@ export class Queue { * */ stop() { - this.status = 'inactive'; + this.changeStatus('inactive'); + } + + /** + * + * Get a job by id from the queue. + * + * @param sync {boolean} - This should be true if you want to guarantee job data is fresh. Otherwise you could receive job data that is not up to date if a write transaction is occuring concurrently. + * @return {promise} - Promise that resolves to a collection of all the jobs in the queue. + */ + async getJob(id, sync = false) { + if (sync) { + let job = null; + this.realm.write(() => { + job = this.realm.objectForPrimaryKey('Job', id); + }); + + return job; + } else { + return await this.realm.objectForPrimaryKey('Job', id); + } } /** @@ -217,15 +318,15 @@ export class Queue { * worker function that has concurrency X > 1, then X related (jobs with same name) * jobs will be returned. * - * If queue is running with a lifespan, only jobs with timeouts at least 500ms < than REMAINING lifespan - * AND a set timeout (ie timeout > 0) will be returned. See Queue.start() for more info. - * * @param queueLifespanRemaining {number} - The remaining lifespan of the current queue process (defaults to indefinite). * @return {promise} - Promise resolves to an array of job(s) to be processed next by the queue. */ async getConcurrentJobs(queueLifespanRemaining = 0) { let concurrentJobs = []; + const workersArr = this.worker.getWorkersAsArray(); + if (workersArr.length === 0) return []; + this.realm.write(() => { // Get next job from queue. let nextJob = null; @@ -234,9 +335,46 @@ export class Queue { // If queueLife const timeoutUpperBound = (queueLifespanRemaining - 500 > 0) ? queueLifespanRemaining - 499 : 0; // Only get jobs with timeout at least 500ms < queueLifespanRemaining. + // Get worker specific minimum time between attempts. + let workerFilters; + if (workersArr.length > 0) { + workerFilters = workersArr.map(worker => { + const { name, minimumMillisBetweenAttempts = 0 } = worker; + let earliestLastFailed = new Date(); + earliestLastFailed.setMilliseconds(earliestLastFailed.getMilliseconds() - minimumMillisBetweenAttempts); + const realmFilterableDate = earliestLastFailed.toISOString().replace('T', '@').split('.')[0] + ':00'; + const workerFiler = ` + ( + name == "${name}" AND + ( + lastFailed == null OR + lastFailed <= ${realmFilterableDate} + ) + )`; + return workerFiler; + }); + } + const initialQuery = (queueLifespanRemaining) - ? '(active == FALSE AND failed == null AND timeout > 0 AND timeout < ' + timeoutUpperBound + ') OR (active == FALSE AND failed == null AND timeout > 0 AND timeout < ' + timeoutUpperBound + ')' - : '(active == FALSE AND failed == null) OR (active == TRUE && failed == null)'; + ? ` + (active == FALSE AND + failed == null AND + (${workerFilters?.join(' OR ')}) AND + timeout > 0 AND + timeout < ${timeoutUpperBound}) + OR (active == FALSE AND + failed == null AND + (${workerFilters?.join(' OR ')}) AND + timeout > 0 AND + timeout < ${timeoutUpperBound})` + + : ` + (active == FALSE AND + (${workerFilters?.join(' OR ')}) AND + failed == null) + OR (active == TRUE AND + (${workerFilters?.join(' OR ')}) AND + failed == null)`; let jobs = Array.from(this.realm.objects('Job') .filtered(initialQuery) @@ -251,19 +389,70 @@ export class Queue { const concurrency = this.worker.getConcurrency(nextJob.name); const allRelatedJobsQuery = (queueLifespanRemaining) - ? '(name == "'+ nextJob.name +'" AND active == FALSE AND failed == null AND timeout > 0 AND timeout < ' + timeoutUpperBound + ') OR (name == "'+ nextJob.name +'" AND active == FALSE AND failed == null AND timeout > 0 AND timeout < ' + timeoutUpperBound + ')' - : '(name == "'+ nextJob.name +'" AND active == FALSE AND failed == null) OR (name == "'+ nextJob.name +'" AND active == TRUE AND failed == null)'; + ? `(name == "${nextJob.name}" AND + active == FALSE AND + (${workerFilters?.join(' OR ')}) AND + failed == null AND + timeout > 0 AND + timeout < ${timeoutUpperBound}) + OR (name == "${nextJob.name}" AND + active == FALSE AND + (${workerFilters?.join(' OR ')}) AND + failed == null AND + timeout > 0 AND + timeout < ${timeoutUpperBound})` + + : `(name == "${nextJob.name}" AND + active == FALSE AND + (${workerFilters?.join(' OR ')}) AND + failed == null) + OR (name == "${nextJob.name}" AND + active == TRUE AND + (${workerFilters?.join(' OR ')}) AND + failed == null)`; const allRelatedJobs = this.realm.objects('Job') .filtered(allRelatedJobsQuery) .sorted([['priority', true], ['created', false]]); - let jobsToMarkActive = allRelatedJobs.slice(0, concurrency); + // Filter out any jobs that are not runnable. + let runnableJobs = []; + for (let index = 0; index < allRelatedJobs.length; index++) { + const job = allRelatedJobs[index]; + const { runnable, reason } = this.worker.execIsJobRunnable(job.name, job); + if (runnable) { + runnableJobs.push(job); + } else { + const jobPayload = JSON.parse(job.payload); + let jobData = JSON.parse(job.data); + + // Increment failed attempts number + if (!jobData.skippedAttempts) { + jobData.skippedAttempts = 1; + } else { + jobData.skippedAttempts++; + } + + // Log skipped reasons + if (!jobData.skippedReasons) { + jobData.skippedReasons = [reason]; + } else { + jobData.skippedReasons.push(reason); + } + + job.data = JSON.stringify(jobData); + + // Fire onSkipped job lifecycle callback + this.worker.executeJobLifecycleCallback('onSkipped', job.name, job.id, {...jobPayload, skippedReason: reason}); + } + } + + let jobsToMarkActive = runnableJobs.slice(0, concurrency); // Grab concurrent job ids to reselect jobs as marking these jobs as active will remove // them from initial selection when write transaction exits. // See: https://stackoverflow.com/questions/47359368/does-realm-support-select-for-update-style-read-locking/47363356#comment81772710_47363356 - const concurrentJobIds = jobsToMarkActive.map( job => job.id); + const concurrentJobIds = jobsToMarkActive.map(job => job.id); // Mark concurrent jobs as active jobsToMarkActive = jobsToMarkActive.map( job => { @@ -271,17 +460,17 @@ export class Queue { }); // Reselect now-active concurrent jobs by id. - const reselectQuery = concurrentJobIds.map( jobId => 'id == "' + jobId + '"').join(' OR '); - const reselectedJobs = Array.from(this.realm.objects('Job') - .filtered(reselectQuery) - .sorted([['priority', true], ['created', false]])); - - concurrentJobs = reselectedJobs.slice(0, concurrency); + if (concurrentJobIds.length > 0) { + const reselectQuery = concurrentJobIds.map(jobId => 'id == "' + jobId + '"').join(' OR '); + const reselectedJobs = Array.from(this.realm.objects('Job') + .filtered(reselectQuery) + .sorted([['priority', true], ['created', false]])); + concurrentJobs = reselectedJobs.slice(0, concurrency); + } } }); return concurrentJobs; - } /** @@ -326,43 +515,97 @@ export class Queue { let jobData = JSON.parse(job.data); const errorMessage = error?.message || ''; - this.realm.write(() => { - // Increment failed attempts number - if (!jobData.failedAttempts) { - jobData.failedAttempts = 1; - } else { - jobData.failedAttempts++; - } + // Call the optional error profiler from the worker.options to learn what we should + // do with this error. If the profiler returns true, we should attempt the job. + const failureBehavior = this.worker.getFailureBehavior(jobName); + + switch (failureBehavior) { + case 'standard': + this.realm.write(() => { + // Increment failed attempts number + if (!jobData.failedAttempts) { + jobData.failedAttempts = 1; + } else { + jobData.failedAttempts++; + } + + // Log error + if (!jobData.errors) { + jobData.errors = [errorMessage]; + } else { + jobData.errors.push(errorMessage); + } + + job.data = JSON.stringify(jobData); + + // Reset active status + job.active = false; + + // Use the same date object for both failure times if last failure + const now = new Date(); + + // Record when this attempt failed + job.lastFailed = now; + + // Mark job as failed if too many attempts + if (jobData.failedAttempts >= jobData.attempts) { + job.failed = now; + } + }); + + // Execute job onFailure lifecycle callback. + this.worker.executeJobLifecycleCallback('onFailure', jobName, jobId, jobPayload); + + // If job has failed all attempts execute job onFailed and onComplete lifecycle callbacks. + if (jobData.failedAttempts >= jobData.attempts) { + this.worker.executeJobLifecycleCallback('onFailed', jobName, jobId, jobPayload); + this.worker.executeJobLifecycleCallback('onComplete', jobName, jobId, jobPayload); + } + break; + default: + break; + } + } + } - // Log error - if (!jobData.errors) { - jobData.errors = [ errorMessage ]; - } else { - jobData.errors.push(errorMessage); - } + /** + * Delete a job from the queue. + * + * @param jobId {string} - Unique id associated with job. + * + */ - job.data = JSON.stringify(jobData); + deleteJob(jobId) { + this.realm.write(() => { + let job = this.realm.objects('Job').filtered('id == "' + jobId + '"'); - // Reset active status - job.active = false; + if (job.length) { + this.realm.delete(job); + } else { + throw new Error('Job ' + jobId + ' does not exist.'); + } + }); + } - // Mark job as failed if too many attempts - if (jobData.failedAttempts >= jobData.attempts) { - job.failed = new Date(); - } - }); + /** + * + * Delete all failed jobs from the queue. + * + * + */ - // Execute job onFailure lifecycle callback. - this.worker.executeJobLifecycleCallback('onFailure', jobName, jobId, jobPayload); + deleteAllFailedJobs() { + this.realm.write(() => { + let jobs = Array.from(this.realm.objects('Job') + .filtered('failed != null')); - // If job has failed all attempts execute job onFailed and onComplete lifecycle callbacks. - if (jobData.failedAttempts >= jobData.attempts) { - this.worker.executeJobLifecycleCallback('onFailed', jobName, jobId, jobPayload); - this.worker.executeJobLifecycleCallback('onComplete', jobName, jobId, jobPayload); + if (jobs.length) { + this.realm.delete(jobs); } - } + }); } + /** * * Delete jobs in the queue. @@ -396,9 +639,9 @@ export class Queue { * * @return {Queue} - A queue instance. */ -export default async function queueFactory() { +export default async function queueFactory(options = {}) { const queue = new Queue(); - await queue.init(); + await queue.init(options); return queue; } diff --git a/Models/Worker.js b/Models/Worker.js index 1c02f54..834a27f 100644 --- a/Models/Worker.js +++ b/Models/Worker.js @@ -36,11 +36,17 @@ export default class Worker { throw new Error('Job name and associated worker function must be supplied.'); } + const defaultIsJobRunnable = (job) => ({ runnable: true }); + // Attach options to worker worker.options = { concurrency: options.concurrency || 1, + isJobRunnable: options.isJobRunnable || defaultIsJobRunnable, + failureBehavior: options.failureBehavior || 'standard', // standard | custom + minimumMillisBetweenAttempts: options.minimumMillisBetweenAttempts || 0, onStart: options.onStart || null, onSuccess: options.onSuccess || null, + onSkipped: options.onSkipped || null, onFailure: options.onFailure || null, onFailed: options.onFailed || null, onComplete: options.onComplete || null @@ -59,6 +65,29 @@ export default class Worker { delete Worker.workers[jobName]; } + /** + * Get an array of all registered workers. + * Each worker object in the array is the worker options object + * with the name property added. + * + * @returns {Array} - Array of worker options with name property added. + */ + getWorkersAsArray() { + return Object.keys(Worker.workers).map(jobName => { + return { ...this.getWorkerOptions(jobName), name: jobName }; + }); + } + + /** + * Get the worker options for a worker by job name. + * + * @param jobName {string} - Name associated with jobs assigned to this worker. + * @returns {Object} worker options object + */ + getWorkerOptions(jobName) { + return Worker.workers[jobName].options; + } + /** * * Get the concurrency setting for a worker. @@ -78,6 +107,64 @@ export default class Worker { return Worker.workers[jobName].options.concurrency; } + /** + * + * Call the options.isJobRunnable function if it exists for a worker. + * + * @param jobName {string} - Name associated with jobs assigned to this worker. + * @throws Throws error if no worker is currently assigned to passed in job name. + * @return {Object} + */ + execIsJobRunnable(jobName, job) { + // If no worker assigned to job name, throw error. + if (!Worker.workers[jobName]) { + throw new Error('Job ' + jobName + ' does not have a worker assigned to it.'); + } + + const isJobRunnable = Worker.workers[jobName].options.isJobRunnable; + if (isJobRunnable && typeof isJobRunnable === 'function') { + return isJobRunnable(job); + }; + + return { runnable: true }; + } + + /** + * Get the minimum duration (ms) between attempts setting for a worker. + * + * Defaults to 0. + * + * @param jobName {string} - Name associated with jobs assigned to this worker. + * @throws Throws error if no worker is currently assigned to passed in job name. + * @return {number} + */ + getMinimumMillisBetweenAttempts(jobName) { + // If no worker assigned to job name, throw error. + if (!Worker.workers[jobName]) { + throw new Error('Job ' + jobName + ' does not have a worker assigned to it.'); + } + + return parseInt(Worker.workers[jobName].options.minimumMillisBetweenAttempts); + } + + /** + * Get the failure behavior setting for a worker. + * + * Defaults to standard failure behavior. + * + * @param jobName {string} - Name associated with jobs assigned to this worker. + * @throws Throws error if no worker is currently assigned to passed in job name. + * @return {string} + */ + getFailureBehavior(jobName) { + // If no worker assigned to job name, throw error. + if (!Worker.workers[jobName]) { + throw new Error('Job ' + jobName + ' does not have a worker assigned to it.'); + } + + return Worker.workers[jobName].options.failureBehavior || null; + } + /** * * Execute the worker function assigned to the passed in job name. @@ -124,7 +211,7 @@ export default class Worker { */ async executeJobLifecycleCallback(callbackName, jobName, jobId, jobPayload) { // Validate callback name - const validCallbacks = ['onStart', 'onSuccess', 'onFailure', 'onFailed', 'onComplete']; + const validCallbacks = ['onStart', 'onSuccess', 'onSkipped', 'onFailure', 'onFailed', 'onComplete']; if (!validCallbacks.includes(callbackName)) { throw new Error('Invalid job lifecycle callback name.'); } diff --git a/README.md b/README.md index 8fd8a5c..c49e24a 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,8 @@ _Forked from [billmalarky/react-native-queue](https://github.com/billmalarky/rea #### Simple. Powerful. Persistent. -[![Node.js CI](https://github.com/sourcetoad/react-native-queue/actions/workflows/build.yml/badge.svg)](https://github.com/sourcetoad/react-native-queue/actions/workflows/build.yml) -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/sourcetoad/react-native-queue/blob/master/LICENSE) +[![Node.js CI](https://github.com/hopdrive/react-native-queue/actions/workflows/build.yml/badge.svg)](https://github.com/hopdrive/react-native-queue/actions/workflows/build.yml) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/hopdrive/react-native-queue/blob/master/LICENSE) A React Native at-least-once priority job queue / task queue backed by persistent Realm storage. Jobs will persist until completed, even if user closes and re-opens app. React Native Queue is easily integrated into OS background processes (services) so you can ensure the queue will continue to process until all jobs are completed even if app isn't in focus. It also plays well with Workers so your jobs can be thrown on the queue, then processed in dedicated worker threads for greatly improved processing performance. @@ -27,8 +27,8 @@ A React Native at-least-once priority job queue / task queue backed by persisten ## Features * **Simple API:** Set up job workers and begin creating your jobs in minutes with just two basic API calls - * queue.addWorker(name, workerFunction, options = {}) - * queue.createJob(name, payload = {}, options = {}, startQueue = true) + * queue.addWorker(name, workerFunction, options = {}) + * queue.createJob(name, payload = {}, options = {}, startQueue = true) * **Powerful options:** Easily modify default functionality. Set job timeouts, number of retry attempts, priority, job lifecycle callbacks, and worker concurrency with an options object. Start queue processing with a lifespan to easily meet OS background task time limits. * **Persistent Jobs:** Jobs are persisted with Realm. Because jobs persist, you can easily continue to process jobs across app restarts or in OS background tasks until completed or failed (or app is uninstalled). * **Powerful Integrations:** React Native Queue was designed to play well with others. The queue quickly integrates with a variety of OS background task and Worker packages so processing your jobs in a background service or dedicated thread have never been easier. @@ -36,7 +36,7 @@ A React Native at-least-once priority job queue / task queue backed by persisten ## React Native Compatibility At the core this package leverages [Realm](https://github.com/realm/realm-js/blob/main/COMPATIBILITY.md) which maintains its own compatibility. This produces -an interesting problem as we depend on a package which enforces React Native compatibility, but peer to react native. +an interesting problem as we depend on a package which enforces React Native compatibility, but peer to react native. This means it's very crucial to respect to select the proper version and respect the peering. @@ -68,13 +68,13 @@ Need advanced task functionality like dedicated worker threads or OS services? E ## Installation ```bash -$ npm install --save @sourcetoad/react-native-queue +$ npm install --save @hopdrive/react-native-queue ``` Or ```bash -$ yarn add @sourcetoad/react-native-queue +$ yarn add @hopdrive/react-native-queue ``` ## Basic Usage @@ -82,16 +82,16 @@ $ yarn add @sourcetoad/react-native-queue React Native Queue is a standard job/task queue built specifically for react native applications. If you have a long-running task, or a large number of tasks, consider turning that task into a job(s) and throwing it/them onto the queue to be processed in the background instead of blocking your UI until task(s) complete. Creating and processing jobs consists of: - + 1. Importing and initializing React Native Queue 2. Registering worker functions (the functions that execute your jobs). 3. Creating jobs. 4. Starting the queue (note this happens automatically on job creation, but sometimes the queue must be explicitly started such as in a OS background task or on app restart). Queue can be started with a lifespan in order to limit queue processing time. ```js -import queueFactory from '@sourcetoad/react-native-queue'; +import queueFactory from '@hopdrive/react-native-queue'; -// Of course this line needs to be in the context of an async function, +// Of course this line needs to be in the context of an async function, // otherwise use queueFactory.then((queue) => { console.log('add workers and jobs here'); }); const queue = await queueFactory(); @@ -99,7 +99,7 @@ const queue = await queueFactory(); queue.addWorker('example-job', async (id, payload) => { console.log('EXECUTING "example-job" with id: ' + id); console.log(payload, 'payload'); - + await new Promise((resolve) => { setTimeout(() => { console.log('"example-job" has completed!'); @@ -112,7 +112,7 @@ queue.addWorker('example-job', async (id, payload) => { // Example job passes a payload of data to 'example-job' worker. // Default settings are used (note the empty options object). -// Because false is passed, the queue won't automatically start when this job is created, so usually queue.start() +// Because false is passed, the queue won't automatically start when this job is created, so usually queue.start() // would have to be manually called. However in the final createJob() below we don't pass false so it will start the queue. // NOTE: We pass false for example purposes. In most scenarios starting queue on createJob() is perfectly fine. queue.createJob('example-job', { @@ -157,43 +157,70 @@ queue.addWorker() accepts an options object in order to tweak standard functiona ```js queue.addWorker('job-name-here', async (id, payload) => { console.log(id); }, { - + // Set max number of jobs for this worker to process concurrently. // Defaults to 1. concurrency: 5, - + + // Sets the behavior of failures on this worker. Possible values are: standard | custom + // standard: If a job fails more than the maximum number of attempts, it will be marked as failed. + // custom: If a job fails more than the maximum number of attempts, it will be retried if the job + // Defaults to standard. + failureBehavior: 'standard', + + // Set min number of milliseconds to wait before for this worker will perform another attempt. + // Defaults to 0. + minimumMillisBetweenAttempts: 1 * 1000, + + // A function that determines if a job is runnable. If the job is not runnable, it will be skipped. + // This function should return an object with a "runnable" boolean property and a "reason" string property. + // If the job is not runnable, the reason will be passed to the onSkipped callback payload as skippedReason. + // Defaults to a function that always returns true. + isJobRunnable: (job) => { + // In this example we will only allow jobs to run if they are at least 15 seconds old. + let reason; + const diffSec = Math.floor((new Date().getTime() - new Date(job?.created).getTime()) / 1000); + if (diffSec < 15) reason = `Must be at least 15 seconds old. Currently ${diffSec} seconds old.`; + return { runnable: diffSec >= 15, reason }; + }, + // JOB LIFECYCLE CALLBACKS - + // onStart job callback handler is fired when a job begins processing. // - // IMPORTANT: Job lifecycle callbacks are executed asynchronously and do not block job processing + // IMPORTANT: Job lifecycle callbacks are executed asynchronously and do not block job processing // (even if the callback returns a promise it will not be "awaited" on). // As such, do not place any logic in onStart that your actual job worker function will depend on, // this type of logic should of course go inside the job worker function itself. + onSkipped: async (id, payload) => { + const { skippedReason } = payload; + console.log('Job "job-name-here" with id ' + id + ' has been skipped because ' + skippedReason); + }, + onStart: async (id, payload) => { console.log('Job "job-name-here" with id ' + id + ' has started processing.'); }, - + // onSuccess job callback handler is fired after a job successfully completes processing. onSuccess: async (id, payload) => { console.log('Job "job-name-here" with id ' + id + ' was successful.'); }, - + // onFailure job callback handler is fired after each time a job fails (onFailed also fires if job has reached max number of attempts). onFailure: async (id, payload) => { console.log('Job "job-name-here" with id ' + id + ' had an attempt end in failure.'); }, - + // onFailed job callback handler is fired if job fails enough times to reach max number of attempts. onFailed: async (id, payload) => { console.log('Job "job-name-here" with id ' + id + ' has failed.'); }, - + // onComplete job callback handler fires after job has completed processing successfully or failed entirely. onComplete: async (id, payload) => { console.log('Job "job-name-here" with id ' + id + ' has completed processing.'); } -}); +}); ``` @@ -207,13 +234,13 @@ queue.createJob('job-name-here', {foo: 'bar'}, { // Any int will work, priority 1000 will be processed before priority 10, though this is probably overkill. // Defaults to 0. priority: 10, // High priority - + // Timeout in ms before job is considered failed. // Use this setting to kill off hanging jobs that are clogging up // your queue, or ensure your jobs finish in a timely manner if you want // to execute jobs in OS background tasks. // - // IMPORTANT: Jobs are required to have a timeout > 0 set in order to be processed + // IMPORTANT: Jobs are required to have a timeout > 0 set in order to be processed // by a queue that has been started with a lifespan. As such, if you want to process // jobs in an OS background task, you MUST give the jobs a timeout setting. // @@ -221,7 +248,7 @@ queue.createJob('job-name-here', {foo: 'bar'}, { // // Defaults to 25000. timeout: 30000, // Timeout in 30 seconds - + // Number of times to attempt a failing job before marking job as failed and moving on. // Defaults to 1. attempts: 4, // If this job fails to process 4 times in a row, it will be marked as failed. @@ -254,7 +281,7 @@ import { Button } from 'react-native'; -import queueFactory from '@sourcetoad/react-native-queue'; +import queueFactory from '@hopdrive/react-native-queue'; export default class App extends Component<{}> { @@ -425,7 +452,7 @@ import { } from 'react-native'; import BackgroundTask from 'react-native-background-task' -import queueFactory from '@sourcetoad/react-native-queue'; +import queueFactory from '@hopdrive/react-native-queue'; BackgroundTask.define(async () => { diff --git a/config/Database.js b/config/Database.js index 7a3c68f..8840141 100644 --- a/config/Database.js +++ b/config/Database.js @@ -16,7 +16,8 @@ const JobSchema = { active: { type: 'bool', default: false}, // Whether or not job is currently being processed. timeout: 'int', // Job timeout in ms. 0 means no timeout. created: 'date', // Job creation timestamp. - failed: 'date?' // Job failure timestamp (null until failure). + failed: 'date?', // Job failure timestamp (null until failure). + lastFailed: 'date?', // Last job failure timestamp (set after each failed attempt). } }; diff --git a/config/config.js b/config/config.js index e2e30b7..ccac1fa 100644 --- a/config/config.js +++ b/config/config.js @@ -5,5 +5,5 @@ */ export const Config = { REALM_PATH: 'reactNativeQueue.realm', // Name of realm database. - REALM_SCHEMA_VERSION: 0 // Must be incremented if data model updates. + REALM_SCHEMA_VERSION: 1 // Must be incremented if data model updates. }; diff --git a/package.json b/package.json index ea4c232..271a4d6 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { - "name": "@sourcetoad/react-native-queue", - "version": "2.2.0", + "name": "@hopdrive/react-native-queue", + "version": "2.4.1", "description": "A React Native Job Queue", "main": "index.js", + "publishConfig": { + "registry": "https://npm.pkg.github.com/@hopdrive" + }, "files": [ "config", "Models", @@ -19,7 +22,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/sourcetoad/react-native-queue/react-native-queue.git" + "url": "https://github.com/hopdrive/react-native-queue/react-native-queue.git" }, "keywords": [ "react", @@ -31,9 +34,9 @@ "author": "Reid Mayo", "license": "MIT", "bugs": { - "url": "https://github.com/sourcetoad/react-native-queue/issues" + "url": "https://github.com/hopdrive/react-native-queue/issues" }, - "homepage": "https://github.com/sourcetoad/react-native-queue#readme", + "homepage": "https://github.com/hopdrive/react-native-queue#readme", "dependencies": { "promise-reflect": "^1.1.0", "react-native-uuid": "^2.0.1",