Repository files navigation

Resolver

GitHub Actions Workflow StatusGitHub License

A dependency-aware task resolver using RxJS observables for asynchronous execution. This library allows you to define tasks with dependencies and automatically resolves them in the correct order.

Features

  • Dependency Resolution: Automatically determines the execution order based on task dependencies
  • RxJS Integration: Built on RxJS observables for powerful async handling
  • Type Safety: Full TypeScript support with type inference
  • Error Handling: Built-in error handling with graceful failure modes

Installation

npm install @robinw151/resolver
# or
pnpm add @robinw151/resolver
# or
yarn add @robinw151/resolver

Usage

Basic Example

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess}from'@robinw151/resolver';// Create a resolver instanceconstresolver=newResolver()// Register task A with no dependencies.register({id: 'A',fn: ()=>'Hello',})// Register task B with no dependencies.register({id: 'B',fn: ()=>'World',})// Register task C that depends on A and B.register({id: 'C',fn: ({ A, B })=>{if(isSuccess(A)&&isSuccess(B)){return`${A.data}${B.data}!`;}thrownewError('Missing dependencies');},},['A','B'],);// Resolve all tasksconstresult=awaitlastValueFrom(resolver.resolve());console.log(result);// { tasks: { A: { data: 'Hello' }, B: { data: 'World' }, C: { data: 'Hello World!' } }}

Advanced Example with Error Handling

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess,isError,hasNoErrors}from'@robinw151/resolver';constresolver=newResolver().register({id: 'fetchUser',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'fetchPosts',fn: ({ fetchUser })=>{if(isSuccess(fetchUser)){return[{id: 1,title: 'Post 1'},{id: 2,title: 'Post 2'},];}thrownewError('User not found');},},['fetchUser'],).register({id: 'generateReport',fn: ({ fetchUser, fetchPosts })=>{if(isSuccess(fetchUser)&&isSuccess(fetchPosts)){return{user: fetchUser.data,postCount: fetchPosts.data.length,timestamp: newDate().toISOString(),};}thrownewError('Missing data for report');},},['fetchUser','fetchPosts'],);try{constresult=awaitlastValueFrom(resolver.resolve());if(isError(result.tasks.fetchUser)){console.error('User fetch failed:',result.tasks.fetchUser.error);}if(isError(result.tasks.fetchPosts)){console.error('Posts fetch failed:',result.tasks.fetchPosts.error);}if(isError(result.tasks.generateReport)){console.error('Report generation failed:',result.tasks.generateReport.error);}if(hasNoErrors(result.tasks)){console.log('Report generated:',result.tasks.generateReport.data);}}catch(error){console.error('Resolution failed:',error);}

Type Safety

The resolver provides full TypeScript support with type inference:

import{Resolver,isSuccess,isError}from'@robinw151/resolver';// Task result types are inferred from the registered task functionsconstresolver=newResolver().register({id: 'user',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'posts',fn: ({ user })=>{// user is typed as { data: { id: number; name: string }} | { error: unknown }if(isSuccess(user)){console.log('User loaded:',user.data.name);return[{id: 1,title: 'Post 1'}];}else{console.error('User failed to load:',user.error);return[];}},},['user'],);

Task Results

Every task produces exactly one result. A task function may return a plain value, a Promise or an Observable:

import{of}from'rxjs';constresolver=newResolver().register({id: 'value',fn: ()=>1}).register({id: 'promise',fn: ()=>Promise.resolve(2)}).register({id: 'observable',fn: ()=>of(3)});

For an Observable the first emitted value becomes the task's result. The subscription is closed right after that value, so later emissions are never observed, and sources that honor unsubscription are cancelled:

// Only the first value is used, the subscription is closed afterwards
fn: ()=>of(1,2,3);// { data: 1 }// Observed as events, an HttpClient request emits `HttpEventType.Sent` first,// so the task resolves with that event and the request is cancelled
fn: ()=>http.get('/user',{observe: 'events',reportProgress: true});

Pipe the source when a different value is needed:

import{last,toArray}from'rxjs';
fn: ()=>of(1,2,3).pipe(last());// { data: 3 }
fn: ()=>of(1,2,3).pipe(toArray());// { data: [1, 2, 3] }

Keep in mind that last() and toArray() only emit once the source completes. Applying either to a source that never completes leaves the task, and therefore the whole resolution, pending indefinitely. The default behavior has no such risk, which is why an infinite source such as interval(1000) resolves with its first value instead of hanging.

A source that completes without emitting any value cannot produce a result. Such a task resolves with an EmptyTaskError instead of blocking the resolution:

import{EMPTY,lastValueFrom}from'rxjs';import{EmptyTaskError,isError,Resolver}from'@robinw151/resolver';constresult=awaitlastValueFrom(newResolver().register({id: 'empty',fn: ()=>EMPTY}).resolve());if(isError(result.tasks.empty)){console.log(result.tasks.empty.errorinstanceofEmptyTaskError);// true}

Error Handling

Tasks can return either successful data or errors. The resolver handles both cases gracefully:

  • Successful tasks return { data: TResult }
  • Failed tasks return { error: unknown }

A task whose Observable completes without emitting a value fails with an EmptyTaskError, which is exported from the package and carries the taskId of the task that produced it.

Global Arguments

The resolver supports global arguments that are passed to all task functions during execution. This is useful for sharing configuration, API keys, or other context across all tasks.

Constructor Global Arguments

You can provide global arguments when creating a resolver instance:

import{lastValueFrom}from'rxjs';import{Resolver}from'@robinw151/resolver';// Create resolver with global argumentsconstresolver=newResolver({apiKey: 'your-api-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchUser',fn: (_args,globalArgs)=>{// globalArgs is typed as { apiKey: string; baseUrl: string }returnfetch(`${globalArgs.baseUrl}/user`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},}).register({id: 'fetchPosts',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/posts`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});constresult=awaitlastValueFrom(resolver.resolve());console.log(result.globalArgs);// { apiKey: 'your-api-key', baseUrl: 'https://api.example.com' }

Because tasks receive the global arguments typed as TGlobalArgs, the constructor argument is required whenever TGlobalArgs cannot be undefined:

newResolver();// OK - no global arguments at allnewResolver({apiKey: 'your-api-key'});// OK - type is inferrednewResolver<{apiKey: string}>({apiKey: 'your-api-key'});// OK - explicit type, value providednewResolver<{apiKey: string}>();// Error - tasks would receive `undefined`

If the arguments are only known later and are supplied through setGlobalArgs() or resolve({ globalArgs }), include undefined in the type. Task functions then have to narrow it before use:

constresolver=newResolver<{apiKey: string}|undefined>().register({id: 'fetchUser',fn: (_args,globalArgs)=>{if(!globalArgs){thrownewError('Global arguments have not been set');}returnfetch('/user',{headers: {Authorization: `Bearer ${globalArgs.apiKey}`}});},});resolver.setGlobalArgs({apiKey: 'your-api-key'});

Dynamic Global Arguments

You can update global arguments after creating the resolver using setGlobalArgs():

constresolver=newResolver({version: 'v1'}).register({id: 'getVersion',fn: (_args,globalArgs)=>globalArgs.version,});// First resolutionconstresult1=awaitlastValueFrom(resolver.resolve());console.log(result1.globalArgs.version);// 'v1'// Update global argumentsresolver.setGlobalArgs({version: 'v2'});// Second resolution with updated argumentsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs.version);// 'v2'

Resolve Options

The resolve() method accepts an optional options parameter to control its behavior:

interfaceResolveOptions{globalArgs?: TGlobalArgs;withLoadingState?: boolean;}

globalArgs (optional)

Provides a temporary override for global arguments passed to all tasks during this specific resolution. This does not mutate the instance's globalArgs and only affects this resolution call.

  • Purpose: Allows different global arguments for specific resolutions without changing the resolver instance
  • Behavior: Overrides the instance's globalArgs for this resolution only
  • Type: Same type as the resolver's global arguments (TGlobalArgs)
constresolver=newResolver({apiKey: 'default-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchData',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/data`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});// Use temporary global args for this resolutionconstresult=awaitlastValueFrom(resolver.resolve({globalArgs: {apiKey: 'temp-key',baseUrl: 'https://temp.api.com'},}),);console.log(result.globalArgs);// { apiKey: 'temp-key', baseUrl: 'https://temp.api.com' }// Next resolution uses the original instance globalArgsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs);// { apiKey: 'default-key', baseUrl: 'https://api.example.com' }

withLoadingState (default: false)

Controls whether the resolver emits a loading state as the first value in the observable stream.

  • false (default): Only emits the final result without the loading state
  • true: Emits { loading: true } as the first value, followed by the final result
import{isLoading}from'@robinw151/resolver';// Without loading state (default behavior)resolver.resolve().subscribe((result)=>{console.log('Final result:',result);});// With loading stateresolver.resolve({withLoadingState: true}).subscribe((result)=>{if(isLoading(result)){console.log('Resolution in progress...');}else{console.log('Final result:',result);}});

Utility Functions

The resolver provides several utility functions to help you work with task results and resolver states:

isSuccess(value)

Type guard that checks if a task result contains successful data.

import{isSuccess}from'@robinw151/resolver';if(isSuccess(taskResult)){// taskResult is typed as { data: TValue }console.log(taskResult.data);}

isError(value)

Type guard that checks if a task result contains an error.

import{isError}from'@robinw151/resolver';if(isError(taskResult)){// taskResult is typed as { error: unknown }console.error(taskResult.error);}

isLoading(value)

Type guard that checks if a resolver result is in a loading state.

import{isLoading}from'@robinw151/resolver';if(isLoading(resolverResult)){console.log('Resolution is still in progress...');}

hasNoErrors(result)

Type guard that checks if all task results in a resolver result contain successful data (no errors). This function performs a runtime check to determine if every task in the result object has completed successfully.

import{hasNoErrors}from'@robinw151/resolver';constresult=awaitlastValueFrom(resolver.resolve());if(hasNoErrors(result.tasks)){// All tasks succeeded - safe to access dataconsole.log('User:',result.tasks.user.data.name);console.log('Posts count:',result.tasks.posts.data.length);}else{// Some tasks failed - handle errors appropriatelyconsole.log('Some tasks failed during resolution');}

License

MIT

About

A dependency-aware task resolver using RxJS observables for asynchronous execution

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Resolver

GitHub Actions Workflow StatusGitHub License

A dependency-aware task resolver using RxJS observables for asynchronous execution. This library allows you to define tasks with dependencies and automatically resolves them in the correct order.

Features

  • Dependency Resolution: Automatically determines the execution order based on task dependencies
  • RxJS Integration: Built on RxJS observables for powerful async handling
  • Type Safety: Full TypeScript support with type inference
  • Error Handling: Built-in error handling with graceful failure modes

Installation

npm install @robinw151/resolver
# or
pnpm add @robinw151/resolver
# or
yarn add @robinw151/resolver

Usage

Basic Example

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess}from'@robinw151/resolver';// Create a resolver instanceconstresolver=newResolver()// Register task A with no dependencies.register({id: 'A',fn: ()=>'Hello',})// Register task B with no dependencies.register({id: 'B',fn: ()=>'World',})// Register task C that depends on A and B.register({id: 'C',fn: ({ A, B })=>{if(isSuccess(A)&&isSuccess(B)){return`${A.data}${B.data}!`;}thrownewError('Missing dependencies');},},['A','B'],);// Resolve all tasksconstresult=awaitlastValueFrom(resolver.resolve());console.log(result);// { tasks: { A: { data: 'Hello' }, B: { data: 'World' }, C: { data: 'Hello World!' } }}

Advanced Example with Error Handling

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess,isError,hasNoErrors}from'@robinw151/resolver';constresolver=newResolver().register({id: 'fetchUser',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'fetchPosts',fn: ({ fetchUser })=>{if(isSuccess(fetchUser)){return[{id: 1,title: 'Post 1'},{id: 2,title: 'Post 2'},];}thrownewError('User not found');},},['fetchUser'],).register({id: 'generateReport',fn: ({ fetchUser, fetchPosts })=>{if(isSuccess(fetchUser)&&isSuccess(fetchPosts)){return{user: fetchUser.data,postCount: fetchPosts.data.length,timestamp: newDate().toISOString(),};}thrownewError('Missing data for report');},},['fetchUser','fetchPosts'],);try{constresult=awaitlastValueFrom(resolver.resolve());if(isError(result.tasks.fetchUser)){console.error('User fetch failed:',result.tasks.fetchUser.error);}if(isError(result.tasks.fetchPosts)){console.error('Posts fetch failed:',result.tasks.fetchPosts.error);}if(isError(result.tasks.generateReport)){console.error('Report generation failed:',result.tasks.generateReport.error);}if(hasNoErrors(result.tasks)){console.log('Report generated:',result.tasks.generateReport.data);}}catch(error){console.error('Resolution failed:',error);}

Type Safety

The resolver provides full TypeScript support with type inference:

import{Resolver,isSuccess,isError}from'@robinw151/resolver';// Task result types are inferred from the registered task functionsconstresolver=newResolver().register({id: 'user',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'posts',fn: ({ user })=>{// user is typed as { data: { id: number; name: string }} | { error: unknown }if(isSuccess(user)){console.log('User loaded:',user.data.name);return[{id: 1,title: 'Post 1'}];}else{console.error('User failed to load:',user.error);return[];}},},['user'],);

Task Results

Every task produces exactly one result. A task function may return a plain value, a Promise or an Observable:

import{of}from'rxjs';constresolver=newResolver().register({id: 'value',fn: ()=>1}).register({id: 'promise',fn: ()=>Promise.resolve(2)}).register({id: 'observable',fn: ()=>of(3)});

For an Observable the first emitted value becomes the task's result. The subscription is closed right after that value, so later emissions are never observed, and sources that honor unsubscription are cancelled:

// Only the first value is used, the subscription is closed afterwards
fn: ()=>of(1,2,3);// { data: 1 }// Observed as events, an HttpClient request emits `HttpEventType.Sent` first,// so the task resolves with that event and the request is cancelled
fn: ()=>http.get('/user',{observe: 'events',reportProgress: true});

Pipe the source when a different value is needed:

import{last,toArray}from'rxjs';
fn: ()=>of(1,2,3).pipe(last());// { data: 3 }
fn: ()=>of(1,2,3).pipe(toArray());// { data: [1, 2, 3] }

Keep in mind that last() and toArray() only emit once the source completes. Applying either to a source that never completes leaves the task, and therefore the whole resolution, pending indefinitely. The default behavior has no such risk, which is why an infinite source such as interval(1000) resolves with its first value instead of hanging.

A source that completes without emitting any value cannot produce a result. Such a task resolves with an EmptyTaskError instead of blocking the resolution:

import{EMPTY,lastValueFrom}from'rxjs';import{EmptyTaskError,isError,Resolver}from'@robinw151/resolver';constresult=awaitlastValueFrom(newResolver().register({id: 'empty',fn: ()=>EMPTY}).resolve());if(isError(result.tasks.empty)){console.log(result.tasks.empty.errorinstanceofEmptyTaskError);// true}

Error Handling

Tasks can return either successful data or errors. The resolver handles both cases gracefully:

  • Successful tasks return { data: TResult }
  • Failed tasks return { error: unknown }

A task whose Observable completes without emitting a value fails with an EmptyTaskError, which is exported from the package and carries the taskId of the task that produced it.

Global Arguments

The resolver supports global arguments that are passed to all task functions during execution. This is useful for sharing configuration, API keys, or other context across all tasks.

Constructor Global Arguments

You can provide global arguments when creating a resolver instance:

import{lastValueFrom}from'rxjs';import{Resolver}from'@robinw151/resolver';// Create resolver with global argumentsconstresolver=newResolver({apiKey: 'your-api-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchUser',fn: (_args,globalArgs)=>{// globalArgs is typed as { apiKey: string; baseUrl: string }returnfetch(`${globalArgs.baseUrl}/user`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},}).register({id: 'fetchPosts',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/posts`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});constresult=awaitlastValueFrom(resolver.resolve());console.log(result.globalArgs);// { apiKey: 'your-api-key', baseUrl: 'https://api.example.com' }

Because tasks receive the global arguments typed as TGlobalArgs, the constructor argument is required whenever TGlobalArgs cannot be undefined:

newResolver();// OK - no global arguments at allnewResolver({apiKey: 'your-api-key'});// OK - type is inferrednewResolver<{apiKey: string}>({apiKey: 'your-api-key'});// OK - explicit type, value providednewResolver<{apiKey: string}>();// Error - tasks would receive `undefined`

If the arguments are only known later and are supplied through setGlobalArgs() or resolve({ globalArgs }), include undefined in the type. Task functions then have to narrow it before use:

constresolver=newResolver<{apiKey: string}|undefined>().register({id: 'fetchUser',fn: (_args,globalArgs)=>{if(!globalArgs){thrownewError('Global arguments have not been set');}returnfetch('/user',{headers: {Authorization: `Bearer ${globalArgs.apiKey}`}});},});resolver.setGlobalArgs({apiKey: 'your-api-key'});

Dynamic Global Arguments

You can update global arguments after creating the resolver using setGlobalArgs():

constresolver=newResolver({version: 'v1'}).register({id: 'getVersion',fn: (_args,globalArgs)=>globalArgs.version,});// First resolutionconstresult1=awaitlastValueFrom(resolver.resolve());console.log(result1.globalArgs.version);// 'v1'// Update global argumentsresolver.setGlobalArgs({version: 'v2'});// Second resolution with updated argumentsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs.version);// 'v2'

Resolve Options

The resolve() method accepts an optional options parameter to control its behavior:

interfaceResolveOptions{globalArgs?: TGlobalArgs;withLoadingState?: boolean;}

globalArgs (optional)

Provides a temporary override for global arguments passed to all tasks during this specific resolution. This does not mutate the instance's globalArgs and only affects this resolution call.

  • Purpose: Allows different global arguments for specific resolutions without changing the resolver instance
  • Behavior: Overrides the instance's globalArgs for this resolution only
  • Type: Same type as the resolver's global arguments (TGlobalArgs)
constresolver=newResolver({apiKey: 'default-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchData',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/data`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});// Use temporary global args for this resolutionconstresult=awaitlastValueFrom(resolver.resolve({globalArgs: {apiKey: 'temp-key',baseUrl: 'https://temp.api.com'},}),);console.log(result.globalArgs);// { apiKey: 'temp-key', baseUrl: 'https://temp.api.com' }// Next resolution uses the original instance globalArgsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs);// { apiKey: 'default-key', baseUrl: 'https://api.example.com' }

withLoadingState (default: false)

Controls whether the resolver emits a loading state as the first value in the observable stream.

  • false (default): Only emits the final result without the loading state
  • true: Emits { loading: true } as the first value, followed by the final result
import{isLoading}from'@robinw151/resolver';// Without loading state (default behavior)resolver.resolve().subscribe((result)=>{console.log('Final result:',result);});// With loading stateresolver.resolve({withLoadingState: true}).subscribe((result)=>{if(isLoading(result)){console.log('Resolution in progress...');}else{console.log('Final result:',result);}});

Utility Functions

The resolver provides several utility functions to help you work with task results and resolver states:

isSuccess(value)

Type guard that checks if a task result contains successful data.

import{isSuccess}from'@robinw151/resolver';if(isSuccess(taskResult)){// taskResult is typed as { data: TValue }console.log(taskResult.data);}

isError(value)

Type guard that checks if a task result contains an error.

import{isError}from'@robinw151/resolver';if(isError(taskResult)){// taskResult is typed as { error: unknown }console.error(taskResult.error);}

isLoading(value)

Type guard that checks if a resolver result is in a loading state.

import{isLoading}from'@robinw151/resolver';if(isLoading(resolverResult)){console.log('Resolution is still in progress...');}

hasNoErrors(result)

Type guard that checks if all task results in a resolver result contain successful data (no errors). This function performs a runtime check to determine if every task in the result object has completed successfully.

import{hasNoErrors}from'@robinw151/resolver';constresult=awaitlastValueFrom(resolver.resolve());if(hasNoErrors(result.tasks)){// All tasks succeeded - safe to access dataconsole.log('User:',result.tasks.user.data.name);console.log('Posts count:',result.tasks.posts.data.length);}else{// Some tasks failed - handle errors appropriatelyconsole.log('Some tasks failed during resolution');}

License

MIT

About

A dependency-aware task resolver using RxJS observables for asynchronous execution

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Resolver

GitHub Actions Workflow StatusGitHub License

A dependency-aware task resolver using RxJS observables for asynchronous execution. This library allows you to define tasks with dependencies and automatically resolves them in the correct order.

Features

  • Dependency Resolution: Automatically determines the execution order based on task dependencies
  • RxJS Integration: Built on RxJS observables for powerful async handling
  • Type Safety: Full TypeScript support with type inference
  • Error Handling: Built-in error handling with graceful failure modes

Installation

npm install @robinw151/resolver
# or
pnpm add @robinw151/resolver
# or
yarn add @robinw151/resolver

Usage

Basic Example

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess}from'@robinw151/resolver';// Create a resolver instanceconstresolver=newResolver()// Register task A with no dependencies.register({id: 'A',fn: ()=>'Hello',})// Register task B with no dependencies.register({id: 'B',fn: ()=>'World',})// Register task C that depends on A and B.register({id: 'C',fn: ({ A, B })=>{if(isSuccess(A)&&isSuccess(B)){return`${A.data}${B.data}!`;}thrownewError('Missing dependencies');},},['A','B'],);// Resolve all tasksconstresult=awaitlastValueFrom(resolver.resolve());console.log(result);// { tasks: { A: { data: 'Hello' }, B: { data: 'World' }, C: { data: 'Hello World!' } }}

Advanced Example with Error Handling

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess,isError,hasNoErrors}from'@robinw151/resolver';constresolver=newResolver().register({id: 'fetchUser',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'fetchPosts',fn: ({ fetchUser })=>{if(isSuccess(fetchUser)){return[{id: 1,title: 'Post 1'},{id: 2,title: 'Post 2'},];}thrownewError('User not found');},},['fetchUser'],).register({id: 'generateReport',fn: ({ fetchUser, fetchPosts })=>{if(isSuccess(fetchUser)&&isSuccess(fetchPosts)){return{user: fetchUser.data,postCount: fetchPosts.data.length,timestamp: newDate().toISOString(),};}thrownewError('Missing data for report');},},['fetchUser','fetchPosts'],);try{constresult=awaitlastValueFrom(resolver.resolve());if(isError(result.tasks.fetchUser)){console.error('User fetch failed:',result.tasks.fetchUser.error);}if(isError(result.tasks.fetchPosts)){console.error('Posts fetch failed:',result.tasks.fetchPosts.error);}if(isError(result.tasks.generateReport)){console.error('Report generation failed:',result.tasks.generateReport.error);}if(hasNoErrors(result.tasks)){console.log('Report generated:',result.tasks.generateReport.data);}}catch(error){console.error('Resolution failed:',error);}

Type Safety

The resolver provides full TypeScript support with type inference:

import{Resolver,isSuccess,isError}from'@robinw151/resolver';// Task result types are inferred from the registered task functionsconstresolver=newResolver().register({id: 'user',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'posts',fn: ({ user })=>{// user is typed as { data: { id: number; name: string }} | { error: unknown }if(isSuccess(user)){console.log('User loaded:',user.data.name);return[{id: 1,title: 'Post 1'}];}else{console.error('User failed to load:',user.error);return[];}},},['user'],);

Task Results

Every task produces exactly one result. A task function may return a plain value, a Promise or an Observable:

import{of}from'rxjs';constresolver=newResolver().register({id: 'value',fn: ()=>1}).register({id: 'promise',fn: ()=>Promise.resolve(2)}).register({id: 'observable',fn: ()=>of(3)});

For an Observable the first emitted value becomes the task's result. The subscription is closed right after that value, so later emissions are never observed, and sources that honor unsubscription are cancelled:

// Only the first value is used, the subscription is closed afterwards
fn: ()=>of(1,2,3);// { data: 1 }// Observed as events, an HttpClient request emits `HttpEventType.Sent` first,// so the task resolves with that event and the request is cancelled
fn: ()=>http.get('/user',{observe: 'events',reportProgress: true});

Pipe the source when a different value is needed:

import{last,toArray}from'rxjs';
fn: ()=>of(1,2,3).pipe(last());// { data: 3 }
fn: ()=>of(1,2,3).pipe(toArray());// { data: [1, 2, 3] }

Keep in mind that last() and toArray() only emit once the source completes. Applying either to a source that never completes leaves the task, and therefore the whole resolution, pending indefinitely. The default behavior has no such risk, which is why an infinite source such as interval(1000) resolves with its first value instead of hanging.

A source that completes without emitting any value cannot produce a result. Such a task resolves with an EmptyTaskError instead of blocking the resolution:

import{EMPTY,lastValueFrom}from'rxjs';import{EmptyTaskError,isError,Resolver}from'@robinw151/resolver';constresult=awaitlastValueFrom(newResolver().register({id: 'empty',fn: ()=>EMPTY}).resolve());if(isError(result.tasks.empty)){console.log(result.tasks.empty.errorinstanceofEmptyTaskError);// true}

Error Handling

Tasks can return either successful data or errors. The resolver handles both cases gracefully:

  • Successful tasks return { data: TResult }
  • Failed tasks return { error: unknown }

A task whose Observable completes without emitting a value fails with an EmptyTaskError, which is exported from the package and carries the taskId of the task that produced it.

Global Arguments

The resolver supports global arguments that are passed to all task functions during execution. This is useful for sharing configuration, API keys, or other context across all tasks.

Constructor Global Arguments

You can provide global arguments when creating a resolver instance:

import{lastValueFrom}from'rxjs';import{Resolver}from'@robinw151/resolver';// Create resolver with global argumentsconstresolver=newResolver({apiKey: 'your-api-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchUser',fn: (_args,globalArgs)=>{// globalArgs is typed as { apiKey: string; baseUrl: string }returnfetch(`${globalArgs.baseUrl}/user`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},}).register({id: 'fetchPosts',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/posts`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});constresult=awaitlastValueFrom(resolver.resolve());console.log(result.globalArgs);// { apiKey: 'your-api-key', baseUrl: 'https://api.example.com' }

Because tasks receive the global arguments typed as TGlobalArgs, the constructor argument is required whenever TGlobalArgs cannot be undefined:

newResolver();// OK - no global arguments at allnewResolver({apiKey: 'your-api-key'});// OK - type is inferrednewResolver<{apiKey: string}>({apiKey: 'your-api-key'});// OK - explicit type, value providednewResolver<{apiKey: string}>();// Error - tasks would receive `undefined`

If the arguments are only known later and are supplied through setGlobalArgs() or resolve({ globalArgs }), include undefined in the type. Task functions then have to narrow it before use:

constresolver=newResolver<{apiKey: string}|undefined>().register({id: 'fetchUser',fn: (_args,globalArgs)=>{if(!globalArgs){thrownewError('Global arguments have not been set');}returnfetch('/user',{headers: {Authorization: `Bearer ${globalArgs.apiKey}`}});},});resolver.setGlobalArgs({apiKey: 'your-api-key'});

Dynamic Global Arguments

You can update global arguments after creating the resolver using setGlobalArgs():

constresolver=newResolver({version: 'v1'}).register({id: 'getVersion',fn: (_args,globalArgs)=>globalArgs.version,});// First resolutionconstresult1=awaitlastValueFrom(resolver.resolve());console.log(result1.globalArgs.version);// 'v1'// Update global argumentsresolver.setGlobalArgs({version: 'v2'});// Second resolution with updated argumentsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs.version);// 'v2'

Resolve Options

The resolve() method accepts an optional options parameter to control its behavior:

interfaceResolveOptions{globalArgs?: TGlobalArgs;withLoadingState?: boolean;}

globalArgs (optional)

Provides a temporary override for global arguments passed to all tasks during this specific resolution. This does not mutate the instance's globalArgs and only affects this resolution call.

  • Purpose: Allows different global arguments for specific resolutions without changing the resolver instance
  • Behavior: Overrides the instance's globalArgs for this resolution only
  • Type: Same type as the resolver's global arguments (TGlobalArgs)
constresolver=newResolver({apiKey: 'default-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchData',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/data`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});// Use temporary global args for this resolutionconstresult=awaitlastValueFrom(resolver.resolve({globalArgs: {apiKey: 'temp-key',baseUrl: 'https://temp.api.com'},}),);console.log(result.globalArgs);// { apiKey: 'temp-key', baseUrl: 'https://temp.api.com' }// Next resolution uses the original instance globalArgsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs);// { apiKey: 'default-key', baseUrl: 'https://api.example.com' }

withLoadingState (default: false)

Controls whether the resolver emits a loading state as the first value in the observable stream.

  • false (default): Only emits the final result without the loading state
  • true: Emits { loading: true } as the first value, followed by the final result
import{isLoading}from'@robinw151/resolver';// Without loading state (default behavior)resolver.resolve().subscribe((result)=>{console.log('Final result:',result);});// With loading stateresolver.resolve({withLoadingState: true}).subscribe((result)=>{if(isLoading(result)){console.log('Resolution in progress...');}else{console.log('Final result:',result);}});

Utility Functions

The resolver provides several utility functions to help you work with task results and resolver states:

isSuccess(value)

Type guard that checks if a task result contains successful data.

import{isSuccess}from'@robinw151/resolver';if(isSuccess(taskResult)){// taskResult is typed as { data: TValue }console.log(taskResult.data);}

isError(value)

Type guard that checks if a task result contains an error.

import{isError}from'@robinw151/resolver';if(isError(taskResult)){// taskResult is typed as { error: unknown }console.error(taskResult.error);}

isLoading(value)

Type guard that checks if a resolver result is in a loading state.

import{isLoading}from'@robinw151/resolver';if(isLoading(resolverResult)){console.log('Resolution is still in progress...');}

hasNoErrors(result)

Type guard that checks if all task results in a resolver result contain successful data (no errors). This function performs a runtime check to determine if every task in the result object has completed successfully.

import{hasNoErrors}from'@robinw151/resolver';constresult=awaitlastValueFrom(resolver.resolve());if(hasNoErrors(result.tasks)){// All tasks succeeded - safe to access dataconsole.log('User:',result.tasks.user.data.name);console.log('Posts count:',result.tasks.posts.data.length);}else{// Some tasks failed - handle errors appropriatelyconsole.log('Some tasks failed during resolution');}

License

MIT

About

A dependency-aware task resolver using RxJS observables for asynchronous execution

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Resolver

GitHub Actions Workflow StatusGitHub License

A dependency-aware task resolver using RxJS observables for asynchronous execution. This library allows you to define tasks with dependencies and automatically resolves them in the correct order.

Features

  • Dependency Resolution: Automatically determines the execution order based on task dependencies
  • RxJS Integration: Built on RxJS observables for powerful async handling
  • Type Safety: Full TypeScript support with type inference
  • Error Handling: Built-in error handling with graceful failure modes

Installation

npm install @robinw151/resolver
# or
pnpm add @robinw151/resolver
# or
yarn add @robinw151/resolver

Usage

Basic Example

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess}from'@robinw151/resolver';// Create a resolver instanceconstresolver=newResolver()// Register task A with no dependencies.register({id: 'A',fn: ()=>'Hello',})// Register task B with no dependencies.register({id: 'B',fn: ()=>'World',})// Register task C that depends on A and B.register({id: 'C',fn: ({ A, B })=>{if(isSuccess(A)&&isSuccess(B)){return`${A.data}${B.data}!`;}thrownewError('Missing dependencies');},},['A','B'],);// Resolve all tasksconstresult=awaitlastValueFrom(resolver.resolve());console.log(result);// { tasks: { A: { data: 'Hello' }, B: { data: 'World' }, C: { data: 'Hello World!' } }}

Advanced Example with Error Handling

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess,isError,hasNoErrors}from'@robinw151/resolver';constresolver=newResolver().register({id: 'fetchUser',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'fetchPosts',fn: ({ fetchUser })=>{if(isSuccess(fetchUser)){return[{id: 1,title: 'Post 1'},{id: 2,title: 'Post 2'},];}thrownewError('User not found');},},['fetchUser'],).register({id: 'generateReport',fn: ({ fetchUser, fetchPosts })=>{if(isSuccess(fetchUser)&&isSuccess(fetchPosts)){return{user: fetchUser.data,postCount: fetchPosts.data.length,timestamp: newDate().toISOString(),};}thrownewError('Missing data for report');},},['fetchUser','fetchPosts'],);try{constresult=awaitlastValueFrom(resolver.resolve());if(isError(result.tasks.fetchUser)){console.error('User fetch failed:',result.tasks.fetchUser.error);}if(isError(result.tasks.fetchPosts)){console.error('Posts fetch failed:',result.tasks.fetchPosts.error);}if(isError(result.tasks.generateReport)){console.error('Report generation failed:',result.tasks.generateReport.error);}if(hasNoErrors(result.tasks)){console.log('Report generated:',result.tasks.generateReport.data);}}catch(error){console.error('Resolution failed:',error);}

Type Safety

The resolver provides full TypeScript support with type inference:

import{Resolver,isSuccess,isError}from'@robinw151/resolver';// Task result types are inferred from the registered task functionsconstresolver=newResolver().register({id: 'user',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'posts',fn: ({ user })=>{// user is typed as { data: { id: number; name: string }} | { error: unknown }if(isSuccess(user)){console.log('User loaded:',user.data.name);return[{id: 1,title: 'Post 1'}];}else{console.error('User failed to load:',user.error);return[];}},},['user'],);

Task Results

Every task produces exactly one result. A task function may return a plain value, a Promise or an Observable:

import{of}from'rxjs';constresolver=newResolver().register({id: 'value',fn: ()=>1}).register({id: 'promise',fn: ()=>Promise.resolve(2)}).register({id: 'observable',fn: ()=>of(3)});

For an Observable the first emitted value becomes the task's result. The subscription is closed right after that value, so later emissions are never observed, and sources that honor unsubscription are cancelled:

// Only the first value is used, the subscription is closed afterwards
fn: ()=>of(1,2,3);// { data: 1 }// Observed as events, an HttpClient request emits `HttpEventType.Sent` first,// so the task resolves with that event and the request is cancelled
fn: ()=>http.get('/user',{observe: 'events',reportProgress: true});

Pipe the source when a different value is needed:

import{last,toArray}from'rxjs';
fn: ()=>of(1,2,3).pipe(last());// { data: 3 }
fn: ()=>of(1,2,3).pipe(toArray());// { data: [1, 2, 3] }

Keep in mind that last() and toArray() only emit once the source completes. Applying either to a source that never completes leaves the task, and therefore the whole resolution, pending indefinitely. The default behavior has no such risk, which is why an infinite source such as interval(1000) resolves with its first value instead of hanging.

A source that completes without emitting any value cannot produce a result. Such a task resolves with an EmptyTaskError instead of blocking the resolution:

import{EMPTY,lastValueFrom}from'rxjs';import{EmptyTaskError,isError,Resolver}from'@robinw151/resolver';constresult=awaitlastValueFrom(newResolver().register({id: 'empty',fn: ()=>EMPTY}).resolve());if(isError(result.tasks.empty)){console.log(result.tasks.empty.errorinstanceofEmptyTaskError);// true}

Error Handling

Tasks can return either successful data or errors. The resolver handles both cases gracefully:

  • Successful tasks return { data: TResult }
  • Failed tasks return { error: unknown }

A task whose Observable completes without emitting a value fails with an EmptyTaskError, which is exported from the package and carries the taskId of the task that produced it.

Global Arguments

The resolver supports global arguments that are passed to all task functions during execution. This is useful for sharing configuration, API keys, or other context across all tasks.

Constructor Global Arguments

You can provide global arguments when creating a resolver instance:

import{lastValueFrom}from'rxjs';import{Resolver}from'@robinw151/resolver';// Create resolver with global argumentsconstresolver=newResolver({apiKey: 'your-api-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchUser',fn: (_args,globalArgs)=>{// globalArgs is typed as { apiKey: string; baseUrl: string }returnfetch(`${globalArgs.baseUrl}/user`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},}).register({id: 'fetchPosts',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/posts`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});constresult=awaitlastValueFrom(resolver.resolve());console.log(result.globalArgs);// { apiKey: 'your-api-key', baseUrl: 'https://api.example.com' }

Because tasks receive the global arguments typed as TGlobalArgs, the constructor argument is required whenever TGlobalArgs cannot be undefined:

newResolver();// OK - no global arguments at allnewResolver({apiKey: 'your-api-key'});// OK - type is inferrednewResolver<{apiKey: string}>({apiKey: 'your-api-key'});// OK - explicit type, value providednewResolver<{apiKey: string}>();// Error - tasks would receive `undefined`

If the arguments are only known later and are supplied through setGlobalArgs() or resolve({ globalArgs }), include undefined in the type. Task functions then have to narrow it before use:

constresolver=newResolver<{apiKey: string}|undefined>().register({id: 'fetchUser',fn: (_args,globalArgs)=>{if(!globalArgs){thrownewError('Global arguments have not been set');}returnfetch('/user',{headers: {Authorization: `Bearer ${globalArgs.apiKey}`}});},});resolver.setGlobalArgs({apiKey: 'your-api-key'});

Dynamic Global Arguments

You can update global arguments after creating the resolver using setGlobalArgs():

constresolver=newResolver({version: 'v1'}).register({id: 'getVersion',fn: (_args,globalArgs)=>globalArgs.version,});// First resolutionconstresult1=awaitlastValueFrom(resolver.resolve());console.log(result1.globalArgs.version);// 'v1'// Update global argumentsresolver.setGlobalArgs({version: 'v2'});// Second resolution with updated argumentsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs.version);// 'v2'

Resolve Options

The resolve() method accepts an optional options parameter to control its behavior:

interfaceResolveOptions{globalArgs?: TGlobalArgs;withLoadingState?: boolean;}

globalArgs (optional)

Provides a temporary override for global arguments passed to all tasks during this specific resolution. This does not mutate the instance's globalArgs and only affects this resolution call.

  • Purpose: Allows different global arguments for specific resolutions without changing the resolver instance
  • Behavior: Overrides the instance's globalArgs for this resolution only
  • Type: Same type as the resolver's global arguments (TGlobalArgs)
constresolver=newResolver({apiKey: 'default-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchData',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/data`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});// Use temporary global args for this resolutionconstresult=awaitlastValueFrom(resolver.resolve({globalArgs: {apiKey: 'temp-key',baseUrl: 'https://temp.api.com'},}),);console.log(result.globalArgs);// { apiKey: 'temp-key', baseUrl: 'https://temp.api.com' }// Next resolution uses the original instance globalArgsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs);// { apiKey: 'default-key', baseUrl: 'https://api.example.com' }

withLoadingState (default: false)

Controls whether the resolver emits a loading state as the first value in the observable stream.

  • false (default): Only emits the final result without the loading state
  • true: Emits { loading: true } as the first value, followed by the final result
import{isLoading}from'@robinw151/resolver';// Without loading state (default behavior)resolver.resolve().subscribe((result)=>{console.log('Final result:',result);});// With loading stateresolver.resolve({withLoadingState: true}).subscribe((result)=>{if(isLoading(result)){console.log('Resolution in progress...');}else{console.log('Final result:',result);}});

Utility Functions

The resolver provides several utility functions to help you work with task results and resolver states:

isSuccess(value)

Type guard that checks if a task result contains successful data.

import{isSuccess}from'@robinw151/resolver';if(isSuccess(taskResult)){// taskResult is typed as { data: TValue }console.log(taskResult.data);}

isError(value)

Type guard that checks if a task result contains an error.

import{isError}from'@robinw151/resolver';if(isError(taskResult)){// taskResult is typed as { error: unknown }console.error(taskResult.error);}

isLoading(value)

Type guard that checks if a resolver result is in a loading state.

import{isLoading}from'@robinw151/resolver';if(isLoading(resolverResult)){console.log('Resolution is still in progress...');}

hasNoErrors(result)

Type guard that checks if all task results in a resolver result contain successful data (no errors). This function performs a runtime check to determine if every task in the result object has completed successfully.

import{hasNoErrors}from'@robinw151/resolver';constresult=awaitlastValueFrom(resolver.resolve());if(hasNoErrors(result.tasks)){// All tasks succeeded - safe to access dataconsole.log('User:',result.tasks.user.data.name);console.log('Posts count:',result.tasks.posts.data.length);}else{// Some tasks failed - handle errors appropriatelyconsole.log('Some tasks failed during resolution');}

License

MIT

About

A dependency-aware task resolver using RxJS observables for asynchronous execution

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Resolver

GitHub Actions Workflow StatusGitHub License

A dependency-aware task resolver using RxJS observables for asynchronous execution. This library allows you to define tasks with dependencies and automatically resolves them in the correct order.

Features

  • Dependency Resolution: Automatically determines the execution order based on task dependencies
  • RxJS Integration: Built on RxJS observables for powerful async handling
  • Type Safety: Full TypeScript support with type inference
  • Error Handling: Built-in error handling with graceful failure modes

Installation

npm install @robinw151/resolver
# or
pnpm add @robinw151/resolver
# or
yarn add @robinw151/resolver

Usage

Basic Example

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess}from'@robinw151/resolver';// Create a resolver instanceconstresolver=newResolver()// Register task A with no dependencies.register({id: 'A',fn: ()=>'Hello',})// Register task B with no dependencies.register({id: 'B',fn: ()=>'World',})// Register task C that depends on A and B.register({id: 'C',fn: ({ A, B })=>{if(isSuccess(A)&&isSuccess(B)){return`${A.data}${B.data}!`;}thrownewError('Missing dependencies');},},['A','B'],);// Resolve all tasksconstresult=awaitlastValueFrom(resolver.resolve());console.log(result);// { tasks: { A: { data: 'Hello' }, B: { data: 'World' }, C: { data: 'Hello World!' } }}

Advanced Example with Error Handling

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess,isError,hasNoErrors}from'@robinw151/resolver';constresolver=newResolver().register({id: 'fetchUser',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'fetchPosts',fn: ({ fetchUser })=>{if(isSuccess(fetchUser)){return[{id: 1,title: 'Post 1'},{id: 2,title: 'Post 2'},];}thrownewError('User not found');},},['fetchUser'],).register({id: 'generateReport',fn: ({ fetchUser, fetchPosts })=>{if(isSuccess(fetchUser)&&isSuccess(fetchPosts)){return{user: fetchUser.data,postCount: fetchPosts.data.length,timestamp: newDate().toISOString(),};}thrownewError('Missing data for report');},},['fetchUser','fetchPosts'],);try{constresult=awaitlastValueFrom(resolver.resolve());if(isError(result.tasks.fetchUser)){console.error('User fetch failed:',result.tasks.fetchUser.error);}if(isError(result.tasks.fetchPosts)){console.error('Posts fetch failed:',result.tasks.fetchPosts.error);}if(isError(result.tasks.generateReport)){console.error('Report generation failed:',result.tasks.generateReport.error);}if(hasNoErrors(result.tasks)){console.log('Report generated:',result.tasks.generateReport.data);}}catch(error){console.error('Resolution failed:',error);}

Type Safety

The resolver provides full TypeScript support with type inference:

import{Resolver,isSuccess,isError}from'@robinw151/resolver';// Task result types are inferred from the registered task functionsconstresolver=newResolver().register({id: 'user',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'posts',fn: ({ user })=>{// user is typed as { data: { id: number; name: string }} | { error: unknown }if(isSuccess(user)){console.log('User loaded:',user.data.name);return[{id: 1,title: 'Post 1'}];}else{console.error('User failed to load:',user.error);return[];}},},['user'],);

Task Results

Every task produces exactly one result. A task function may return a plain value, a Promise or an Observable:

import{of}from'rxjs';constresolver=newResolver().register({id: 'value',fn: ()=>1}).register({id: 'promise',fn: ()=>Promise.resolve(2)}).register({id: 'observable',fn: ()=>of(3)});

For an Observable the first emitted value becomes the task's result. The subscription is closed right after that value, so later emissions are never observed, and sources that honor unsubscription are cancelled:

// Only the first value is used, the subscription is closed afterwards
fn: ()=>of(1,2,3);// { data: 1 }// Observed as events, an HttpClient request emits `HttpEventType.Sent` first,// so the task resolves with that event and the request is cancelled
fn: ()=>http.get('/user',{observe: 'events',reportProgress: true});

Pipe the source when a different value is needed:

import{last,toArray}from'rxjs';
fn: ()=>of(1,2,3).pipe(last());// { data: 3 }
fn: ()=>of(1,2,3).pipe(toArray());// { data: [1, 2, 3] }

Keep in mind that last() and toArray() only emit once the source completes. Applying either to a source that never completes leaves the task, and therefore the whole resolution, pending indefinitely. The default behavior has no such risk, which is why an infinite source such as interval(1000) resolves with its first value instead of hanging.

A source that completes without emitting any value cannot produce a result. Such a task resolves with an EmptyTaskError instead of blocking the resolution:

import{EMPTY,lastValueFrom}from'rxjs';import{EmptyTaskError,isError,Resolver}from'@robinw151/resolver';constresult=awaitlastValueFrom(newResolver().register({id: 'empty',fn: ()=>EMPTY}).resolve());if(isError(result.tasks.empty)){console.log(result.tasks.empty.errorinstanceofEmptyTaskError);// true}

Error Handling

Tasks can return either successful data or errors. The resolver handles both cases gracefully:

  • Successful tasks return { data: TResult }
  • Failed tasks return { error: unknown }

A task whose Observable completes without emitting a value fails with an EmptyTaskError, which is exported from the package and carries the taskId of the task that produced it.

Global Arguments

The resolver supports global arguments that are passed to all task functions during execution. This is useful for sharing configuration, API keys, or other context across all tasks.

Constructor Global Arguments

You can provide global arguments when creating a resolver instance:

import{lastValueFrom}from'rxjs';import{Resolver}from'@robinw151/resolver';// Create resolver with global argumentsconstresolver=newResolver({apiKey: 'your-api-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchUser',fn: (_args,globalArgs)=>{// globalArgs is typed as { apiKey: string; baseUrl: string }returnfetch(`${globalArgs.baseUrl}/user`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},}).register({id: 'fetchPosts',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/posts`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});constresult=awaitlastValueFrom(resolver.resolve());console.log(result.globalArgs);// { apiKey: 'your-api-key', baseUrl: 'https://api.example.com' }

Because tasks receive the global arguments typed as TGlobalArgs, the constructor argument is required whenever TGlobalArgs cannot be undefined:

newResolver();// OK - no global arguments at allnewResolver({apiKey: 'your-api-key'});// OK - type is inferrednewResolver<{apiKey: string}>({apiKey: 'your-api-key'});// OK - explicit type, value providednewResolver<{apiKey: string}>();// Error - tasks would receive `undefined`

If the arguments are only known later and are supplied through setGlobalArgs() or resolve({ globalArgs }), include undefined in the type. Task functions then have to narrow it before use:

constresolver=newResolver<{apiKey: string}|undefined>().register({id: 'fetchUser',fn: (_args,globalArgs)=>{if(!globalArgs){thrownewError('Global arguments have not been set');}returnfetch('/user',{headers: {Authorization: `Bearer ${globalArgs.apiKey}`}});},});resolver.setGlobalArgs({apiKey: 'your-api-key'});

Dynamic Global Arguments

You can update global arguments after creating the resolver using setGlobalArgs():

constresolver=newResolver({version: 'v1'}).register({id: 'getVersion',fn: (_args,globalArgs)=>globalArgs.version,});// First resolutionconstresult1=awaitlastValueFrom(resolver.resolve());console.log(result1.globalArgs.version);// 'v1'// Update global argumentsresolver.setGlobalArgs({version: 'v2'});// Second resolution with updated argumentsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs.version);// 'v2'

Resolve Options

The resolve() method accepts an optional options parameter to control its behavior:

interfaceResolveOptions{globalArgs?: TGlobalArgs;withLoadingState?: boolean;}

globalArgs (optional)

Provides a temporary override for global arguments passed to all tasks during this specific resolution. This does not mutate the instance's globalArgs and only affects this resolution call.

  • Purpose: Allows different global arguments for specific resolutions without changing the resolver instance
  • Behavior: Overrides the instance's globalArgs for this resolution only
  • Type: Same type as the resolver's global arguments (TGlobalArgs)
constresolver=newResolver({apiKey: 'default-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchData',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/data`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});// Use temporary global args for this resolutionconstresult=awaitlastValueFrom(resolver.resolve({globalArgs: {apiKey: 'temp-key',baseUrl: 'https://temp.api.com'},}),);console.log(result.globalArgs);// { apiKey: 'temp-key', baseUrl: 'https://temp.api.com' }// Next resolution uses the original instance globalArgsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs);// { apiKey: 'default-key', baseUrl: 'https://api.example.com' }

withLoadingState (default: false)

Controls whether the resolver emits a loading state as the first value in the observable stream.

  • false (default): Only emits the final result without the loading state
  • true: Emits { loading: true } as the first value, followed by the final result
import{isLoading}from'@robinw151/resolver';// Without loading state (default behavior)resolver.resolve().subscribe((result)=>{console.log('Final result:',result);});// With loading stateresolver.resolve({withLoadingState: true}).subscribe((result)=>{if(isLoading(result)){console.log('Resolution in progress...');}else{console.log('Final result:',result);}});

Utility Functions

The resolver provides several utility functions to help you work with task results and resolver states:

isSuccess(value)

Type guard that checks if a task result contains successful data.

import{isSuccess}from'@robinw151/resolver';if(isSuccess(taskResult)){// taskResult is typed as { data: TValue }console.log(taskResult.data);}

isError(value)

Type guard that checks if a task result contains an error.

import{isError}from'@robinw151/resolver';if(isError(taskResult)){// taskResult is typed as { error: unknown }console.error(taskResult.error);}

isLoading(value)

Type guard that checks if a resolver result is in a loading state.

import{isLoading}from'@robinw151/resolver';if(isLoading(resolverResult)){console.log('Resolution is still in progress...');}

hasNoErrors(result)

Type guard that checks if all task results in a resolver result contain successful data (no errors). This function performs a runtime check to determine if every task in the result object has completed successfully.

import{hasNoErrors}from'@robinw151/resolver';constresult=awaitlastValueFrom(resolver.resolve());if(hasNoErrors(result.tasks)){// All tasks succeeded - safe to access dataconsole.log('User:',result.tasks.user.data.name);console.log('Posts count:',result.tasks.posts.data.length);}else{// Some tasks failed - handle errors appropriatelyconsole.log('Some tasks failed during resolution');}

License

MIT

About

A dependency-aware task resolver using RxJS observables for asynchronous execution

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Resolver

GitHub Actions Workflow StatusGitHub License

A dependency-aware task resolver using RxJS observables for asynchronous execution. This library allows you to define tasks with dependencies and automatically resolves them in the correct order.

Features

  • Dependency Resolution: Automatically determines the execution order based on task dependencies
  • RxJS Integration: Built on RxJS observables for powerful async handling
  • Type Safety: Full TypeScript support with type inference
  • Error Handling: Built-in error handling with graceful failure modes

Installation

npm install @robinw151/resolver
# or
pnpm add @robinw151/resolver
# or
yarn add @robinw151/resolver

Usage

Basic Example

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess}from'@robinw151/resolver';// Create a resolver instanceconstresolver=newResolver()// Register task A with no dependencies.register({id: 'A',fn: ()=>'Hello',})// Register task B with no dependencies.register({id: 'B',fn: ()=>'World',})// Register task C that depends on A and B.register({id: 'C',fn: ({ A, B })=>{if(isSuccess(A)&&isSuccess(B)){return`${A.data}${B.data}!`;}thrownewError('Missing dependencies');},},['A','B'],);// Resolve all tasksconstresult=awaitlastValueFrom(resolver.resolve());console.log(result);// { tasks: { A: { data: 'Hello' }, B: { data: 'World' }, C: { data: 'Hello World!' } }}

Advanced Example with Error Handling

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess,isError,hasNoErrors}from'@robinw151/resolver';constresolver=newResolver().register({id: 'fetchUser',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'fetchPosts',fn: ({ fetchUser })=>{if(isSuccess(fetchUser)){return[{id: 1,title: 'Post 1'},{id: 2,title: 'Post 2'},];}thrownewError('User not found');},},['fetchUser'],).register({id: 'generateReport',fn: ({ fetchUser, fetchPosts })=>{if(isSuccess(fetchUser)&&isSuccess(fetchPosts)){return{user: fetchUser.data,postCount: fetchPosts.data.length,timestamp: newDate().toISOString(),};}thrownewError('Missing data for report');},},['fetchUser','fetchPosts'],);try{constresult=awaitlastValueFrom(resolver.resolve());if(isError(result.tasks.fetchUser)){console.error('User fetch failed:',result.tasks.fetchUser.error);}if(isError(result.tasks.fetchPosts)){console.error('Posts fetch failed:',result.tasks.fetchPosts.error);}if(isError(result.tasks.generateReport)){console.error('Report generation failed:',result.tasks.generateReport.error);}if(hasNoErrors(result.tasks)){console.log('Report generated:',result.tasks.generateReport.data);}}catch(error){console.error('Resolution failed:',error);}

Type Safety

The resolver provides full TypeScript support with type inference:

import{Resolver,isSuccess,isError}from'@robinw151/resolver';// Task result types are inferred from the registered task functionsconstresolver=newResolver().register({id: 'user',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'posts',fn: ({ user })=>{// user is typed as { data: { id: number; name: string }} | { error: unknown }if(isSuccess(user)){console.log('User loaded:',user.data.name);return[{id: 1,title: 'Post 1'}];}else{console.error('User failed to load:',user.error);return[];}},},['user'],);

Task Results

Every task produces exactly one result. A task function may return a plain value, a Promise or an Observable:

import{of}from'rxjs';constresolver=newResolver().register({id: 'value',fn: ()=>1}).register({id: 'promise',fn: ()=>Promise.resolve(2)}).register({id: 'observable',fn: ()=>of(3)});

For an Observable the first emitted value becomes the task's result. The subscription is closed right after that value, so later emissions are never observed, and sources that honor unsubscription are cancelled:

// Only the first value is used, the subscription is closed afterwards
fn: ()=>of(1,2,3);// { data: 1 }// Observed as events, an HttpClient request emits `HttpEventType.Sent` first,// so the task resolves with that event and the request is cancelled
fn: ()=>http.get('/user',{observe: 'events',reportProgress: true});

Pipe the source when a different value is needed:

import{last,toArray}from'rxjs';
fn: ()=>of(1,2,3).pipe(last());// { data: 3 }
fn: ()=>of(1,2,3).pipe(toArray());// { data: [1, 2, 3] }

Keep in mind that last() and toArray() only emit once the source completes. Applying either to a source that never completes leaves the task, and therefore the whole resolution, pending indefinitely. The default behavior has no such risk, which is why an infinite source such as interval(1000) resolves with its first value instead of hanging.

A source that completes without emitting any value cannot produce a result. Such a task resolves with an EmptyTaskError instead of blocking the resolution:

import{EMPTY,lastValueFrom}from'rxjs';import{EmptyTaskError,isError,Resolver}from'@robinw151/resolver';constresult=awaitlastValueFrom(newResolver().register({id: 'empty',fn: ()=>EMPTY}).resolve());if(isError(result.tasks.empty)){console.log(result.tasks.empty.errorinstanceofEmptyTaskError);// true}

Error Handling

Tasks can return either successful data or errors. The resolver handles both cases gracefully:

  • Successful tasks return { data: TResult }
  • Failed tasks return { error: unknown }

A task whose Observable completes without emitting a value fails with an EmptyTaskError, which is exported from the package and carries the taskId of the task that produced it.

Global Arguments

The resolver supports global arguments that are passed to all task functions during execution. This is useful for sharing configuration, API keys, or other context across all tasks.

Constructor Global Arguments

You can provide global arguments when creating a resolver instance:

import{lastValueFrom}from'rxjs';import{Resolver}from'@robinw151/resolver';// Create resolver with global argumentsconstresolver=newResolver({apiKey: 'your-api-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchUser',fn: (_args,globalArgs)=>{// globalArgs is typed as { apiKey: string; baseUrl: string }returnfetch(`${globalArgs.baseUrl}/user`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},}).register({id: 'fetchPosts',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/posts`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});constresult=awaitlastValueFrom(resolver.resolve());console.log(result.globalArgs);// { apiKey: 'your-api-key', baseUrl: 'https://api.example.com' }

Because tasks receive the global arguments typed as TGlobalArgs, the constructor argument is required whenever TGlobalArgs cannot be undefined:

newResolver();// OK - no global arguments at allnewResolver({apiKey: 'your-api-key'});// OK - type is inferrednewResolver<{apiKey: string}>({apiKey: 'your-api-key'});// OK - explicit type, value providednewResolver<{apiKey: string}>();// Error - tasks would receive `undefined`

If the arguments are only known later and are supplied through setGlobalArgs() or resolve({ globalArgs }), include undefined in the type. Task functions then have to narrow it before use:

constresolver=newResolver<{apiKey: string}|undefined>().register({id: 'fetchUser',fn: (_args,globalArgs)=>{if(!globalArgs){thrownewError('Global arguments have not been set');}returnfetch('/user',{headers: {Authorization: `Bearer ${globalArgs.apiKey}`}});},});resolver.setGlobalArgs({apiKey: 'your-api-key'});

Dynamic Global Arguments

You can update global arguments after creating the resolver using setGlobalArgs():

constresolver=newResolver({version: 'v1'}).register({id: 'getVersion',fn: (_args,globalArgs)=>globalArgs.version,});// First resolutionconstresult1=awaitlastValueFrom(resolver.resolve());console.log(result1.globalArgs.version);// 'v1'// Update global argumentsresolver.setGlobalArgs({version: 'v2'});// Second resolution with updated argumentsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs.version);// 'v2'

Resolve Options

The resolve() method accepts an optional options parameter to control its behavior:

interfaceResolveOptions{globalArgs?: TGlobalArgs;withLoadingState?: boolean;}

globalArgs (optional)

Provides a temporary override for global arguments passed to all tasks during this specific resolution. This does not mutate the instance's globalArgs and only affects this resolution call.

  • Purpose: Allows different global arguments for specific resolutions without changing the resolver instance
  • Behavior: Overrides the instance's globalArgs for this resolution only
  • Type: Same type as the resolver's global arguments (TGlobalArgs)
constresolver=newResolver({apiKey: 'default-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchData',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/data`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});// Use temporary global args for this resolutionconstresult=awaitlastValueFrom(resolver.resolve({globalArgs: {apiKey: 'temp-key',baseUrl: 'https://temp.api.com'},}),);console.log(result.globalArgs);// { apiKey: 'temp-key', baseUrl: 'https://temp.api.com' }// Next resolution uses the original instance globalArgsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs);// { apiKey: 'default-key', baseUrl: 'https://api.example.com' }

withLoadingState (default: false)

Controls whether the resolver emits a loading state as the first value in the observable stream.

  • false (default): Only emits the final result without the loading state
  • true: Emits { loading: true } as the first value, followed by the final result
import{isLoading}from'@robinw151/resolver';// Without loading state (default behavior)resolver.resolve().subscribe((result)=>{console.log('Final result:',result);});// With loading stateresolver.resolve({withLoadingState: true}).subscribe((result)=>{if(isLoading(result)){console.log('Resolution in progress...');}else{console.log('Final result:',result);}});

Utility Functions

The resolver provides several utility functions to help you work with task results and resolver states:

isSuccess(value)

Type guard that checks if a task result contains successful data.

import{isSuccess}from'@robinw151/resolver';if(isSuccess(taskResult)){// taskResult is typed as { data: TValue }console.log(taskResult.data);}

isError(value)

Type guard that checks if a task result contains an error.

import{isError}from'@robinw151/resolver';if(isError(taskResult)){// taskResult is typed as { error: unknown }console.error(taskResult.error);}

isLoading(value)

Type guard that checks if a resolver result is in a loading state.

import{isLoading}from'@robinw151/resolver';if(isLoading(resolverResult)){console.log('Resolution is still in progress...');}

hasNoErrors(result)

Type guard that checks if all task results in a resolver result contain successful data (no errors). This function performs a runtime check to determine if every task in the result object has completed successfully.

import{hasNoErrors}from'@robinw151/resolver';constresult=awaitlastValueFrom(resolver.resolve());if(hasNoErrors(result.tasks)){// All tasks succeeded - safe to access dataconsole.log('User:',result.tasks.user.data.name);console.log('Posts count:',result.tasks.posts.data.length);}else{// Some tasks failed - handle errors appropriatelyconsole.log('Some tasks failed during resolution');}

License

MIT

About

A dependency-aware task resolver using RxJS observables for asynchronous execution

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Resolver

GitHub Actions Workflow StatusGitHub License

A dependency-aware task resolver using RxJS observables for asynchronous execution. This library allows you to define tasks with dependencies and automatically resolves them in the correct order.

Features

  • Dependency Resolution: Automatically determines the execution order based on task dependencies
  • RxJS Integration: Built on RxJS observables for powerful async handling
  • Type Safety: Full TypeScript support with type inference
  • Error Handling: Built-in error handling with graceful failure modes

Installation

npm install @robinw151/resolver
# or
pnpm add @robinw151/resolver
# or
yarn add @robinw151/resolver

Usage

Basic Example

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess}from'@robinw151/resolver';// Create a resolver instanceconstresolver=newResolver()// Register task A with no dependencies.register({id: 'A',fn: ()=>'Hello',})// Register task B with no dependencies.register({id: 'B',fn: ()=>'World',})// Register task C that depends on A and B.register({id: 'C',fn: ({ A, B })=>{if(isSuccess(A)&&isSuccess(B)){return`${A.data}${B.data}!`;}thrownewError('Missing dependencies');},},['A','B'],);// Resolve all tasksconstresult=awaitlastValueFrom(resolver.resolve());console.log(result);// { tasks: { A: { data: 'Hello' }, B: { data: 'World' }, C: { data: 'Hello World!' } }}

Advanced Example with Error Handling

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess,isError,hasNoErrors}from'@robinw151/resolver';constresolver=newResolver().register({id: 'fetchUser',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'fetchPosts',fn: ({ fetchUser })=>{if(isSuccess(fetchUser)){return[{id: 1,title: 'Post 1'},{id: 2,title: 'Post 2'},];}thrownewError('User not found');},},['fetchUser'],).register({id: 'generateReport',fn: ({ fetchUser, fetchPosts })=>{if(isSuccess(fetchUser)&&isSuccess(fetchPosts)){return{user: fetchUser.data,postCount: fetchPosts.data.length,timestamp: newDate().toISOString(),};}thrownewError('Missing data for report');},},['fetchUser','fetchPosts'],);try{constresult=awaitlastValueFrom(resolver.resolve());if(isError(result.tasks.fetchUser)){console.error('User fetch failed:',result.tasks.fetchUser.error);}if(isError(result.tasks.fetchPosts)){console.error('Posts fetch failed:',result.tasks.fetchPosts.error);}if(isError(result.tasks.generateReport)){console.error('Report generation failed:',result.tasks.generateReport.error);}if(hasNoErrors(result.tasks)){console.log('Report generated:',result.tasks.generateReport.data);}}catch(error){console.error('Resolution failed:',error);}

Type Safety

The resolver provides full TypeScript support with type inference:

import{Resolver,isSuccess,isError}from'@robinw151/resolver';// Task result types are inferred from the registered task functionsconstresolver=newResolver().register({id: 'user',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'posts',fn: ({ user })=>{// user is typed as { data: { id: number; name: string }} | { error: unknown }if(isSuccess(user)){console.log('User loaded:',user.data.name);return[{id: 1,title: 'Post 1'}];}else{console.error('User failed to load:',user.error);return[];}},},['user'],);

Task Results

Every task produces exactly one result. A task function may return a plain value, a Promise or an Observable:

import{of}from'rxjs';constresolver=newResolver().register({id: 'value',fn: ()=>1}).register({id: 'promise',fn: ()=>Promise.resolve(2)}).register({id: 'observable',fn: ()=>of(3)});

For an Observable the first emitted value becomes the task's result. The subscription is closed right after that value, so later emissions are never observed, and sources that honor unsubscription are cancelled:

// Only the first value is used, the subscription is closed afterwards
fn: ()=>of(1,2,3);// { data: 1 }// Observed as events, an HttpClient request emits `HttpEventType.Sent` first,// so the task resolves with that event and the request is cancelled
fn: ()=>http.get('/user',{observe: 'events',reportProgress: true});

Pipe the source when a different value is needed:

import{last,toArray}from'rxjs';
fn: ()=>of(1,2,3).pipe(last());// { data: 3 }
fn: ()=>of(1,2,3).pipe(toArray());// { data: [1, 2, 3] }

Keep in mind that last() and toArray() only emit once the source completes. Applying either to a source that never completes leaves the task, and therefore the whole resolution, pending indefinitely. The default behavior has no such risk, which is why an infinite source such as interval(1000) resolves with its first value instead of hanging.

A source that completes without emitting any value cannot produce a result. Such a task resolves with an EmptyTaskError instead of blocking the resolution:

import{EMPTY,lastValueFrom}from'rxjs';import{EmptyTaskError,isError,Resolver}from'@robinw151/resolver';constresult=awaitlastValueFrom(newResolver().register({id: 'empty',fn: ()=>EMPTY}).resolve());if(isError(result.tasks.empty)){console.log(result.tasks.empty.errorinstanceofEmptyTaskError);// true}

Error Handling

Tasks can return either successful data or errors. The resolver handles both cases gracefully:

  • Successful tasks return { data: TResult }
  • Failed tasks return { error: unknown }

A task whose Observable completes without emitting a value fails with an EmptyTaskError, which is exported from the package and carries the taskId of the task that produced it.

Global Arguments

The resolver supports global arguments that are passed to all task functions during execution. This is useful for sharing configuration, API keys, or other context across all tasks.

Constructor Global Arguments

You can provide global arguments when creating a resolver instance:

import{lastValueFrom}from'rxjs';import{Resolver}from'@robinw151/resolver';// Create resolver with global argumentsconstresolver=newResolver({apiKey: 'your-api-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchUser',fn: (_args,globalArgs)=>{// globalArgs is typed as { apiKey: string; baseUrl: string }returnfetch(`${globalArgs.baseUrl}/user`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},}).register({id: 'fetchPosts',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/posts`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});constresult=awaitlastValueFrom(resolver.resolve());console.log(result.globalArgs);// { apiKey: 'your-api-key', baseUrl: 'https://api.example.com' }

Because tasks receive the global arguments typed as TGlobalArgs, the constructor argument is required whenever TGlobalArgs cannot be undefined:

newResolver();// OK - no global arguments at allnewResolver({apiKey: 'your-api-key'});// OK - type is inferrednewResolver<{apiKey: string}>({apiKey: 'your-api-key'});// OK - explicit type, value providednewResolver<{apiKey: string}>();// Error - tasks would receive `undefined`

If the arguments are only known later and are supplied through setGlobalArgs() or resolve({ globalArgs }), include undefined in the type. Task functions then have to narrow it before use:

constresolver=newResolver<{apiKey: string}|undefined>().register({id: 'fetchUser',fn: (_args,globalArgs)=>{if(!globalArgs){thrownewError('Global arguments have not been set');}returnfetch('/user',{headers: {Authorization: `Bearer ${globalArgs.apiKey}`}});},});resolver.setGlobalArgs({apiKey: 'your-api-key'});

Dynamic Global Arguments

You can update global arguments after creating the resolver using setGlobalArgs():

constresolver=newResolver({version: 'v1'}).register({id: 'getVersion',fn: (_args,globalArgs)=>globalArgs.version,});// First resolutionconstresult1=awaitlastValueFrom(resolver.resolve());console.log(result1.globalArgs.version);// 'v1'// Update global argumentsresolver.setGlobalArgs({version: 'v2'});// Second resolution with updated argumentsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs.version);// 'v2'

Resolve Options

The resolve() method accepts an optional options parameter to control its behavior:

interfaceResolveOptions{globalArgs?: TGlobalArgs;withLoadingState?: boolean;}

globalArgs (optional)

Provides a temporary override for global arguments passed to all tasks during this specific resolution. This does not mutate the instance's globalArgs and only affects this resolution call.

  • Purpose: Allows different global arguments for specific resolutions without changing the resolver instance
  • Behavior: Overrides the instance's globalArgs for this resolution only
  • Type: Same type as the resolver's global arguments (TGlobalArgs)
constresolver=newResolver({apiKey: 'default-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchData',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/data`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});// Use temporary global args for this resolutionconstresult=awaitlastValueFrom(resolver.resolve({globalArgs: {apiKey: 'temp-key',baseUrl: 'https://temp.api.com'},}),);console.log(result.globalArgs);// { apiKey: 'temp-key', baseUrl: 'https://temp.api.com' }// Next resolution uses the original instance globalArgsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs);// { apiKey: 'default-key', baseUrl: 'https://api.example.com' }

withLoadingState (default: false)

Controls whether the resolver emits a loading state as the first value in the observable stream.

  • false (default): Only emits the final result without the loading state
  • true: Emits { loading: true } as the first value, followed by the final result
import{isLoading}from'@robinw151/resolver';// Without loading state (default behavior)resolver.resolve().subscribe((result)=>{console.log('Final result:',result);});// With loading stateresolver.resolve({withLoadingState: true}).subscribe((result)=>{if(isLoading(result)){console.log('Resolution in progress...');}else{console.log('Final result:',result);}});

Utility Functions

The resolver provides several utility functions to help you work with task results and resolver states:

isSuccess(value)

Type guard that checks if a task result contains successful data.

import{isSuccess}from'@robinw151/resolver';if(isSuccess(taskResult)){// taskResult is typed as { data: TValue }console.log(taskResult.data);}

isError(value)

Type guard that checks if a task result contains an error.

import{isError}from'@robinw151/resolver';if(isError(taskResult)){// taskResult is typed as { error: unknown }console.error(taskResult.error);}

isLoading(value)

Type guard that checks if a resolver result is in a loading state.

import{isLoading}from'@robinw151/resolver';if(isLoading(resolverResult)){console.log('Resolution is still in progress...');}

hasNoErrors(result)

Type guard that checks if all task results in a resolver result contain successful data (no errors). This function performs a runtime check to determine if every task in the result object has completed successfully.

import{hasNoErrors}from'@robinw151/resolver';constresult=awaitlastValueFrom(resolver.resolve());if(hasNoErrors(result.tasks)){// All tasks succeeded - safe to access dataconsole.log('User:',result.tasks.user.data.name);console.log('Posts count:',result.tasks.posts.data.length);}else{// Some tasks failed - handle errors appropriatelyconsole.log('Some tasks failed during resolution');}

License

MIT

About

A dependency-aware task resolver using RxJS observables for asynchronous execution

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Resolver

GitHub Actions Workflow StatusGitHub License

A dependency-aware task resolver using RxJS observables for asynchronous execution. This library allows you to define tasks with dependencies and automatically resolves them in the correct order.

Features

  • Dependency Resolution: Automatically determines the execution order based on task dependencies
  • RxJS Integration: Built on RxJS observables for powerful async handling
  • Type Safety: Full TypeScript support with type inference
  • Error Handling: Built-in error handling with graceful failure modes

Installation

npm install @robinw151/resolver
# or
pnpm add @robinw151/resolver
# or
yarn add @robinw151/resolver

Usage

Basic Example

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess}from'@robinw151/resolver';// Create a resolver instanceconstresolver=newResolver()// Register task A with no dependencies.register({id: 'A',fn: ()=>'Hello',})// Register task B with no dependencies.register({id: 'B',fn: ()=>'World',})// Register task C that depends on A and B.register({id: 'C',fn: ({ A, B })=>{if(isSuccess(A)&&isSuccess(B)){return`${A.data}${B.data}!`;}thrownewError('Missing dependencies');},},['A','B'],);// Resolve all tasksconstresult=awaitlastValueFrom(resolver.resolve());console.log(result);// { tasks: { A: { data: 'Hello' }, B: { data: 'World' }, C: { data: 'Hello World!' } }}

Advanced Example with Error Handling

import{lastValueFrom}from'rxjs';import{Resolver,isSuccess,isError,hasNoErrors}from'@robinw151/resolver';constresolver=newResolver().register({id: 'fetchUser',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'fetchPosts',fn: ({ fetchUser })=>{if(isSuccess(fetchUser)){return[{id: 1,title: 'Post 1'},{id: 2,title: 'Post 2'},];}thrownewError('User not found');},},['fetchUser'],).register({id: 'generateReport',fn: ({ fetchUser, fetchPosts })=>{if(isSuccess(fetchUser)&&isSuccess(fetchPosts)){return{user: fetchUser.data,postCount: fetchPosts.data.length,timestamp: newDate().toISOString(),};}thrownewError('Missing data for report');},},['fetchUser','fetchPosts'],);try{constresult=awaitlastValueFrom(resolver.resolve());if(isError(result.tasks.fetchUser)){console.error('User fetch failed:',result.tasks.fetchUser.error);}if(isError(result.tasks.fetchPosts)){console.error('Posts fetch failed:',result.tasks.fetchPosts.error);}if(isError(result.tasks.generateReport)){console.error('Report generation failed:',result.tasks.generateReport.error);}if(hasNoErrors(result.tasks)){console.log('Report generated:',result.tasks.generateReport.data);}}catch(error){console.error('Resolution failed:',error);}

Type Safety

The resolver provides full TypeScript support with type inference:

import{Resolver,isSuccess,isError}from'@robinw151/resolver';// Task result types are inferred from the registered task functionsconstresolver=newResolver().register({id: 'user',fn: ()=>({id: 1,name: 'John'}),}).register({id: 'posts',fn: ({ user })=>{// user is typed as { data: { id: number; name: string }} | { error: unknown }if(isSuccess(user)){console.log('User loaded:',user.data.name);return[{id: 1,title: 'Post 1'}];}else{console.error('User failed to load:',user.error);return[];}},},['user'],);

Task Results

Every task produces exactly one result. A task function may return a plain value, a Promise or an Observable:

import{of}from'rxjs';constresolver=newResolver().register({id: 'value',fn: ()=>1}).register({id: 'promise',fn: ()=>Promise.resolve(2)}).register({id: 'observable',fn: ()=>of(3)});

For an Observable the first emitted value becomes the task's result. The subscription is closed right after that value, so later emissions are never observed, and sources that honor unsubscription are cancelled:

// Only the first value is used, the subscription is closed afterwards
fn: ()=>of(1,2,3);// { data: 1 }// Observed as events, an HttpClient request emits `HttpEventType.Sent` first,// so the task resolves with that event and the request is cancelled
fn: ()=>http.get('/user',{observe: 'events',reportProgress: true});

Pipe the source when a different value is needed:

import{last,toArray}from'rxjs';
fn: ()=>of(1,2,3).pipe(last());// { data: 3 }
fn: ()=>of(1,2,3).pipe(toArray());// { data: [1, 2, 3] }

Keep in mind that last() and toArray() only emit once the source completes. Applying either to a source that never completes leaves the task, and therefore the whole resolution, pending indefinitely. The default behavior has no such risk, which is why an infinite source such as interval(1000) resolves with its first value instead of hanging.

A source that completes without emitting any value cannot produce a result. Such a task resolves with an EmptyTaskError instead of blocking the resolution:

import{EMPTY,lastValueFrom}from'rxjs';import{EmptyTaskError,isError,Resolver}from'@robinw151/resolver';constresult=awaitlastValueFrom(newResolver().register({id: 'empty',fn: ()=>EMPTY}).resolve());if(isError(result.tasks.empty)){console.log(result.tasks.empty.errorinstanceofEmptyTaskError);// true}

Error Handling

Tasks can return either successful data or errors. The resolver handles both cases gracefully:

  • Successful tasks return { data: TResult }
  • Failed tasks return { error: unknown }

A task whose Observable completes without emitting a value fails with an EmptyTaskError, which is exported from the package and carries the taskId of the task that produced it.

Global Arguments

The resolver supports global arguments that are passed to all task functions during execution. This is useful for sharing configuration, API keys, or other context across all tasks.

Constructor Global Arguments

You can provide global arguments when creating a resolver instance:

import{lastValueFrom}from'rxjs';import{Resolver}from'@robinw151/resolver';// Create resolver with global argumentsconstresolver=newResolver({apiKey: 'your-api-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchUser',fn: (_args,globalArgs)=>{// globalArgs is typed as { apiKey: string; baseUrl: string }returnfetch(`${globalArgs.baseUrl}/user`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},}).register({id: 'fetchPosts',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/posts`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});constresult=awaitlastValueFrom(resolver.resolve());console.log(result.globalArgs);// { apiKey: 'your-api-key', baseUrl: 'https://api.example.com' }

Because tasks receive the global arguments typed as TGlobalArgs, the constructor argument is required whenever TGlobalArgs cannot be undefined:

newResolver();// OK - no global arguments at allnewResolver({apiKey: 'your-api-key'});// OK - type is inferrednewResolver<{apiKey: string}>({apiKey: 'your-api-key'});// OK - explicit type, value providednewResolver<{apiKey: string}>();// Error - tasks would receive `undefined`

If the arguments are only known later and are supplied through setGlobalArgs() or resolve({ globalArgs }), include undefined in the type. Task functions then have to narrow it before use:

constresolver=newResolver<{apiKey: string}|undefined>().register({id: 'fetchUser',fn: (_args,globalArgs)=>{if(!globalArgs){thrownewError('Global arguments have not been set');}returnfetch('/user',{headers: {Authorization: `Bearer ${globalArgs.apiKey}`}});},});resolver.setGlobalArgs({apiKey: 'your-api-key'});

Dynamic Global Arguments

You can update global arguments after creating the resolver using setGlobalArgs():

constresolver=newResolver({version: 'v1'}).register({id: 'getVersion',fn: (_args,globalArgs)=>globalArgs.version,});// First resolutionconstresult1=awaitlastValueFrom(resolver.resolve());console.log(result1.globalArgs.version);// 'v1'// Update global argumentsresolver.setGlobalArgs({version: 'v2'});// Second resolution with updated argumentsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs.version);// 'v2'

Resolve Options

The resolve() method accepts an optional options parameter to control its behavior:

interfaceResolveOptions{globalArgs?: TGlobalArgs;withLoadingState?: boolean;}

globalArgs (optional)

Provides a temporary override for global arguments passed to all tasks during this specific resolution. This does not mutate the instance's globalArgs and only affects this resolution call.

  • Purpose: Allows different global arguments for specific resolutions without changing the resolver instance
  • Behavior: Overrides the instance's globalArgs for this resolution only
  • Type: Same type as the resolver's global arguments (TGlobalArgs)
constresolver=newResolver({apiKey: 'default-key',baseUrl: 'https://api.example.com'}).register({id: 'fetchData',fn: (_args,globalArgs)=>{returnfetch(`${globalArgs.baseUrl}/data`,{headers: {Authorization: `Bearer ${globalArgs.apiKey}`},});},});// Use temporary global args for this resolutionconstresult=awaitlastValueFrom(resolver.resolve({globalArgs: {apiKey: 'temp-key',baseUrl: 'https://temp.api.com'},}),);console.log(result.globalArgs);// { apiKey: 'temp-key', baseUrl: 'https://temp.api.com' }// Next resolution uses the original instance globalArgsconstresult2=awaitlastValueFrom(resolver.resolve());console.log(result2.globalArgs);// { apiKey: 'default-key', baseUrl: 'https://api.example.com' }

withLoadingState (default: false)

Controls whether the resolver emits a loading state as the first value in the observable stream.

  • false (default): Only emits the final result without the loading state
  • true: Emits { loading: true } as the first value, followed by the final result
import{isLoading}from'@robinw151/resolver';// Without loading state (default behavior)resolver.resolve().subscribe((result)=>{console.log('Final result:',result);});// With loading stateresolver.resolve({withLoadingState: true}).subscribe((result)=>{if(isLoading(result)){console.log('Resolution in progress...');}else{console.log('Final result:',result);}});

Utility Functions

The resolver provides several utility functions to help you work with task results and resolver states:

isSuccess(value)

Type guard that checks if a task result contains successful data.

import{isSuccess}from'@robinw151/resolver';if(isSuccess(taskResult)){// taskResult is typed as { data: TValue }console.log(taskResult.data);}

isError(value)

Type guard that checks if a task result contains an error.

import{isError}from'@robinw151/resolver';if(isError(taskResult)){// taskResult is typed as { error: unknown }console.error(taskResult.error);}

isLoading(value)

Type guard that checks if a resolver result is in a loading state.

import{isLoading}from'@robinw151/resolver';if(isLoading(resolverResult)){console.log('Resolution is still in progress...');}

hasNoErrors(result)

Type guard that checks if all task results in a resolver result contain successful data (no errors). This function performs a runtime check to determine if every task in the result object has completed successfully.

import{hasNoErrors}from'@robinw151/resolver';constresult=awaitlastValueFrom(resolver.resolve());if(hasNoErrors(result.tasks)){// All tasks succeeded - safe to access dataconsole.log('User:',result.tasks.user.data.name);console.log('Posts count:',result.tasks.posts.data.length);}else{// Some tasks failed - handle errors appropriatelyconsole.log('Some tasks failed during resolution');}

License

MIT

About

A dependency-aware task resolver using RxJS observables for asynchronous execution

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages