@wrnrlr/prelude 0.0.1 → 0.1.2

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/src/reactive.ts CHANGED
@@ -1,4 +1,4 @@
1
-
1
+ // @ts-nocheck:
2
2
  export interface EffectOptions {
3
3
  name?: string;
4
4
  }
@@ -145,7 +145,7 @@ abstract class Observer {
145
145
 
146
146
  class Root extends Observer {
147
147
  wrap<T>(fn: RootFn<T>): T {
148
- return wrap(() => fn(this.dispose), this, false)!
148
+ return observe(() => fn(this.dispose), this, false)!
149
149
  }
150
150
  }
151
151
 
@@ -165,7 +165,7 @@ class Computation<T = unknown> extends Observer {
165
165
  private run = (): T => {
166
166
  this.dispose()
167
167
  this.parent?.observers.add(this)
168
- return wrap(this.fn, this, true)!
168
+ return observe(this.fn, this, true)!
169
169
  }
170
170
 
171
171
  update = (): void => {
@@ -279,21 +279,52 @@ export function useContext<T>(context: Context<T>): T {
279
279
  return context.get()
280
280
  }
281
281
 
282
+ export type S<T> = Getter<T> | Setter<T>
283
+
284
+ /**
285
+
286
+ @param s Signal
287
+ @param k
288
+ */
289
+ export function wrap<T>(s:S<Array<T>>, k:number|(()=>number)): S<T>
290
+ export function wrap<T>(s:S<Record<string,T>>, k:string|(()=>string)): S<T>
291
+ export function wrap<T>(s:S<Array<T>>|S<Record<string,T>>, k:number|string|(()=>number)|(()=>string)): S<T> {
292
+ const t = typeof k
293
+ if (t === 'number') {
294
+ return ((...a:T[]) => {
295
+ const b = (s as Getter<Array<T>>)()
296
+ return (a.length) ? (s as Setter<Array<T>>)((b as any).toSpliced(k as number, 1, a[0])).at(k as number) : b.at(k as number)
297
+ }) as S<T>
298
+ } else if (t === 'string') {
299
+ return ((...a:T[]) => {
300
+ const b = (s as Getter<Record<string,T>>)()
301
+ return (a.length) ? (s as Setter<Record<string,T>>)({...b, [k as string]:a[0]})[k as string] : b[k as string]
302
+ }) as S<T>
303
+ } else if (t === 'function')
304
+ return ((...a:T[]) => {
305
+ const i = (k as ()=>string|number)(), c = typeof i
306
+ if (c==='number') return a.length ? (s as Setter<Array<T>>)((old:any) => old.toSpliced(i, 1, a[0]))[i as number] : (s as Getter<Array<T>>)()[i as number]
307
+ else if (c === 'string') return a.length ? (s as Setter<Record<string,T>>)((b) => ({...b, [i]:a[0]}))[i as string] : (s as Getter<Record<string,T>>)()[i]
308
+ throw new Error('Cannot wrap signal')
309
+ }) as S<T>
310
+ throw new Error('Cannot wrap signal')
311
+ }
312
+
282
313
  export function getOwner(): Observer | undefined {
283
314
  return OBSERVER
284
315
  }
285
316
 
286
317
  export function runWithOwner<T>(observer: Observer|undefined, fn: ()=>T):T {
287
318
  const tracking = observer instanceof Computation
288
- return wrap(fn, observer, tracking)!
319
+ return observe(fn, observer, tracking)!
289
320
  }
290
321
 
291
322
  /**
292
- Execute the function `fn` only once. Implemented as an {@link effect} wrapping a {@link sample}.
323
+ Execute the function `fn` only once. Implemented as an {@link effect} wrapping a {@link untrack}.
293
324
  @group Reactive Primitive
294
325
  */
295
326
  export function onMount(fn: () => void) {
296
- effect(() => sample(fn));
327
+ effect(() => untrack(fn));
297
328
  }
298
329
 
299
330
  export function onCleanup(fn: Fn):void {
@@ -327,20 +358,17 @@ export function batch<T>(fn: Fn<T>):T {
327
358
  }
328
359
 
329
360
  /**
361
+ Get the value of a signal without subscribing to future updates.
330
362
 
331
363
  @param fn
364
+ @returns value returned from `fn`
332
365
  @group Reactive Primitive
333
366
  */
334
- export function sample<T>(fn: Fn<T>):T {
335
- return wrap(fn, OBSERVER, false)!
367
+ export function untrack<T>(fn: ()=>T):T {
368
+ return observe(fn, OBSERVER, false)!
336
369
  }
337
370
 
338
- /**
339
-
340
- @param fn
341
- @group Reactive Primitive
342
- */
343
- function wrap<T>(fn: Fn<T>, observer: Observer | undefined, tracking: boolean ): T|undefined {
371
+ function observe<T>(fn: Fn<T>, observer: Observer | undefined, tracking: boolean ): T|undefined {
344
372
  const OBSERVER_PREV = OBSERVER;
345
373
  const TRACKING_PREV = TRACKING;
346
374
  OBSERVER = observer;
@@ -0,0 +1,184 @@
1
+ // @ts-nocheck:
2
+ import {signal,effect,untrack,memo,batch,useContext,onCleanup} from './reactive.ts'
3
+
4
+ const NO_INIT = {}
5
+
6
+ /**
7
+ resource
8
+ */
9
+ export function resource(pSource,pFetcher,pOptions) {
10
+ let source
11
+ let fetcher
12
+ let options
13
+
14
+ if ((arguments.length === 2 && typeof pFetcher === 'object') || arguments.length === 1) {
15
+ source = true
16
+ fetcher = pSource
17
+ options = (pFetcher || {})
18
+ } else {
19
+ source = pSource
20
+ fetcher = pFetcher
21
+ options = pOptions || {}
22
+ }
23
+
24
+ let pr = null,
25
+ initP = NO_INIT,
26
+ scheduled = false,
27
+ resolved = 'initialValue' in options
28
+ const dynamic = source.call && memo(source)
29
+
30
+ const contexts = new Set(),
31
+ value = signal(options.initialValue),
32
+ error = signal(undefined),
33
+ track = signal(undefined, {equals: false}),
34
+ state = signal(resolved ? 'ready' : 'unresolved')
35
+
36
+ function loadEnd(p, v, error, key) {
37
+ if (pr === p) {
38
+ pr = null
39
+ key !== undefined && (resolved = true)
40
+ initP = NO_INIT
41
+ completeLoad(v, error)
42
+ }
43
+ return v
44
+ }
45
+
46
+ function completeLoad(v, err) {
47
+ batch(() => {
48
+ if (err === undefined) value(() => v)
49
+ state(err !== undefined ? 'errored' : resolved ? 'ready' : 'unresolved')
50
+ error(err)
51
+ for (const c of contexts.keys()) c.decrement()
52
+ contexts.clear()
53
+ }, false)
54
+ }
55
+
56
+ function read() {
57
+ const v = value(),
58
+ err = error();
59
+ if (err !== undefined && !pr) throw err;
60
+ return v;
61
+ }
62
+
63
+ function load(refetching = true) {
64
+ if (refetching !== false && scheduled) return;
65
+ scheduled = false;
66
+ const lookup = dynamic ? dynamic() : (source);
67
+
68
+ if (lookup == null || lookup === false) {
69
+ loadEnd(pr, untrack(value));
70
+ return;
71
+ }
72
+ const p = initP !== NO_INIT ? (initP)
73
+ : untrack(() => fetcher(lookup, {value: value(), refetching}))
74
+
75
+ if (!isPromise(p)) {
76
+ loadEnd(pr, p, undefined, lookup);
77
+ return p
78
+ }
79
+ pr = p;
80
+
81
+ if ('value' in p) {
82
+ if ((p).status === "success") loadEnd(pr, p.value, undefined, lookup);
83
+ else loadEnd(pr, undefined, undefined, lookup);
84
+ return p;
85
+ }
86
+
87
+ scheduled = true
88
+ queueMicrotask(() => (scheduled = false));
89
+ batch(() => {
90
+ state(resolved ? 'refreshing' : 'pending')
91
+ track()
92
+ }, false);
93
+ return p.then(
94
+ v => loadEnd(p, v, undefined, lookup),
95
+ e => loadEnd(p, undefined, castError(e), lookup)
96
+ );
97
+ }
98
+
99
+ Object.defineProperties(read, {
100
+ state: { get: () => state() },
101
+ error: { get: () => error() },
102
+ loading: {
103
+ get() {
104
+ const s = state();
105
+ return s === 'pending' || s === 'refreshing';
106
+ }
107
+ },
108
+ latest: {
109
+ get() {
110
+ if (!resolved) return read();
111
+ const err = error();
112
+ if (err && !pr) throw err;
113
+ return value();
114
+ }
115
+ }
116
+ })
117
+
118
+ if (dynamic) effect(() => load(false));
119
+ else load(false);
120
+
121
+ return read
122
+ }
123
+
124
+
125
+ function isPromise(v) {
126
+ return v && typeof v === 'object' && 'then' in v
127
+ }
128
+
129
+ /**
130
+ Creates and handles an AbortSignal**
131
+ ```ts
132
+ const [signal, abort, filterAbortError] = makeAbortable({ timeout: 10000 });
133
+ const fetcher = (url) => fetch(url, {signal:signal()}).catch(filterAbortError); // filters abort errors
134
+ ```
135
+ Returns an accessor for the signal and the abort callback.
136
+
137
+ Options are optional and include:
138
+ - `timeout`: time in Milliseconds after which the fetcher aborts automatically
139
+ - `noAutoAbort`: can be set to true to make a new source not automatically abort a previous request
140
+ */
141
+ export function makeAbortable(options) {
142
+ let controller, timeout
143
+ const abort = (reason) => {
144
+ timeout && clearTimeout(timeout);
145
+ controller?.abort(reason);
146
+ }
147
+ const signal = () => {
148
+ if (!options.noAutoAbort && controller?.signal.aborted === false) abort("retry");
149
+ controller = new AbortController();
150
+ if (options.timeout) timeout = setTimeout(() => abort("timeout"), options.timeout)
151
+ return controller.signal;
152
+ }
153
+ const error = err => {
154
+ if (err.name === "AbortError") return undefined
155
+ throw err;
156
+ }
157
+ return [signal, abort, error]
158
+ }
159
+
160
+ /**
161
+ Creates and handles an AbortSignal with automated cleanup
162
+ ```ts
163
+ const [signal, abort, filterAbortError] =
164
+ createAbortable();
165
+ const fetcher = (url) => fetch(url, { signal: signal() })
166
+ .catch(filterAbortError); // filters abort errors
167
+ ```
168
+ Returns an accessor for the signal and the abort callback.
169
+
170
+ Options are optional and include:
171
+ - `timeout`: time in Milliseconds after which the fetcher aborts automatically
172
+ - `noAutoAbort`: can be set to true to make a new source not automatically abort a previous request
173
+ */
174
+
175
+ export function abortable(options) {
176
+ const [signal, abort, filterAbortError] = makeAbortable(options);
177
+ onCleanup(abort);
178
+ return [signal, abort, filterAbortError];
179
+ }
180
+
181
+ function castError(err) {
182
+ if (err instanceof Error) return err;
183
+ return new Error(typeof err === "string" ? err : "Unknown error", { cause: err });
184
+ }
package/src/router.js ADDED
@@ -0,0 +1,65 @@
1
+ // @ts-nocheck
2
+ import {signal,effect,batch,untrack,memo,wrap,context,useContext,onMount} from './reactive.ts'
3
+ // import {h} from './hyperscript.ts'
4
+
5
+ const Ctx = context()
6
+ const useRouter = () => useContext(Ctx)
7
+ export const useNavigate = () => wrap(useRouter(),'navigate')
8
+ export const useParams = () => wrap(useRouter(),'params')
9
+ export const useSearch = () => wrap(useRouter(),'search')
10
+
11
+ export function Router(props) {
12
+ const routes = props.children
13
+ const NotFound = ()=>'Not found'
14
+ const render = () => {console.log('render')}
15
+ // const loaded = signal(false)
16
+ const navigate = signal(null)
17
+ const params = signal(null)
18
+ const search = signal(null)
19
+ onMount(()=>{
20
+ window.addEventListener("popstate", (event) => {
21
+ batch(()=>{
22
+ const hash = parseHash(document.location.hash)
23
+ navigate(hash)
24
+ })
25
+ })
26
+ const hash = parseHash(document.location.hash)
27
+ navigate(hash)
28
+ })
29
+ const children = memo(() => {
30
+ let location = navigate()
31
+ const route = routes.find(r=>r.path===location.pathname)
32
+ return route?.component() || NotFound
33
+ })
34
+ return Ctx({navigate,params,search,children:()=>children})
35
+ }
36
+
37
+ function parseHash(s) {
38
+ const res = {pathname:'/'}
39
+ if (s[0]!=='#') return res
40
+ s = s.substr(1)
41
+
42
+ let i = s.indexOf('?')
43
+ if (i===-1) i = s.length
44
+ res.pathname += s.substr(0,i)
45
+ if (res.pathname==='') res.pathname = '/'
46
+ return res
47
+ }
48
+
49
+ function parsePathname(s) {
50
+ let i = s.indexOf('?', 0)
51
+ if (i===-1) i = s.length
52
+ return s.substr(0,i)
53
+ }
54
+
55
+ function parseSearch(s) {
56
+
57
+ }
58
+
59
+ export function Route(props) {
60
+
61
+ }
62
+
63
+ export function A(props) {
64
+
65
+ }
package/src/runtime.ts CHANGED
@@ -1,4 +1,5 @@
1
- import {effect,sample,root} from './reactive.ts'
1
+ // @ts-nocheck:
2
+ import {effect,untrack,root} from './reactive.ts'
2
3
  import {SVGNamespace,SVGElements,ChildProperties,getPropAlias,Properties,Aliases,DelegatedEvents} from './constants.ts'
3
4
  import type {Window,Mountable,Elem,Node} from './constants.ts'
4
5
 
@@ -22,7 +23,7 @@ export type Runtime = {
22
23
  }
23
24
 
24
25
  /**
25
-
26
+ Create `Runtime` for `window`
26
27
  @param window
27
28
  @group Internal
28
29
  */
@@ -54,7 +55,7 @@ export function runtime(window:Window):Runtime {
54
55
  function spread(node:Elem, props:any = {}, skipChildren:boolean) {
55
56
  const prevProps:any = {}
56
57
  if (!skipChildren) effect(() => (prevProps.children = insertExpression(node, props.children, prevProps.children)))
57
- effect(() => (props.ref?.call ? sample(() => props.ref(node)) : (props.ref = node)))
58
+ effect(() => (props.ref?.call ? untrack(() => props.ref(node)) : (props.ref = node)))
58
59
  effect(() => assign(node, props, true, prevProps, true))
59
60
  return prevProps
60
61
  }
@@ -344,10 +345,10 @@ function style(node:Node, value:any, prev:any) {
344
345
  }
345
346
 
346
347
  function toPropertyName(name:string):string {
347
- return name.toLowerCase().replace(/-([a-z])/g, (_:any, w:string) => w.toUpperCase());
348
+ return name.toLowerCase().replace(/-([a-z])/g, (_:unknown, w:string) => w.toUpperCase());
348
349
  }
349
350
 
350
- function toggleClassKey(node:any, key:any, value:any) {
351
+ function toggleClassKey(node:Node, key:string, value:boolean) {
351
352
  const classNames = key.trim().split(/\s+/)
352
353
  for (let i = 0, nameLen = classNames.length; i < nameLen; i++)
353
354
  node.classList.toggle(classNames[i], value)
@@ -360,13 +361,13 @@ function appendNodes(parent:Node, array:Node[], marker:null|Node = null) {
360
361
 
361
362
  // Slightly modified version of: https://github.com/WebReflection/udomdiff/blob/master/index.js
362
363
  function reconcileArrays(parentNode:Node, a:Node[], b:Node[]) {
363
- let bLength = b.length,
364
- aEnd = a.length,
364
+ const bLength = b.length
365
+ let aEnd = a.length,
365
366
  bEnd = bLength,
366
367
  aStart = 0,
367
368
  bStart = 0,
368
- after = a[aEnd - 1].nextSibling,
369
369
  map:Map<Node,number>|null = null;
370
+ const after = a[aEnd - 1].nextSibling
370
371
 
371
372
  while (aStart < aEnd || bStart < bEnd) {
372
373
  // common prefix
@@ -1,7 +1,7 @@
1
1
  import {runtime} from '../src/runtime.ts'
2
2
  import {hyperscript} from '../src/hyperscript.ts'
3
3
  import {signal,root} from '../src/reactive.ts'
4
- import {JSDOM} from 'npm:jsdom'
4
+ import {JSDOM} from 'jsdom'
5
5
  import {assertEquals} from '@std/assert'
6
6
 
7
7
  const {window} = new JSDOM('<!DOCTYPE html>', {runScripts:'dangerously'})
@@ -36,7 +36,7 @@ testing('h with basic element', {skip:true}, async test => {
36
36
  await test("number content", () => assertHTML(h('i',1), '<i>1</i>'))
37
37
  await test("bigint content", () => assertHTML(h('i',2n), '<i>2</i>'))
38
38
  await test("symbol content", () => assertHTML(h('i',Symbol('A')), '<i>Symbol(A)</i>'))
39
- await test('regex content', () => assertHTML(h('b',/\w/), '<b>/\\w/</b>'))
39
+ // await test('regex content', () => assertHTML(h('b',/\w/), '<b>/\\w/</b>'))
40
40
  await test("signal content", () => assertHTML(h('i',()=>1), '<i>1</i>'))
41
41
  await test('array content', () => assertHTML(h('i',['A',1,2n]), '<i>A12</i>'))
42
42
  await test('style attribute', () => assertHTML(h('hr',{style:'color:red'}), '<hr style="color: red;">'))
package/test/reactive.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import {assertEquals,assert} from '@std/assert'
2
2
  import {describe,it} from '@std/testing/bdd'
3
3
 
4
- import {signal,effect,sample,batch,memo,context} from '../src/reactive.ts'
5
- import {wrap} from '../src/controlflow.js'
4
+ import {signal,effect,untrack,batch,memo,context,useContext,root,wrap} from '../src/reactive.ts'
6
5
 
7
6
  describe('signal', () => {
8
7
  const a = signal(1)
@@ -14,6 +13,15 @@ describe('signal', () => {
14
13
  assertEquals(a(null),null)
15
14
  })
16
15
 
16
+ describe('signal with equals option', () => {
17
+ const n = signal(0,{equals:false})
18
+ let m = 0
19
+ effect(() => m += 1 + n() )
20
+ assertEquals(m,1)
21
+ assertEquals(n(0),0)
22
+ assertEquals(m,2)
23
+ })
24
+
17
25
  describe('effect', () => {
18
26
  const n = signal(1)
19
27
  let m = 0
@@ -24,10 +32,10 @@ describe('effect', () => {
24
32
  assertEquals(m,3)
25
33
  })
26
34
 
27
- describe('sample',()=>{
35
+ describe('untrack',()=>{
28
36
  const n = signal(1)
29
37
  let m = 0
30
- effect(()=>m = sample(n))
38
+ effect(()=>m = untrack(n))
31
39
  assertEquals(m,1)
32
40
  n(2)
33
41
  assertEquals(m,1)