brustjs 0.1.19-alpha → 0.1.21-alpha

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.
@@ -0,0 +1,152 @@
1
+ // Minimal pull-based reactive core: push-on-write, pull-on-read, synchronous notify.
2
+ // Framework-agnostic — no react, no dom. Foundation for defineStore (Spec A) and
3
+ // the Alpine-style client runtime (Spec B).
4
+
5
+ // Symbol.for (GLOBAL registry), NOT Symbol(): every island is a SEPARATE Bun.build
6
+ // chunk that inlines its own copy of this module, so a plain `Symbol()` brand would
7
+ // be a DIFFERENT value per chunk — `isSignal` from chunk B then fails to recognize a
8
+ // signal created in chunk A. That poisons the shared store snapshot (a cross-chunk
9
+ // reader computes `{}` and caches it), so e.g. the team dock reads empty after a SPA
10
+ // nav loads a new island chunk. A global registry symbol is identical across chunks.
11
+ const SIGNAL = Symbol.for('brust.signal')
12
+ const COMPUTED = Symbol.for('brust.computed')
13
+
14
+ export interface Signal<T> {
15
+ (): T
16
+ set(next: T | ((prev: T) => T)): void
17
+ readonly [SIGNAL]: true
18
+ }
19
+ export interface Computed<T> {
20
+ (): T
21
+ readonly [COMPUTED]: true
22
+ }
23
+
24
+ export function isSignal(v: unknown): v is Signal<unknown> {
25
+ return typeof v === 'function' && (v as { [SIGNAL]?: true })[SIGNAL] === true
26
+ }
27
+ export function isComputed(v: unknown): v is Computed<unknown> {
28
+ return typeof v === 'function' && (v as { [COMPUTED]?: true })[COMPUTED] === true
29
+ }
30
+
31
+ // A reactive consumer (effect or computed) tracking its dependencies.
32
+ interface Consumer {
33
+ run(): void
34
+ deps: Set<Set<Consumer>>
35
+ running: boolean
36
+ }
37
+
38
+ let activeConsumer: Consumer | null = null
39
+ let batchDepth = 0
40
+ const pendingNotify = new Set<Consumer>()
41
+
42
+ function track(subscribers: Set<Consumer>): void {
43
+ if (activeConsumer) {
44
+ subscribers.add(activeConsumer)
45
+ activeConsumer.deps.add(subscribers)
46
+ }
47
+ }
48
+
49
+ function notify(subscribers: Set<Consumer>): void {
50
+ // Snapshot — a consumer re-running mutates the set.
51
+ for (const c of [...subscribers]) {
52
+ if (batchDepth > 0) pendingNotify.add(c)
53
+ else c.run()
54
+ }
55
+ }
56
+
57
+ function flush(): void {
58
+ const queued = [...pendingNotify]
59
+ pendingNotify.clear()
60
+ for (const c of queued) c.run()
61
+ }
62
+
63
+ export function batch(fn: () => void): void {
64
+ batchDepth++
65
+ try {
66
+ fn()
67
+ } finally {
68
+ batchDepth--
69
+ if (batchDepth === 0) flush()
70
+ }
71
+ }
72
+
73
+ export function signal<T>(initial: T): Signal<T> {
74
+ let value = initial
75
+ const subscribers = new Set<Consumer>()
76
+ const read = (() => {
77
+ track(subscribers)
78
+ return value
79
+ }) as Signal<T>
80
+ read.set = (next: T | ((prev: T) => T)) => {
81
+ const v = typeof next === 'function' ? (next as (p: T) => T)(value) : next
82
+ if (Object.is(v, value)) return
83
+ value = v
84
+ notify(subscribers)
85
+ }
86
+ Object.defineProperty(read, SIGNAL, { value: true })
87
+ return read
88
+ }
89
+
90
+ function clearDeps(c: Consumer): void {
91
+ for (const dep of c.deps) dep.delete(c)
92
+ c.deps.clear()
93
+ }
94
+
95
+ export function computed<T>(fn: () => T): Computed<T> {
96
+ let cached: T
97
+ let dirty = true
98
+ const subscribers = new Set<Consumer>()
99
+ const self: Consumer = {
100
+ deps: new Set(),
101
+ running: false,
102
+ run() {
103
+ if (self.running) return
104
+ self.running = true
105
+ try {
106
+ dirty = true
107
+ notify(subscribers) // downstream recomputes lazily on next read
108
+ } finally {
109
+ self.running = false
110
+ }
111
+ },
112
+ }
113
+ const read = (() => {
114
+ track(subscribers)
115
+ if (dirty) {
116
+ clearDeps(self)
117
+ const prev = activeConsumer
118
+ activeConsumer = self
119
+ try {
120
+ cached = fn()
121
+ dirty = false
122
+ } finally {
123
+ activeConsumer = prev
124
+ }
125
+ }
126
+ return cached
127
+ }) as Computed<T>
128
+ Object.defineProperty(read, COMPUTED, { value: true })
129
+ return read
130
+ }
131
+
132
+ export function effect(fn: () => void): () => void {
133
+ const self: Consumer = {
134
+ deps: new Set(),
135
+ running: false,
136
+ run() {
137
+ if (self.running) return
138
+ self.running = true
139
+ clearDeps(self)
140
+ const prev = activeConsumer
141
+ activeConsumer = self
142
+ try {
143
+ fn()
144
+ } finally {
145
+ activeConsumer = prev
146
+ self.running = false
147
+ }
148
+ },
149
+ }
150
+ self.run()
151
+ return () => clearDeps(self)
152
+ }
@@ -1,25 +0,0 @@
1
- // Cross-island sync bus.
2
- //
3
- // GAP S4: brust has no cross-island shared-state primitive. AddToTeamButton and
4
- // TeamBuilder are two SEPARATE island chunks (each its own Bun.build bundle), so
5
- // a module-scope store imported by both would be DUPLICATED — two instances that
6
- // never see each other. The one thing both chunks genuinely share is the
7
- // `window` object, so we coordinate through a window CustomEvent. This works,
8
- // but it's a hand-rolled workaround for a pattern (cart / team / selection) that
9
- // most apps need. See ../FRAMEWORK-GAPS.md S4.
10
-
11
- import type { TeamMember } from '../lib/types'
12
-
13
- export const TEAM_EVENT = 'brust-pokedex:team'
14
-
15
- export function emitTeam(team: TeamMember[]): void {
16
- if (typeof window === 'undefined') return
17
- window.dispatchEvent(new CustomEvent<TeamMember[]>(TEAM_EVENT, { detail: team }))
18
- }
19
-
20
- export function onTeam(fn: (team: TeamMember[]) => void): () => void {
21
- if (typeof window === 'undefined') return () => {}
22
- const handler = (e: Event) => fn((e as CustomEvent<TeamMember[]>).detail)
23
- window.addEventListener(TEAM_EVENT, handler)
24
- return () => window.removeEventListener(TEAM_EVENT, handler)
25
- }