@voxgig/build 4.3.0 → 4.4.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voxgig/build",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "main": "dist/build.js",
5
5
  "type": "commonjs",
6
6
  "types": "dist/build.d.ts",
@@ -5,6 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>$$Name$$</title>
7
7
  <script src="/seneca-browser.js"></script>
8
+ <script src="/seneca-browser-store.js"></script>
9
+ <script src="/seneca-browser-debug.js"></script>
8
10
  </head>
9
11
  <body>
10
12
  <vg-app></vg-app>
@@ -4,13 +4,15 @@
4
4
  "private": true,
5
5
  "description": "$$Name$$ SPA: web components on a Seneca service bus",
6
6
  "scripts": {
7
- "postinstall": "node -e \"require('fs').copyFileSync(require.resolve('seneca-browser'),'public/seneca-browser.js')\"",
7
+ "postinstall": "node -e \"const fs=require('fs');fs.mkdirSync('public',{recursive:true});for(const p of ['seneca-browser','@voxgig/seneca-browser-store','@voxgig/seneca-browser-debug'])fs.copyFileSync(require.resolve(p),'public/'+p.replace('@voxgig/','')+'.js')\"",
8
8
  "dev": "vite",
9
9
  "build": "vite build",
10
10
  "e2e": "playwright test"
11
11
  },
12
12
  "devDependencies": {
13
13
  "@playwright/test": "^1.58.2",
14
+ "@voxgig/seneca-browser-debug": "^0.1.0",
15
+ "@voxgig/seneca-browser-store": "^0.1.0",
14
16
  "seneca-browser": "^8.0.0-rc4",
15
17
  "vite": "^7.3.4"
16
18
  }
@@ -9,6 +9,10 @@ export default defineConfig({
9
9
  testDir: './e2e',
10
10
  timeout: 30000,
11
11
  fullyParallel: false,
12
+ // One worker: all specs share a single backend web runner with one
13
+ // in-memory store and the same seeded users, so they must run serially to
14
+ // avoid cross-spec contention on shared state.
15
+ workers: 1,
12
16
  reporter: [['list']],
13
17
  use: {
14
18
  baseURL: `http://localhost:${PORT}`,
@@ -8,7 +8,7 @@
8
8
  // Cross-component notification uses the tiny `evt` plugin below:
9
9
  // components post cmp:bus,emit:<name> and subscribe with onEvent().
10
10
 
11
- /* global Seneca */
11
+ /* global Seneca, SenecaBrowserStore, SenecaBrowserDebug */
12
12
 
13
13
  const bus = Seneca({
14
14
  legacy: false,
@@ -26,31 +26,48 @@ const bus = Seneca({
26
26
  .client({ type: 'browser', pin: 'aim:*' })
27
27
 
28
28
 
29
- // Event fanout: seneca actions have single handlers, so the bus plugin
30
- // keeps a listener registry for UI notification.
31
- const listeners = {}
32
-
33
- bus.use(function evt() {
34
- this.add('cmp:bus,emit:evt', function (msg, reply) {
35
- const subs = listeners[msg.name] || []
36
- for (const fn of subs) {
37
- try {
38
- fn(msg.data || {})
39
- }
40
- catch (e) {
41
- console.error('evt listener failed', msg.name, e)
42
- }
43
- }
44
- reply({ ok: true, count: subs.length })
45
- })
29
+ // Transparent state cache (Redux-like): caches aim:* query results and
30
+ // invalidates on writes, so repeated reads are served without a round-trip.
31
+ // Registered AFTER .client() so the aim:* pin actions exist to be wrapped.
32
+ if ('undefined' !== typeof SenecaBrowserStore) {
33
+ bus.use(SenecaBrowserStore, { pin: 'aim:*' })
34
+ }
35
+
36
+ // In-window devtools: a resizable/hideable pop-up showing live Seneca
37
+ // message flows, config, and (via the store plugin above) the cache tree.
38
+ // aim:* messages are the remote (backend) calls. Loaded as a global script
39
+ // in index.html; guarded so a missing bundle never breaks the app.
40
+ if ('undefined' !== typeof SenecaBrowserDebug) {
41
+ bus.use(SenecaBrowserDebug, { remotePins: 'aim:*' })
42
+ }
43
+
44
+
45
+ // Event fanout, unified on Seneca messages: cross-component events use the
46
+ // bus's own sub/act. Unlike a plain `add` (single handler), `sub` allows
47
+ // many observers per pattern - so it IS the pub/sub primitive. A no-op sink
48
+ // action lets `act` resolve; observers subscribe via `sub`. This is the same
49
+ // mechanism reactivity uses (see @seneca/browser-store's changed:* events).
50
+ bus.add('cmp:evt', function (msg, reply) {
51
+ reply()
46
52
  })
47
53
 
48
54
  function onEvent(name, fn) {
49
- (listeners[name] = listeners[name] || []).push(fn)
55
+ bus.sub('cmp:evt,name:' + name, function (msg) {
56
+ fn(msg.data || {})
57
+ })
50
58
  }
51
59
 
52
60
  function emit(name, data) {
53
- bus.post('cmp:bus,emit:evt', { name, data })
61
+ bus.act('cmp:evt,name:' + name, { data })
62
+ }
63
+
64
+
65
+ // Avoid cross-user staleness: drop all cached queries whenever auth changes
66
+ // (sign in / sign out), since cached results are principal-scoped.
67
+ if ('undefined' !== typeof SenecaBrowserStore) {
68
+ onEvent('auth', function () {
69
+ bus.act('sys:browser-store,clear:store', function () {})
70
+ })
54
71
  }
55
72
 
56
73
 
@@ -58,4 +75,4 @@ export {
58
75
  bus,
59
76
  onEvent,
60
77
  emit,
61
- }
78
+ }
@@ -4,7 +4,7 @@
4
4
  // <vg-entity-admin> component renders nav / list / form and talks to the
5
5
  // plugin (and through it, the backend) only via bus messages.
6
6
 
7
- import { bus, emit } from '../bus.js'
7
+ import { bus } from '../bus.js'
8
8
 
9
9
 
10
10
  // Fields managed by the system, not the form.
@@ -58,12 +58,14 @@ bus.use(function entity_admin() {
58
58
  })
59
59
  })
60
60
 
61
+ // Save/remove just pass through to the backend. The UI refresh is NOT
62
+ // triggered here: the store plugin emits a sys:browser-store,changed:group
63
+ // message on the write, and <vg-entity-admin> re-lists reactively in
64
+ // response (see connectedCallback). This decouples "who mutated" from
65
+ // "who must refresh".
61
66
  seneca.add('cmp:entity_admin,save:item', function (msg, reply) {
62
67
  this.act({ ...msgFor(msg.canon, 'save'), item: msg.item },
63
68
  function (err, out) {
64
- if (!err && out.ok) {
65
- emit('entity', { canon: msg.canon })
66
- }
67
69
  reply(err ? { ok: false, why: err.message } : out)
68
70
  })
69
71
  })
@@ -71,9 +73,6 @@ bus.use(function entity_admin() {
71
73
  seneca.add('cmp:entity_admin,remove:item', function (msg, reply) {
72
74
  this.act({ ...msgFor(msg.canon, 'remove'), id: msg.id },
73
75
  function (err, out) {
74
- if (!err && out.ok) {
75
- emit('entity', { canon: msg.canon })
76
- }
77
76
  reply(err ? { ok: false, why: err.message } : out)
78
77
  })
79
78
  })
@@ -101,6 +100,20 @@ class VgEntityAdmin extends HTMLElement {
101
100
  }
102
101
 
103
102
  connectedCallback() {
103
+ // Reactivity via Seneca messages: re-list whenever the store signals that
104
+ // this entity's data changed - regardless of which component wrote it.
105
+ // `sub` is Seneca's native fan-out (many observers per pattern), so this
106
+ // is the same messaging primitive used everywhere else.
107
+ bus.sub('sys:browser-store,changed:group', (msg) => {
108
+ if (this.isConnected && msg.group === this.canon) {
109
+ this.renderList()
110
+ }
111
+ })
112
+ bus.sub('sys:browser-store,changed:all', () => {
113
+ if (this.isConnected && this.canon) {
114
+ this.renderList()
115
+ }
116
+ })
104
117
  this.render()
105
118
  }
106
119
 
@@ -144,6 +157,11 @@ class VgEntityAdmin extends HTMLElement {
144
157
  }
145
158
 
146
159
  async renderList() {
160
+ // May be invoked reactively (store change event) before the first full
161
+ // render has built the DOM - skip until the list container exists.
162
+ if (!this.entities || !this.querySelector('#vg-list')) {
163
+ return
164
+ }
147
165
  const out = await bus.post('cmp:entity_admin,list:item',
148
166
  { canon: this.canon })
149
167
  const items = (out.ok && out.list) || []
@@ -183,8 +201,8 @@ class VgEntityAdmin extends HTMLElement {
183
201
  b.onclick = async () => {
184
202
  await bus.post('cmp:entity_admin,remove:item',
185
203
  { canon: this.canon, id: b.dataset.id })
204
+ // The list refresh comes reactively from the store's changed event.
186
205
  this.editing = null
187
- await this.renderList()
188
206
  this.renderForm()
189
207
  }
190
208
  }
@@ -243,12 +261,12 @@ class VgEntityAdmin extends HTMLElement {
243
261
  'save failed: ' + (res.why || '')
244
262
  }
245
263
  else {
264
+ // The list refresh comes reactively from the store's changed event.
246
265
  this.editing = null
247
- await this.renderList()
248
266
  this.renderForm()
249
267
  }
250
268
  }
251
269
  }
252
270
  }
253
271
 
254
- customElements.define('vg-entity-admin', VgEntityAdmin)
272
+ customElements.define('vg-entity-admin', VgEntityAdmin)