realar-project / realar

5 kB Advanced state manager for React

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

low: the stream of conciseness 0.7+ roadmap

betula opened this issue · comments

"ready" -> "trigger"
"sub" -> "update.by"

// cast -- to
v.cast.signal.trigger()
v.cast.value.trigger()
v.to(console.log);
value(0).view()
value(0).view.untrack()

value(0).flow() //-- stoppable
value(0).flow.untrack()
value(0).flow.filter()
value(0).flow.filter.untrack()
value(0).flow.filter.not()
value(0).flow.filter.not.untrack()

value(0).chan() //-- stoppable
value(0).chan.untrack()
value(0).chan.filter()
value(0).chan.filter.untrack()
value(0).chan.filter.not()
value(0).chan.filter.not.untrack()

value(0).chan.debounce(300)
value(0).chan.throttle(300)

value(0).pre() //-- stoppable (untracked by default) expression
value(0).pre.track()

value(0).pre.filter()
value(0).pre.filter.untrack()
value(0).pre.filter.not()
value(0).pre.filter.not.untrack()

value(0).pre.chan()
value(0).pre.chan.untrack()
value(0).pre.chan.filter()
value(0).pre.chan.filter.untrack()
value(0).pre.chan.filter.not()
value(0).pre.chan.filter.not.untrack()

value(0).pre.chan.debounce(300)
value(0).pre.chan.throttle(300)
value(0).to.signal()
value(0).to.value()
value(0).to.ready()
value(0).to.flag()
value(0).to.flag.not()

value(0).promise

value(0).join(a, b)
value(0).join.chan(a, b)
value(0).join.signal(a, b)
value(0).join.signal.ready(a, b)
value(0).join.value(a, b)
value(0).join.value.ready(a, b)

value(0).sync(b)

value(0).interceptor()
value(0).sub.interceptor()

value(0).sub()
value(0).sub.once()

value(0).watch()
value(0).watch.once()
value()
value.from()
value.ready()
value.ready.from()
value.ready.flag()
value.ready.flag.from()
value.flag()
value.flag.from()

signal()
signal.from()
signal.ready()              // proposal: ready to trigger rename?
signal.ready.from()
signal.ready.flag()
signal.ready.flag.from()
signal.flag()
signal.flag.from()
pool()

pool().pre.throttle()
pool().pre.pipe(pool(), pool())
pool().pipe(pool(), pool())

Backlog from api-0.6 implementation

  [] trigger should be touchable
  [] value.touchable(initial) <- The ".from" construction not available for values with "initial" dependency requireds
  [] signal.touchable(initial)
  [] v.as.readonly()
  [] flow.resolve
  [] flow as root level exportable factory function
  [] .chan
  [] value.trigger.from
  [] value.trigger.flag.from
  [] .map <- sysnonym for .view (on thinking)
  [] x.group -- x.op -- x.block
    x.block((ctx) => ({  // if returns non undefined
      a: ctx.a,
      b: ctx.a.select()
    })).b.val
  [] v.as.signal(), v.as.trigger.flag()

Backlog:
[] Add callback to join, and combine. After It possible to use object config for join with callback
[] .as.trigger
[] .as.value.trigger
[] .as.signal.trigger

[] signal.trigger.resolved
[] value.trigger.resolved

[] test case "should work signal.trigger with configured .pre"

[] signal.trigger.from
[] value.trigger.from

[] Add Loader example to documentation

  // Second example
  const Loader = () => {
    const count = value(0);

    return {
      start: count.updater(v => v + 1),
      stop: count.updater(v => v - 1),
      pending: count.select(v => v > 0)
    }
  }

Join brainstorm:

[] Add track|untrack for join with callback

I think better way in that case is removing callback support for current version.
because without callback for combine and join no have any conficts for syntax.
I can use track|untrack for join without callback with no disharmony

--> decision for version 0.6:
  [] remove callback support for join and combine
  [] Add track|untrack for join
    ...disharmony was stay
--> decision number two) The second
  [] rename join to "attach" or "merge"

    Task: The saving consistency of track|untrack for append|attach|join|merge function

    Have a reason for join without tracking for a set of rective values.

      Have a reason join for signals?

value.combine() -> ok
value.join() -> ok

I cant implement signal.combine([], fn and s.join([], fn) because I have not flag for ready all
signals from argument

signal.combine() -> not ok
signal.join() -> not ok

maybe support combine and join only for values now.
For signal I can use merge

const s = signal(0);
s.merge([() => s.val, ...]).val == 0; without callback

Summary:
[] I should implement value.combine([]|{}, fn?), and value(0).combine([] | {}, fn?)
- track|untrack for combine? (I can not support untrack for combine values)

[] No have similar for signal in 0.6 versions.

value.from(() => ({
a: a.val,
b: b.val
}))
value.combine({ a, b }) -> better...
Always tracked, and it used only for values

value(0).combine - have a problem with track|untrack semantic not for 0.6 version.

I should make only value.combine. Aren't It?
Ok. For 0.6 its enough

Backlog
[] as.value(dafault_value?)
[] support reactionable for untrack
[] value().get.untrack()

Backlog
[] Add default for resolve "Will" type

  const v = value(0).filter().default(0)

Backlog:

const r = signal();
const u = signal<number>();

const v = value(0);

v.reset.by(r);

v.update.by(u, (state, up_value, up_value_prev) => {
  return state + (up_value - (up_value_prev || 0));
});

u(10);
r();

And I think to very interesting idea is the convert "reset" function method to signal

const a = value(0);

a.reset.to(() => console.log("reset"));

proposal: local

cosnt v = value(0);

v.flow((value) => {
  const ticket = local(() => wait(k));
  if (!ticket.val) return flow.stop();
  return value;
});

v.flow.filter(() => {
  return local(() => wait(k)).val
});

local(expr) // <- bind only first time not necessary bind it every time
                   // if you need bind every time you should use plain function not local expression.
                   // synonim of local(expr, []);
local(expr, [<real value dep>, ...])
local(expr, <refresh symbol or expression>);

// or all combination of that
local(expr, <refresh symbol or expression>, [<real value dep>, ...]);
local(expr, [<real value dep>, ...], <refresh symbol or expression>);

proposal: wait

const v = value(0);
const s = signal(0);

// wait.all([v, s]) - it is a ready (trigger)

await wait.all([v, s]).promise
await wait.once([v, s]).promise

wait(v) // similar to signal.trigger.from(v) or v.to.signal.trigger()
const k = signal(0);
const m = value(0);

v.flow.wait(m)
  .flow.wait.all(k, m) // or synonim to .flow.wait.all([k, m]) -- think about
  .flow.wait.race(k, m)
  .to((v) => console.log(v));
const k = signal(0);
const m = value(0);

v.flow.wait(m, (v_val, m_val) => return v_val + m_val)
  .flow.wait.all([k, m], (v_val, [k_val, m_val]) => return v_val + k_val + m_val) // or stop of course (maybe I can get resolve???)
  .flow.wait.race([k, m], (v_val, [k_val, m_val], resolve, v_val_prev) => {});
  .to((v) => console.log(v));

up: replace useMemo by useRef

You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to “forget” some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components. Write your code so that it still works without useMemo — and then add it to optimize performance.

proposal: chain

Think about "chain" abstraction

const a = value(0);
const b = chain(() => a.val);

assert(b.val === undefined);
a(5);
assert(b.val === undefined);

b.input.to(b.output); // "to" better than "watch"
a(6);
assert(b.val === 6);

proposal: pool.flat

private finish_purchase = pool(async () => {
    if (this.finish_purchase.count > 1) return; // not count.val here
  });
const a = pool(async () => {
    if (a.count.val > 1) return a.threads[0].promise; // Return promise from first thread
}

assert(a.flat.pending === false);
assert(chan(async () => {}).flat.pending === false);

proposal: signal.union

Collect signals from several values and signals.
Worked as last value of set

const a = value(0);
const b = signal(0);

const u = signal.union(a, b);

u.val === 0;
a(5);
u.val === 5;
b(10);
u.val === 10;

proposal: chan

Chan - is therm for define async flow.
Previous proposals for async are "flow.async", pipe.

const a = chan(async (p, k, m) => {}, empty_val?);
a(1,2,3);

const b = a.chan(async (a_val, prev_a_val) => {
});

chan(async () => {})
  .flow.filter()
  .flow.filter.untrack(() => this.loaded)
  .watch.once(() => {});

chan.untrack(async () => {})
  .join(b)
  .select(([a,b]) => a + b)
  .flow.untrack(() => {}, empty_val?)
  .watch(() => {})

chan(async () => {}).wrap.filter.not()

// ...

chan.untrack(async () => {}, empty_val)

value.from(() => {})
  .chan(async () => {}, empty_val)
  .flow.untrack(() => {}, empty_val?)
  .flow((a) => a + b.val)
  .watch(() => {});

Chan is not a "pool" because "chan" provides only one value, and flows around that value. Overwise "pool" provides a group of parallel or series async execution processes.

But both these meanings can be used with similar async operators.

const t = value(0)
  .view(() => {})
  .wrap.throttle(300)
  .chan.debounce(300);

// .flow.debounce(300); ??

// const t = pool(async () => {})        // "pool" untracked by default for safety reason
//  .wrap.throttle // hmmm multiples of parameters and throttle can be strange combination.
// Necessary to think about "throttle" and "debounce" for "pool".

Possible typecast syntax

chan(async () => {}).select.untrack(() => {});

signal(0).to.value()     // vs "signal(0).to.value"
value(0).to.signal()
signal(0).to.ready()

signal(0).promise
value(0).promise
value(0).to.ready().promise      // promise recreate on demand     // vs value(0).to.ready.promise

value.flag()
value.flag.not()
value.ready()
signal.flag()            // equivalent to "value.flag"
signal.ready()

// Think about rename "value.ready" to "value.once" or "value.trigger" ...

signal(0).to.trigger()  // The primary candidate for changing to
signal(0).to.once()     // hmm..

low: proposal pool single and pool static chaining factory

pool.single(async () => {

})

// or

pool(async () => {

}).single()

// Will be same result
pool.debounce(300).single().flow(async () => {});
// ok

low: proposal flow async and error with unhandled flow unsub

const a = value(0);

const b = value(0);
const c = value(0);

// For a first error on next cases
a.flow.async(async (a) => {
  const data = b.flow(() => {}).val; // Error here, necessary for using isolate here
}};


// Flow async proposal

const s = a.flow.async(async (a) => {
  if (!a) return stoppable.stop();

  return await load_subscriptions();
});

s.val // subscriptions or undefined
s.initialized.val
s.error.val
s.pending()

a.flow.async.debounce(300)
  .pipe(
    customDebounce, 
    async (a) => {
      if (!a) return stoppable.stop();
      const m = c.val; // Subscrible to m or not??
      return await load_subscriptions(m);
    }
  ); // readonly async flow

const all_ok = value.combine(a, s.initialized).select(([a, s]) => a && s)  // or value.combine([a, s.initialized])

// Rename pool to asyncs)))

// Constructors

value.async(async (v) => {});
signal.async(async (v) => {});

a.flow.async(async (a) => {});

const m = a.flow.async(async (a) => {}).single();

const h = a.sub.async(reactionable, async (state, val) => {}); // h.pending.val

const h_2 = pool(async (a,b,c,d,e) => {
  stoppable.stop();
});


// Syntax possibilities

a.async
   .debounce(300)
  .pipe(
    customDebounce, 
    async (a) => {
      if (!a) return stoppable.stop();
      const m = c.val; // Subscrible to m or not?? (untrack inside pipe section... hmmmm)
      return await load_subscriptions(m);
    }
  )
  .flow(async (a) => {
    return await load(a, b.val, c.val); // Depend on change any of that values
   })
; // readonly async flow

// Pipe <> Flow same words for not obvious differences... hmm

const s = flow.async(async () => {
  return await load(a.val, b.val, c.val);
});
// Syntax possibilities

const t = a.async
   .debounce(300)
  .flow.untrack(
    customDebounce, 
    async (a) => {
      if (!a) return stoppable.stop();
      const m = c.val;                                       // not subscribe to c
      return await load_subscriptions(m);
    }
  )
  .flow(async (a) => {
    return await load(a, b.val, c.val);              // subscribe to b and c
   })
; // readonly async flow

t.val 
t.ready.val
t.error.val
t.pending.val

// flow factory (only track, untrack unsupported)
const s = flow.async(async () => {
  return await load(a.val, b.val, c.val);
});
const f_sync = flow(() => {
  return sync_load(a.val, b.val, c.val);
});

// And pool

const p = pool(async (a,b,c,d,e) => {
  stoppable.stop();
});

p.pending.val

low: proposal pool syntax

const a = pool(async () => {});

a.single()
a.debounce(300)
a.throttle()

// or

a.pipe.single()
a.pipe.debounce(300)

a.pipe( ... ) // what is It?

and for values and signals

const a = value(0);

a.pool.debounce(100) // what is it?

low: on((stop) => {}, () => {}) implement

low: try deprecate stopppable

selector(init_value?, (stop) => {});
selector((stop?) => {});

cycle((stop) => {});
on((stop) => {}, () => {});




// But I think pool no need to use "(stop) =>" syntax, maybe stoppable() is better for pool?
// pool.stop

pool(async () => {
  const stop = pool.stop;
  pool.stop();
  pool.stop.throw();
});

pool.stoppable((stop) => {
  return async () => {

  }
});

low: signal.trigger.from method

Should be added:
signal.trigger.from
signal.trigger.flag.from

(Declined) proposal: signal.flag and value.flag

const flag = signal(false).pre((v) => !!v);
ver very low: proposal proxy property
const v = value({});
v.proxy.a = 10;
v.update.proxy(p => { // one transaction
  p.b = (p.a += 15);
});
proposal: name for production ready version "Universal data-flow"
low: think about "in one transaction" feature
const s = signal();
const v = value();

on(s, (s_v) => v.val += s_v);
on(() => [s,v], console.log);


on.transaction(s, (s_v) => v.val += s_v); // used one transaction with changed s

// Think about

proposal: queue implementation

const q = queue<Type?>(init?: Type[]);

// q(10); // Think about queue - is a reactive value with set of elements or is the signal for add next one... Hmm.

// q.queue // []

q.front // top
q.back // last element
// q.all // array of elements in queue
q.size
q.clear()

//

q.val
q.first: Value
q.last: Value
q.size: Value
q.active: Value
q.clear()

// Support queue for q.wrap and q.view // think about api
// val: <array_of_elements> readonly
// q(<new_element>)

Or q() - no possible, but 

q.add(value: Type)
q.val: Type
q.first: Value
q.last: Value
q.size: Value
q.active: Value
q.clear()

// How I can make queue with limited size same as in my swipe impl?

q = queue();

q(values: Type[]);
q.set(values: Type[]);
q.update(...);
q.val: Type;

q.add(value: Type);

q.first: Value
q.last: Value
q.size: Value
q.active: Value
q.empty: Value
q.clear()


//

const t = q.first

t.val: Type
t.started: Value
t.start();
t.release(); // remove item from queue (or dequeue)

// or

t.val: Type
t.start: ready(false).to(true)
t.remove(); // remove item from queue // or ready(false).to(true) too

Decorating add signal

const q = queue<Type>([], (queue, value: Type) => {
  const excluded = queue.slice(3);
  // const data = queue..
  // How to convert value to queue element?
});

const q = queue<Type>();
q.before (Signal)

q.before((value) => {
  stoppable.stop();
  transaction
  check q
  remove unnecessary q elems
  add new
  finish transaction
});

proposal: interceptor method

const a = value(5);
const b = value(1);

const unsub = a.interceptor((new_value, current_value, stop?) => {
  if (new_value == 1) {
    b.set(10);
    stop();
    return;
  }
  return current_value + new_value;
});
// stoppable supported

a(1); // b.val === 10, a.val === 5
a(2); // a.val === 7

Think about syntax for sub interceptions

const v = value(0);
const s = signal(0);
v.sub.intercept(s, (v_value, new_signal_value, stop ) => {
  ///
});
v.intercept.sub // alias

// or
v.sub.intercept(s, (v_value, new_signal_value, prev_signal_value ) => {
  stoppable.stop();
});
v.intercept.sub // alias

proposal: pool async oparators

pool.single(async ({ cancelled }) => {
  const zip = await search();
  if (cancelled()) return;
  return await unpack(zip);
});

// If exists active promise each query return It

// pool.throttle(150, async ({ cancelled, abortController?.., cancel }) => {
pool.throttle(150, async ({ stopped, abortController?.., stop }) => {
  const zip = await search();
  if (zip === 0) stop();
  if (stopped()) return;
  return await unpack(zip);
});

pool.debounce(150, async function * ({ cancel }) {
  const zip = yield search();
  if (zip === 0) cancel();
  return yield unpack(zip);
});

It is offtopic, but I want to think about throttle and debounce for signals:

const start = signal();
const delayedStart = signal.throttle(150, start);

const a = value();
const delayedA = value.throttle(150, a);

const p = pool(async () => await search());
const delayedP = pool.throttle(150, p);

Maybe only one throttle function for all necessaries.

low: external package realar-form

very low: add jsx method
const a = value(0);

return (
  <p>{a.jsx}</p>
)

return (
  <p>{a.jsx(v => v.map((i, k) => <i key={k}>{i}</i>))}</p>
)

😉

a.jsx
a.jsx.if
a.jsx.else
a.jsx.ifelse
a.jsx.map

// or

jsx(a)
jsx.if(a)
jsx.map(a)
// etc..

For making super performant interfaces 👍

very low: state machine impl

This abstraction is necessary for the implementation of serial operations. Reactions on each section of the serial operations.

const m = machine()
  .step(a, () => {})
  .step(b, () => 0)
  .step(machine.oneOf()
    .select(a, () => {})
    .select(b)
    .select(c), () => {})
  .loop();

But I think the yield generator function is an interesting way of decision.

loop(function *() {
  yield a;
  yield b;
  yield loop.oneOf([a, b, c]) // or loop.oneOf().select(a, () => {}).select(b, () => {})... chain function
  yield loop.race([a, b, c])
  yield loop.all([a, b, c])

  // Promise namespace operations for suggest
});

low: readonly method

const v = value(0);
const readonly_v = v.readonly();

Should research about argument passing to "prop" decorator, for the easy binding reactive container to class property:

class A {
  @prop(this.store) flat_store
}

proposal: value.extract

It should be extract object or array of reactive containers to object or array of usual javascript values.

const email = value('a@x');
const values = value.extract({ email });
console.log(values.email) // a@x.com

Possible kinds of name: "values" consistent with useValues

proposal: input as exportable api function

input.handler <- (ev) => ev.target.value
// proposal for another names.
// input.delegate
// input.change
// input.event

input.valid
input.valid.pending

input.validator(
  signal<string>().map((state) => /a/.test(state))
)
// input.validator.async() // or

// Interested but no consistency to other code base.
// input.validator((signal) => {
//   return signal.as.chan().map(async () => {})
// });

// Final perfect version
input.validator([
  signal<string>().filter() <-- what will happens if validator will be stopped
  chan<string>().map(async (state) => await fetch_is_valid(state)),
  (state) => /aa/.test(state),
  async (state) => await ext_test(state)
]);

proposal: remove select.multiple

Improve "select" to "select.multiple" signature

const s = select({ a: (state) => state.a });

The argument can be function, reactive container, object map, or array tuple.

proposal: reactive container as argument of "map" (and array of)

const pipe_double = signal<number>().map(state => state * 2);

value(1).map(pipe_double).val // 2
value(1).map([pipe_double, pipe_double]).val // 4

// Use signature as an array or as reactive container, or usual function signature.

low: test ts typings

https://github.com/reduxjs/reselect/blob/master/typescript_test/test.ts#L479

 // typings:expect-error     
 const baz2: string = baz;

fix: update component warn from modification in the render function

https://codesandbox.io/s/realar-api-unsubscribe-scopes-control-0sziu?file=/src/App.tsx

Warning: Cannot update a component (`fn`) while rendering a different component (`Form`). To locate the bad setState() call inside `Form`, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
    at Form (https://0sziu.csb.app/src/App.tsx:30:35)
    at fn (https://0sziu.csb.app/node_modules/realar/build/index.js:697:30)

brainstorm: signals two directional synchronization

const a = signal<number>();
const b = signal<number>();

sync(a, b);
sync(b, a);

Signals connection without the infinity loop error

I should realize a way to the bi-directional connection between signals.

const a = signal(0);
const b = signal(0);

sync.bi(a, b);
a.val++
b.val++
assert(a.val === 2)

signal.from.select broken ts types with tuples

signal
  .from(() => {
    return [
      buyAnnualLocale.val,
      sharedPurchases().annual_subscription.val
    ]
  })
  .select(([locale, sub]) => { // both variables have one type "string | Subscription | undefined"
    if (!sub) return void 0;
    const localized_price = (sub as any)?.localizedPrice;
    if (!localized_price) return void 0;
    
    return locale.text
  })

value(false) broken types for ts <4

const y: Value<boolean> = value(false);
/*
Type 'Value<false, false>' is not assignable to type 'Value<boolean, boolean>'.
  Type 'Value<false, false>' is not assignable to type '{ (value: boolean): void; set(value: boolean): void; }'.
    Types of parameters 'value' and 'value' are incompatible.
      Type 'boolean' is not assignable to type 'false'.ts(2322)
*/

proposal: manual rerun cycle outside

const h = cycle(() => {});
h.stop();
h.run();

proposal: shortcut not

sharedEye().enabled
    .as.signal()
    .filter()
    .filter(isNext.map(flag => !flag)) // TODO: shortcut not isNext.not()

proposal: value with no initial parameter

export const swipeAnimDirection = value<undefined | 'up' | 'down'>();

proposal: typings improvement for filter

 value<void | number>()
    .as.signal()
    .filter() // Should remove undefined | null | void from Will signal type
    .to(onlyNumberSignal);

proposal: new Scope api

const MyLogic = (initial: string) => {}

const MyScope = scope(MyLogic)

const A = () => {
  const B = useJsx(() => {
    const logic = useScoped(MyLogic)

    /* proposal: use scope logic from local section
    useLocal(() => {
      const logic = scoped(MyLogic)  // Warning scope recreation
      const logic = scoped.current(MuLogic) // Think about
    });
    */
    return
  });
  return <MyScope initial={'hello'}><B>{children}</B></MyScope>
}

fix: callback of sync or to methods should be untracked ... hmm.. check it

loader_queue.sync(async queue => {
  // should be untracked here
})

fix: typings for select without parameter

  const toggle = value(false).pre((_, state) => !state);
  const enabled = toggle.select(); // Incorrect: ValueReadonly<unknown>

proposel: value.will

const t = value.will<number>();
expect(t.val).toBeUndefined();
t.map(n: number => n + 1);

proposal

const takeUser = value();
const takeAuth = value();
takeAuth.onChange(takeUser.andUpdate)
takeUser.andSet()
takeUser() - alias for takeUser.andGet

const start = signal();
start.get - doesn't allowed
start.fire(new_value)

takeUser.onChange(start.fire)

start.getValue() - returns value instance

isDirty, isUndefined, isTouched
All methods created only by demand.

start.flow
start.pre

takeUser.compose