@dungarees/core 0.11.1 → 0.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/application.d.ts CHANGED
@@ -1,12 +1,15 @@
1
+ import type { JsonType } from './type-util.ts';
1
2
  import { type OptionalPatternList } from './util.ts';
2
3
  export type Application<CONFIG extends Partial<ApplicationTypeConfig> = DefaultApplicationTypeConfig> = {
3
- run: (identity?: CONFIG['identity'], overrides?: Partial<SingleApplicationArgs<ApplicationTypeConfigWithDefaults<CONFIG>>>) => ApplicationRunReturn<CONFIG>;
4
+ run: (...args: RunArgs<CONFIG>) => ApplicationRunReturn<CONFIG>;
4
5
  getArgs: () => Array<Partial<CreateApplicationArgs<CONFIG>>>;
5
6
  };
6
7
  export declare const createApplication: <const CONFIG extends Partial<ApplicationTypeConfig> = DefaultApplicationTypeConfig>(appArgs?: Partial<CreateApplicationArgs<ApplicationTypeConfigWithDefaults<CONFIG>>>, otherApp?: () => Application<ApplicationTypeConfigWithDefaults<CONFIG>>) => Application<ApplicationTypeConfigWithDefaults<CONFIG>>;
8
+ type RunArgs<CONFIG extends Partial<ApplicationTypeConfig>> = undefined extends CONFIG['identity'] ? [identity?: CONFIG['identity'], overrides?: RunOverrides<CONFIG>] : [identity: CONFIG['identity'], overrides?: RunOverrides<CONFIG>];
9
+ type RunOverrides<CONFIG extends Partial<ApplicationTypeConfig>> = Partial<SingleApplicationArgs<ApplicationTypeConfigWithDefaults<CONFIG>>>;
7
10
  export type SingleApplicationArgs<CONFIG extends Partial<ApplicationTypeConfig>> = {
8
- getExternalServices: GetExternalServices<CONFIG>;
9
- getInternalServices: GetInternalServices<CONFIG>;
11
+ getServices: GetServices<CONFIG>;
12
+ getBehaviors: GetBehaviors<CONFIG>;
10
13
  preMain: PreMain<CONFIG>;
11
14
  getDelivery: GetDelivery<CONFIG>;
12
15
  main: Main<CONFIG>;
@@ -19,45 +22,48 @@ export type CreateApplicationArgs<CONFIG extends Partial<ApplicationTypeConfig>>
19
22
  [KEY in keyof SingleApplicationArgs<CONFIG>]: OptionalPatternList<SingleApplicationArgs<CONFIG>[KEY], CONFIG['identity']>;
20
23
  };
21
24
  export type ApplicationTypeConfig = {
22
- externalServices: Record<string, any>;
23
- internalServices: Record<string, any>;
24
- delivery: Record<string, any>;
25
- output: any;
26
- identity: any;
27
- exportState: any;
25
+ services: Record<string, unknown>;
26
+ behaviors: Record<string, unknown>;
27
+ delivery: Record<string, unknown>;
28
+ output: unknown;
29
+ identity: JsonType;
30
+ exportState: unknown;
28
31
  };
29
32
  type DefaultApplicationTypeConfig = {
30
- externalServices: Record<string, never>;
31
- internalServices: Record<string, never>;
33
+ services: Record<string, never>;
34
+ behaviors: Record<string, never>;
32
35
  delivery: Record<string, never>;
33
36
  output: undefined;
34
37
  identity: undefined;
35
38
  exportState: undefined;
36
39
  };
37
- type GetExternalServices<CONFIG extends Partial<ApplicationTypeConfig>> = (identity: CONFIG['identity']) => CONFIG['externalServices'];
38
- type GetInternalServices<CONFIG extends Partial<ApplicationTypeConfig>> = (externalServices: CONFIG['externalServices'], identity: CONFIG['identity']) => CONFIG['internalServices'];
39
- type PreMain<CONFIG extends Partial<ApplicationTypeConfig>> = (internalServices: CONFIG['internalServices'], identity: CONFIG['identity']) => void;
40
+ type GetServices<CONFIG extends Partial<ApplicationTypeConfig>> = (identity: CONFIG['identity']) => CONFIG['services'];
41
+ type GetBehaviors<CONFIG extends Partial<ApplicationTypeConfig>> = (services: CONFIG['services'], identity: CONFIG['identity']) => CONFIG['behaviors'];
42
+ type PreMain<CONFIG extends Partial<ApplicationTypeConfig>> = (behaviors: CONFIG['behaviors'], identity: CONFIG['identity']) => void;
40
43
  type GetDelivery<CONFIG extends Partial<ApplicationTypeConfig>> = (injected: {
41
- internalServices: CONFIG['internalServices'];
44
+ services: CONFIG['services'];
45
+ behaviors: CONFIG['behaviors'];
42
46
  exportState: () => CONFIG['exportState'];
43
47
  }, identity: CONFIG['identity']) => CONFIG['delivery'];
44
48
  type Main<CONFIG extends Partial<ApplicationTypeConfig>> = (injected: {
45
- internalServices: CONFIG['internalServices'];
49
+ services: CONFIG['services'];
50
+ behaviors: CONFIG['behaviors'];
46
51
  delivery: CONFIG['delivery'];
47
52
  }, identity: CONFIG['identity']) => CONFIG['output'];
48
53
  type OnError = (error: unknown) => void;
49
54
  type TopLevelErrorHandling = (onError: (error: unknown) => void) => void;
50
- type ExportState<CONFIG extends Partial<ApplicationTypeConfig>> = (internalServices: CONFIG['internalServices']) => CONFIG['exportState'];
51
- type ImportState<CONFIG extends Partial<ApplicationTypeConfig>> = (internalServices: CONFIG['internalServices'], newState: CONFIG['exportState']) => void;
55
+ type ExportState<CONFIG extends Partial<ApplicationTypeConfig>> = (behaviors: CONFIG['behaviors']) => CONFIG['exportState'];
56
+ type ImportState<CONFIG extends Partial<ApplicationTypeConfig>> = (behaviors: CONFIG['behaviors'], newState: CONFIG['exportState']) => void;
57
+ type RecordShapedKey = 'services' | 'behaviors' | 'delivery';
52
58
  type ApplicationTypeConfigWithDefaults<CONFIG extends Partial<ApplicationTypeConfig>> = {
53
- [KEY in keyof DefaultApplicationTypeConfig]: CONFIG[KEY] extends ApplicationTypeConfig[KEY] ? CONFIG[KEY] : DefaultApplicationTypeConfig[KEY];
59
+ [KEY in keyof DefaultApplicationTypeConfig]: KEY extends RecordShapedKey ? CONFIG[KEY] extends ApplicationTypeConfig[KEY] ? CONFIG[KEY] : DefaultApplicationTypeConfig[KEY] : CONFIG[KEY];
54
60
  };
55
61
  export type ApplicationTypeConfigWithAnys<CONFIG extends Partial<ApplicationTypeConfig>> = {
56
62
  [KEY in keyof ApplicationTypeConfig]: CONFIG[KEY] extends ApplicationTypeConfig[KEY] ? CONFIG[KEY] : ApplicationTypeConfig[KEY];
57
63
  };
58
64
  export type ApplicationRunReturn<CONFIG extends Partial<ApplicationTypeConfig>> = {
59
- externalServices: CONFIG['externalServices'];
60
- internalServices: CONFIG['internalServices'];
65
+ services: CONFIG['services'];
66
+ behaviors: CONFIG['behaviors'];
61
67
  delivery: CONFIG['delivery'];
62
68
  output: CONFIG['output'];
63
69
  importState: (newState: CONFIG['exportState']) => void;
package/application.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { assertDefined, findByPattern, optionalPatternToList, } from './util.js';
2
2
  export const createApplication = (appArgs = {}, otherApp) => {
3
3
  const DEFAULTS = {
4
- getExternalServices: () => ({}),
5
- getInternalServices: () => ({}),
4
+ getServices: () => ({}),
5
+ getBehaviors: () => ({}),
6
6
  preMain: () => { },
7
7
  getDelivery: () => ({}),
8
8
  main: () => { },
@@ -23,7 +23,6 @@ export const createApplication = (appArgs = {}, otherApp) => {
23
23
  }));
24
24
  const registerArg = (config, key, argPatternLists) => {
25
25
  if (config[key] !== undefined) {
26
- const oldArgPatterns = argPatternLists[key];
27
26
  argPatternLists[key] = [
28
27
  ...argPatternLists[key],
29
28
  ...optionalPatternToList(config[key]),
@@ -37,33 +36,33 @@ export const createApplication = (appArgs = {}, otherApp) => {
37
36
  return argPatternLists;
38
37
  };
39
38
  const app = {
40
- run: (runIdentity, overrides = {}) => {
39
+ run: (...[runIdentity, overrides = {}]) => {
41
40
  const [baseArg, ...restArgs] = app.getArgs();
42
41
  const firstArgsWithDefaults = {
43
42
  ...DEFAULTS,
44
43
  ...baseArg,
45
44
  };
46
45
  const argPatternLists = restArgs.reduce((list, config) => register(config, list), toPatternLists(firstArgsWithDefaults));
47
- const { getExternalServices, getInternalServices, preMain, getDelivery, main, onError, topLevelErrorHandling, exportState: exportStateOriginal, importState, } = {
46
+ const { getServices, getBehaviors, preMain, getDelivery, main, onError, topLevelErrorHandling, exportState: exportStateOriginal, importState, } = {
48
47
  ...getDefaultedArgs(argPatternLists, runIdentity),
49
48
  ...overrides,
50
49
  };
51
50
  try {
52
51
  topLevelErrorHandling(onError);
53
- const externalServices = getExternalServices(runIdentity);
54
- const internalServices = getInternalServices(externalServices, runIdentity);
55
- preMain(internalServices, runIdentity);
56
- const exportState = () => exportStateOriginal(internalServices);
57
- const delivery = getDelivery({ internalServices, exportState }, runIdentity);
58
- const output = main({ delivery, internalServices }, runIdentity);
52
+ const services = getServices(runIdentity);
53
+ const behaviors = getBehaviors(services, runIdentity);
54
+ preMain(behaviors, runIdentity);
55
+ const exportState = () => exportStateOriginal(behaviors);
56
+ const delivery = getDelivery({ services, behaviors, exportState }, runIdentity);
57
+ const output = main({ services, delivery, behaviors }, runIdentity);
59
58
  return {
60
- externalServices,
61
- internalServices,
59
+ services,
60
+ behaviors,
62
61
  delivery,
63
62
  output,
64
63
  exportState,
65
64
  importState: (newState) => {
66
- importState(internalServices, newState);
65
+ importState(behaviors, newState);
67
66
  },
68
67
  };
69
68
  }
@@ -73,7 +72,7 @@ export const createApplication = (appArgs = {}, otherApp) => {
73
72
  }
74
73
  },
75
74
  getArgs: () => {
76
- return (otherApp !== undefined ? [...otherApp().getArgs(), appArgs] : [appArgs]);
75
+ return otherApp !== undefined ? [...otherApp().getArgs(), appArgs] : [appArgs];
77
76
  },
78
77
  };
79
78
  return app;
package/error.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export declare const getErrorMessage: (cause: unknown) => string;
2
+ export declare const createCausedError: ({ message, cause }: {
3
+ message: string;
4
+ cause: unknown;
5
+ }) => Error;
6
+ export declare const getThrownError: (run: () => unknown) => Error;
7
+ export declare const isAbortError: (cause: unknown) => boolean;
8
+ export declare const isNetworkError: (cause: unknown) => boolean;
package/error.js ADDED
@@ -0,0 +1,31 @@
1
+ export const getErrorMessage = (cause) => cause instanceof Error ? cause.message : String(cause);
2
+ export const createCausedError = ({ message, cause }) => new Error(`${message}: ${getErrorMessage(cause)}`, { cause });
3
+ export const getThrownError = (run) => {
4
+ try {
5
+ run();
6
+ }
7
+ catch (cause) {
8
+ if (cause instanceof Error) {
9
+ return cause;
10
+ }
11
+ throw new Error(`Expected an Error to be thrown, got: ${getErrorMessage(cause)}`);
12
+ }
13
+ throw new Error('Expected a throw, but nothing was thrown');
14
+ };
15
+ // Reads the property without asserting a shape, so a rejection carrying a plain object is treated
16
+ // the same as one carrying an Error.
17
+ const readStringProperty = (value, property) => {
18
+ if (typeof value !== 'object' || value === null || !(property in value)) {
19
+ return undefined;
20
+ }
21
+ const read = Reflect.get(value, property);
22
+ return typeof read === 'string' ? read : undefined;
23
+ };
24
+ // The message is checked as well as the name because a cancelled request is often re-thrown
25
+ // wrapped, which keeps the reason in the message but resets the name to plain 'Error'.
26
+ export const isAbortError = (cause) => readStringProperty(cause, 'name') === 'AbortError' ||
27
+ /AbortError/i.test(readStringProperty(cause, 'message') ?? '');
28
+ // A fetch that never reached the network rejects with a TypeError whose message is the only thing
29
+ // distinguishing it from a programming error, and each engine words it differently.
30
+ const NETWORK_FAILURE_MESSAGE = /Load failed/i;
31
+ export const isNetworkError = (cause) => cause instanceof TypeError && NETWORK_FAILURE_MESSAGE.test(cause.message);
@@ -0,0 +1 @@
1
+ export {};
package/error.test.js ADDED
@@ -0,0 +1,72 @@
1
+ import { createCausedError, getErrorMessage, getThrownError, isAbortError, isNetworkError, } from './error.js';
2
+ import { expect, test } from 'vitest';
3
+ test('getErrorMessage reads the message off an Error', () => {
4
+ expect(getErrorMessage(new Error('it broke'))).toBe('it broke');
5
+ });
6
+ test('getErrorMessage stringifies anything else that was thrown', () => {
7
+ expect(getErrorMessage('a bare string')).toBe('a bare string');
8
+ expect(getErrorMessage(undefined)).toBe('undefined');
9
+ expect(getErrorMessage(404)).toBe('404');
10
+ });
11
+ test('getErrorMessage reads a subclass like any other Error', () => {
12
+ class ParseError extends Error {
13
+ }
14
+ expect(getErrorMessage(new ParseError('bad json'))).toBe('bad json');
15
+ });
16
+ test('createCausedError prefixes the message of the error it is given', () => {
17
+ expect(createCausedError({ message: 'Invalid package.json', cause: new Error('bad json') }).message).toBe('Invalid package.json: bad json');
18
+ });
19
+ test('createCausedError keeps the original as the cause, so the chain survives', () => {
20
+ const original = new Error('bad json');
21
+ expect(createCausedError({ message: 'Invalid package.json', cause: original }).cause).toBe(original);
22
+ });
23
+ test('createCausedError works on a non-Error cause without losing it', () => {
24
+ const caused = createCausedError({ message: 'Invalid package.json', cause: 'thrown string' });
25
+ expect(caused.message).toBe('Invalid package.json: thrown string');
26
+ expect(caused.cause).toBe('thrown string');
27
+ });
28
+ test('getThrownError hands back the Error that was thrown, cause and all', () => {
29
+ const cause = new Error('bad literal');
30
+ const thrown = getThrownError(() => {
31
+ throw new Error('Invalid type in store', { cause });
32
+ });
33
+ expect(thrown.message).toBe('Invalid type in store');
34
+ expect(thrown.cause).toBe(cause);
35
+ });
36
+ test('getThrownError fails when nothing is thrown, rather than returning undefined', () => {
37
+ expect(() => getThrownError(() => 'no throw here')).toThrow('Expected a throw');
38
+ });
39
+ test('getThrownError fails when the thrown value is not an Error', () => {
40
+ const notAnError = 'a bare string';
41
+ expect(() => getThrownError(() => {
42
+ throw notAnError;
43
+ })).toThrow('Expected an Error to be thrown, got: a bare string');
44
+ });
45
+ test('isAbortError recognises the DOMException a real abort produces', () => {
46
+ const controller = new AbortController();
47
+ controller.abort();
48
+ expect(isAbortError(controller.signal.reason)).toBe(true);
49
+ });
50
+ test('isAbortError recognises an error that only names AbortError in its message', () => {
51
+ expect(isAbortError(new Error('Fetch failed: AbortError'))).toBe(true);
52
+ });
53
+ test('isAbortError rejects an ordinary error', () => {
54
+ expect(isAbortError(new Error('it broke'))).toBe(false);
55
+ });
56
+ test('isAbortError rejects values that carry no name or message at all', () => {
57
+ expect(isAbortError(null)).toBe(false);
58
+ expect(isAbortError(undefined)).toBe(false);
59
+ expect(isAbortError('AbortError')).toBe(false);
60
+ });
61
+ test('isNetworkError recognises the TypeError a failed fetch throws', () => {
62
+ expect(isNetworkError(new TypeError('Load failed'))).toBe(true);
63
+ });
64
+ test('isNetworkError rejects a TypeError thrown for any other reason', () => {
65
+ expect(isNetworkError(new TypeError('x is not a function'))).toBe(false);
66
+ });
67
+ test('isNetworkError rejects a matching message on an error that is not a TypeError', () => {
68
+ expect(isNetworkError(new Error('Load failed'))).toBe(false);
69
+ });
70
+ test('isNetworkError rejects a nullish value', () => {
71
+ expect(isNetworkError(null)).toBe(false);
72
+ });
package/event.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { FromKebabCase, Serializable, ToCamelCase } from './type-util.ts';
2
+ export type DomainEvent<TYPE extends string = string, PAYLOAD extends Serializable = Serializable> = {
3
+ type: TYPE;
4
+ payload: PAYLOAD;
5
+ };
6
+ export type DomainEventOf<PAYLOADS extends Record<string, Serializable>> = {
7
+ [TYPE in keyof PAYLOADS & string]: DomainEvent<TYPE, PAYLOADS[TYPE]>;
8
+ }[keyof PAYLOADS & string];
9
+ export type EventCreators<PAYLOADS extends Record<string, Serializable>> = {
10
+ [TYPE in keyof PAYLOADS & string as ToCamelCase<FromKebabCase<TYPE>>]: undefined extends PAYLOADS[TYPE] ? () => DomainEvent<TYPE, PAYLOADS[TYPE]> : (payload: PAYLOADS[TYPE]) => DomainEvent<TYPE, PAYLOADS[TYPE]>;
11
+ };
12
+ export declare const createEventCreators: <PAYLOADS extends Record<string, Serializable>>() => EventCreators<PAYLOADS>;
package/event.js ADDED
@@ -0,0 +1,8 @@
1
+ import { camelCase2kebabCase } from './util.js';
2
+ // The cast is unavoidable: a Proxy target cannot be statically typed as the generated shape,
3
+ // and no type-safe construction of a dynamic keyed object exists.
4
+ export const createEventCreators = () => new Proxy({}, {
5
+ get: (_target, name) => typeof name === 'string'
6
+ ? (payload) => ({ type: camelCase2kebabCase(name), payload })
7
+ : undefined,
8
+ });
@@ -0,0 +1 @@
1
+ export {};
package/event.test.js ADDED
@@ -0,0 +1,22 @@
1
+ import { createEventCreators } from './event.js';
2
+ import { expect, expectTypeOf, test } from 'vitest';
3
+ test('DomainEventOf derives the discriminated union from the payload map', () => {
4
+ expectTypeOf().toEqualTypeOf();
5
+ });
6
+ test('createEventCreators builds a creator that emits its event type and payload', () => {
7
+ const creators = createEventCreators();
8
+ expect(creators.greet({ name: 'Ada' })).toEqual({ type: 'greet', payload: { name: 'Ada' } });
9
+ });
10
+ test('createEventCreators converts kebab-case event types to camelCase creator names', () => {
11
+ const creators = createEventCreators();
12
+ expect(creators.taskDone({ id: '7' })).toEqual({ type: 'task-done', payload: { id: '7' } });
13
+ });
14
+ test('createEventCreators makes an undefined-payload creator take no argument', () => {
15
+ const creators = createEventCreators();
16
+ expect(creators.done()).toEqual({ type: 'done', payload: undefined });
17
+ });
18
+ test('createEventCreators types each creator from the payload map', () => {
19
+ const creators = createEventCreators();
20
+ expectTypeOf(creators.taskDone).toEqualTypeOf();
21
+ expectTypeOf(creators.done).toEqualTypeOf();
22
+ });
package/fake.d.ts CHANGED
@@ -8,10 +8,10 @@ type FakeConfig = {
8
8
  error: Error;
9
9
  };
10
10
  type FakeWithThrowingMethods<FAKE extends object, FAKE_CONFIGS extends FakeConfigs<FAKE>> = {
11
- [KEY in keyof FAKE]: KEY extends keyof FAKE_CONFIGS ? FAKE[KEY] extends (...args: any[]) => Observable<any> ? ThrowingObservable : FAKE[KEY] extends (...args: any[]) => Promise<any> ? ThrowingAsync : FAKE[KEY] extends (...args: any[]) => any ? ThrowingSync : FAKE[KEY] : FAKE[KEY];
11
+ [KEY in keyof FAKE]: KEY extends keyof FAKE_CONFIGS ? FAKE[KEY] extends (...args: never[]) => Observable<unknown> ? ThrowingObservable : FAKE[KEY] extends (...args: never[]) => Promise<unknown> ? ThrowingAsync : FAKE[KEY] extends (...args: never[]) => unknown ? ThrowingSync : FAKE[KEY] : FAKE[KEY];
12
12
  };
13
- type ThrowingSync = (...args: any[]) => never;
14
- type ThrowingAsync = (...args: any[]) => Promise<never>;
15
- type ThrowingObservable = (...args: any[]) => Observable<never>;
16
- export declare const addErrorMethodsToFake: <T extends object, ARGS extends any[]>(originalFake: (...args: ARGS) => T) => (configs?: FakeConfigs<T>, ...restArgs: any[]) => FakeWithThrowingMethods<T, FakeConfigs<T>>;
13
+ type ThrowingSync = (...args: unknown[]) => never;
14
+ type ThrowingAsync = (...args: unknown[]) => Promise<never>;
15
+ type ThrowingObservable = (...args: unknown[]) => Observable<never>;
16
+ export declare const addErrorMethodsToFake: <T extends object, ARGS extends unknown[]>(originalFake: (...args: ARGS) => T) => (configs?: FakeConfigs<T>, ...restArgs: ARGS) => FakeWithThrowingMethods<T, typeof configs>;
17
17
  export {};
package/fake.js CHANGED
@@ -3,12 +3,9 @@ import { mergeMap } from 'rxjs/operators';
3
3
  export const addErrorMethodsToFake = (originalFake) => (configs = {}, ...restArgs) => {
4
4
  const fake = originalFake(...restArgs);
5
5
  const throwingMethods = generateThrowingMethods(configs);
6
- // It is a valid cast to more specific tpye
7
- // eslint-disable-next-line
8
- return {
9
- ...fake,
10
- ...throwingMethods,
11
- };
6
+ const merged = { ...fake, ...throwingMethods };
7
+ // Narrowing to the more specific type: the throwing methods replace the ones they shadow.
8
+ return merged;
12
9
  };
13
10
  const THROWING_METOD_GENERATORS = {
14
11
  sync: (error) => () => {
@@ -21,9 +18,7 @@ const THROWING_METOD_GENERATORS = {
21
18
  };
22
19
  const generateThrowingMethods = (configs) => {
23
20
  const entries = Object.entries(configs);
24
- const throwingMethods = entries.map(([method, { error, type }]) => [
25
- method,
26
- THROWING_METOD_GENERATORS[type](error),
27
- ]);
21
+ const throwingMethods = entries.map(([method, { error, type }]) => [method, THROWING_METOD_GENERATORS[type](error)]);
22
+ // The generated values are checked; only their keys cannot be tied back to FAKE at runtime.
28
23
  return Object.fromEntries(throwingMethods);
29
24
  };
package/fake.test.js CHANGED
@@ -51,3 +51,11 @@ test('Forwarding constructor paramters', () => {
51
51
  expect(fake.method()).toBe(1);
52
52
  expect(fake.method2()).toBe(2);
53
53
  });
54
+ test('A throwing method stays callable with the arguments the original took', () => {
55
+ const originalFake = () => ({ method: (_, count) => count });
56
+ const fakeCreator = addErrorMethodsToFake(originalFake);
57
+ expect(fakeCreator().method('a', 1)).toBe(1);
58
+ const error = new Error('boom');
59
+ const fake = fakeCreator({ method: { type: 'sync', error } });
60
+ expect(() => fake.method('a', 1)).toThrow(error);
61
+ });
@@ -1,3 +1,4 @@
1
+ import type { DetachableMethods } from './type-util.ts';
1
2
  import type { Observable } from 'rxjs';
2
3
  import { type NamedCase, type UnnamedCase } from 'rxjs-marbles/cases.js';
3
4
  import { type Configuration } from 'rxjs-marbles/configuration.js';
@@ -17,19 +18,19 @@ declare const marbles: MarblesFunction;
17
18
  type Marbles = typeof marbles;
18
19
  type MarblesRunner = Parameters<Marbles>[0];
19
20
  type MarblesParam = Parameters<MarblesRunner>[0];
20
- type Runner = (m: MarblesExtensions, ...args: any[]) => ReturnType<MarblesRunner>;
21
+ type Runner<ARGS extends unknown[] = []> = (m: MarblesExtensions, ...args: ARGS) => void | Promise<void>;
21
22
  type MarbleFunctions = Record<string, () => void>;
22
23
  type MarblesExtensions = {
23
24
  coldCall: (marble: string, functions: MarbleFunctions) => void;
24
25
  coldBoolean: (marble: string) => TestObservableLike<boolean>;
25
- coldValue: <T = any>(marble: string, value: T) => TestObservableLike<T>;
26
- coldValueOrUndefined: <T = any>(marble: string, value: T) => TestObservableLike<T | undefined>;
27
- coldStep: <T = any>(value: T, steps?: number) => TestObservableLike<T>;
28
- coldStepAndClose: <T = any>(value: T, steps?: number) => TestObservableLike<T>;
29
- coldError: (error: any, steps?: number) => TestObservableLike<any>;
30
- coldStepAndError: <T = any>(value: any, error: any, steps?: number) => TestObservableLike<T>;
31
- expect: <T = any>(actual: Observable<T>, subscription?: string) => ExtendedExpect<T>;
32
- } & MarblesParam;
26
+ coldValue: <T = unknown>(marble: string, value: T) => TestObservableLike<T>;
27
+ coldValueOrUndefined: <T = unknown>(marble: string, value: T) => TestObservableLike<T | undefined>;
28
+ coldStep: <T = unknown>(value: T, steps?: number) => TestObservableLike<T>;
29
+ coldStepAndClose: <T = unknown>(value: T, steps?: number) => TestObservableLike<T>;
30
+ coldError: (error: unknown, steps?: number) => TestObservableLike<never>;
31
+ coldStepAndError: <T = unknown>(value: T, error: unknown, steps?: number) => TestObservableLike<T>;
32
+ expect: <T = unknown>(actual: Observable<T>, subscription?: string) => ExtendedExpect<T>;
33
+ } & DetachableMethods<MarblesParam>;
33
34
  declare class ExtendedExpect<T> extends Expect<T> {
34
35
  readonly actual_: Observable<T>;
35
36
  readonly helpers_: ExpectHelpers;
@@ -39,17 +40,20 @@ declare class ExtendedExpect<T> extends Expect<T> {
39
40
  toBeObservableValue(value: T): void;
40
41
  toBeObservableValue(marble: string, value: T): void;
41
42
  toBeObservableValueAndClose(value: T): void;
42
- toBeObservableValueAndError(value: T, error: any): void;
43
+ toBeObservableValueAndError(value: T, error: unknown): void;
43
44
  toBeObservableValueOrUndefined(marble: string, value: T): void;
44
45
  toBeObservableStep(value: T, steps?: number): void;
45
46
  toBeObservableStepAndClose(value: T, steps?: number): void;
46
- toBeObservableError(error: any, steps?: number): void;
47
- toBeObservableStepAndError(value: T, error: any, steps?: number): void;
47
+ toBeObservableError(error: unknown, steps?: number): void;
48
+ toBeObservableStepAndError(value: T, error: unknown, steps?: number): void;
48
49
  }
49
- export declare const coreMarbles: (runner: Runner) => (() => void);
50
+ export declare const coreMarbles: <ARGS extends unknown[] = []>(runner: Runner<ARGS>) => ((...args: ARGS) => void | Promise<void>);
50
51
  export declare const MARBLES_BOOLEAN: {
51
52
  t: boolean;
52
53
  f: boolean;
53
54
  };
54
- export declare const mtest: (name: string, runner: Runner) => void;
55
+ export declare const mtest: {
56
+ (name: string, runner: Runner): void;
57
+ each<T extends unknown[]>(cases: T[]): ((name: string, runner: Runner<T>) => void);
58
+ };
55
59
  export {};
package/marbles-vitest.js CHANGED
@@ -15,7 +15,7 @@ export function configure(configuration) {
15
15
  function cases(name, func, cases) {
16
16
  describe(name, () => {
17
17
  _cases((c) => {
18
- const t = c?.only !== undefined ? test.only : c?.skip === undefined ? test.skip : test;
18
+ const t = c.only === true ? test.only : c.skip === true ? test.skip : test;
19
19
  if (func.length > 2) {
20
20
  t(c.name, marbles((m, second, ...rest) => func(m, c, second, ...rest)));
21
21
  }
@@ -39,8 +39,7 @@ class ExtendedExpect extends Expect {
39
39
  this.toBeObservable(marble, MARBLES_BOOLEAN);
40
40
  }
41
41
  toBeObservableValue(...args) {
42
- const value = args.length === 1 ? args[0] : args[1];
43
- const marble = args.length === 1 ? 'v' : args[0];
42
+ const [marble, value] = args.length === 1 ? ['v', args[0]] : args;
44
43
  this.toBeObservable(marble, { v: value });
45
44
  }
46
45
  toBeObservableValueAndClose(value) {
@@ -59,7 +58,7 @@ class ExtendedExpect extends Expect {
59
58
  }
60
59
  toBeObservableStepAndClose(value, steps = 1) {
61
60
  const marble = `-`.repeat(steps) + '(v|)';
62
- this.toBeObservable(marble, { v: value });
61
+ return this.toBeObservable(marble, { v: value });
63
62
  }
64
63
  toBeObservableError(error, steps = 1) {
65
64
  const marble = `-`.repeat(steps) + '#';
@@ -70,54 +69,57 @@ class ExtendedExpect extends Expect {
70
69
  this.toBeObservable(marble, { v: value }, error);
71
70
  }
72
71
  }
73
- export const coreMarbles = (runner) => (...args) => marbles((m) => {
74
- const coldCall = (marble, functions) => {
75
- const marbleDefinition = Object.fromEntries(Object.keys(functions).map((key) => [key, key]));
76
- m.cold(marble, marbleDefinition).subscribe((key) => {
77
- functions[key]?.();
78
- });
79
- };
80
- const coldBoolean = (marble) => m.cold(marble, MARBLES_BOOLEAN);
81
- const coldValue = (marble, value) => m.cold(marble, { v: value });
82
- const coldValueOrUndefined = (marble, value) => m.cold(marble, { v: value, 0: undefined });
83
- const coldStep = (value, steps = 1) => m.cold(`-`.repeat(steps) + 'v', { v: value });
84
- const coldStepAndClose = (value, steps = 1) => m.cold(`-`.repeat(steps) + '(v|)', { v: value });
85
- const coldError = (error, steps = 1) => m.cold(`-`.repeat(steps) + '#', {}, error);
86
- const coldStepAndError = (value, error, steps = 1) => m.cold(`-`.repeat(steps) + '(v#)', { v: value }, error);
87
- // This function and the ExtendedExpect depends on internals of the `rxjs-marbles` library
88
- // potentially not future proof
89
- const expect = (actual, subscription) => {
90
- const { helpers_ } = m;
91
- return new ExtendedExpect(actual, helpers_, subscription);
92
- };
93
- // The methods on `m` (the RunContext) are on the prototype, so we have to bind the original
94
- // ones to be able to use destructuring
95
- return runner({
96
- get autoFlush() {
97
- return m.autoFlush;
98
- },
99
- coldCall,
100
- coldBoolean,
101
- coldValue,
102
- coldValueOrUndefined,
103
- coldStep,
104
- coldStepAndClose,
105
- coldError,
106
- coldStepAndError,
107
- expect,
108
- equal: m.equal.bind(m),
109
- cold: m.cold.bind(m),
110
- bind: m.bind.bind(m),
111
- configure: m.configure.bind(m),
112
- flush: m.flush.bind(m),
113
- has: m.has.bind(m),
114
- hot: m.hot.bind(m),
115
- reframe: m.reframe.bind(m),
116
- scheduler: m.scheduler,
117
- teardown: m.teardown.bind(m),
118
- time: m.time.bind(m),
119
- }, ...args);
120
- })();
72
+ export const coreMarbles = (runner) => (...args) => {
73
+ const runInMarbles = marbles((m) => {
74
+ const coldCall = (marble, functions) => {
75
+ const marbleDefinition = Object.fromEntries(Object.keys(functions).map((key) => [key, key]));
76
+ m.cold(marble, marbleDefinition).subscribe((key) => {
77
+ functions[key]?.();
78
+ });
79
+ };
80
+ const coldBoolean = (marble) => m.cold(marble, MARBLES_BOOLEAN);
81
+ const coldValue = (marble, value) => m.cold(marble, { v: value });
82
+ const coldValueOrUndefined = (marble, value) => m.cold(marble, { v: value, 0: undefined });
83
+ const coldStep = (value, steps = 1) => m.cold(`-`.repeat(steps) + 'v', { v: value });
84
+ const coldStepAndClose = (value, steps = 1) => m.cold(`-`.repeat(steps) + '(v|)', { v: value });
85
+ const coldError = (error, steps = 1) => m.cold(`-`.repeat(steps) + '#', {}, error);
86
+ const coldStepAndError = (value, error, steps = 1) => m.cold(`-`.repeat(steps) + '(v#)', { v: value }, error);
87
+ // This function and the ExtendedExpect depends on internals of the `rxjs-marbles` library
88
+ // potentially not future proof
89
+ const expect = (actual, subscription) => {
90
+ const { helpers_ } = m;
91
+ return new ExtendedExpect(actual, helpers_, subscription);
92
+ };
93
+ // The methods on `m` (the RunContext) are on the prototype, so we have to bind the original
94
+ // ones to be able to use destructuring
95
+ return runner({
96
+ get autoFlush() {
97
+ return m.autoFlush;
98
+ },
99
+ coldCall,
100
+ coldBoolean,
101
+ coldValue,
102
+ coldValueOrUndefined,
103
+ coldStep,
104
+ coldStepAndClose,
105
+ coldError,
106
+ coldStepAndError,
107
+ expect,
108
+ equal: m.equal.bind(m),
109
+ cold: m.cold.bind(m),
110
+ bind: m.bind.bind(m),
111
+ configure: m.configure.bind(m),
112
+ flush: m.flush.bind(m),
113
+ has: m.has.bind(m),
114
+ hot: m.hot.bind(m),
115
+ reframe: m.reframe.bind(m),
116
+ scheduler: m.scheduler,
117
+ teardown: m.teardown.bind(m),
118
+ time: m.time.bind(m),
119
+ }, ...args);
120
+ });
121
+ return runInMarbles();
122
+ };
121
123
  export const MARBLES_BOOLEAN = {
122
124
  t: true,
123
125
  f: false,
@@ -125,3 +127,6 @@ export const MARBLES_BOOLEAN = {
125
127
  export const mtest = (name, runner) => {
126
128
  test(name, coreMarbles((m) => runner(m)));
127
129
  };
130
+ mtest.each = (cases) => {
131
+ return (name, runner) => test.each(cases)(name, coreMarbles(runner));
132
+ };