@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
|
@@ -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').
|
|
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
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
bus.use(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
(
|
|
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.
|
|
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
|
|
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)
|