@omega.js/client 0.1.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/LICENSE +98 -0
- package/README.md +874 -0
- package/dist/index.js +999 -0
- package/dist/modules/analytics.js +584 -0
- package/dist/modules/auth.js +469 -0
- package/dist/modules/bindings.js +319 -0
- package/dist/modules/device.js +282 -0
- package/dist/modules/dom.js +96 -0
- package/dist/modules/features.js +30 -0
- package/dist/modules/firestore.js +313 -0
- package/dist/modules/form-manager.js +1577 -0
- package/dist/modules/icon-core.js +226 -0
- package/dist/modules/icon-renderer.js +149 -0
- package/dist/modules/live-page.js +235 -0
- package/dist/modules/logger.js +36 -0
- package/dist/modules/motion.js +853 -0
- package/dist/modules/notifications.js +433 -0
- package/dist/modules/path-prefix.js +22 -0
- package/dist/modules/request.js +223 -0
- package/dist/modules/sentry.js +108 -0
- package/dist/modules/service-worker.js +237 -0
- package/dist/modules/storage.js +133 -0
- package/dist/modules/triggers.js +117 -0
- package/dist/modules/utilities.js +479 -0
- package/dist/modules/vert-document.js +354 -0
- package/dist/modules/verts.js +1133 -0
- package/dist/vendor/account/engine.js +182 -0
- package/dist/vendor/account/features.js +220 -0
- package/dist/vendor/account/index.js +53 -0
- package/dist/vendor/account/schema.js +272 -0
- package/dist/vendor/account/subscription.js +38 -0
- package/dist/vendor/analytics/adapters/ga4.js +26 -0
- package/dist/vendor/analytics/adapters/meta.js +26 -0
- package/dist/vendor/analytics/adapters/resolve.js +130 -0
- package/dist/vendor/analytics/adapters/tiktok.js +27 -0
- package/dist/vendor/analytics/catalog.js +908 -0
- package/dist/vendor/analytics/consent.js +49 -0
- package/dist/vendor/analytics/core.js +141 -0
- package/dist/vendor/analytics/identity.js +136 -0
- package/dist/vendor/analytics/index.js +170 -0
- package/dist/vendor/analytics/logger.js +40 -0
- package/dist/vendor/analytics/transports/browser.js +110 -0
- package/dist/vendor/monitoring/browser.js +207 -0
- package/dist/vendor/monitoring/core.js +180 -0
- package/dist/vendor/monitoring/logger.js +39 -0
- package/docs/architecture.md +59 -0
- package/docs/bindings.md +235 -0
- package/docs/build-system.md +32 -0
- package/docs/cdp-debugging.md +29 -0
- package/docs/code-patterns.md +96 -0
- package/docs/common-tasks.md +36 -0
- package/docs/dependencies.md +19 -0
- package/docs/index.md +159 -0
- package/docs/modules.md +180 -0
- package/docs/shared/agent-docs.md +89 -0
- package/docs/shared/analytics.md +612 -0
- package/docs/shared/brands.md +51 -0
- package/docs/shared/breaking-changes.md +497 -0
- package/docs/shared/config.md +1387 -0
- package/docs/shared/deploys.md +215 -0
- package/docs/shared/icons.md +201 -0
- package/docs/shared/local-dev.md +147 -0
- package/docs/shared/logging.md +202 -0
- package/docs/shared/monitoring.md +153 -0
- package/docs/shared/publishing.md +183 -0
- package/docs/shared/rulings.md +34 -0
- package/docs/shared/testing.md +147 -0
- package/docs/shared/theming.md +604 -0
- package/docs/shared/translation.md +291 -0
- package/docs/shared/updates.md +61 -0
- package/docs/testing.md +9 -0
- package/package.json +65 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The client runtime's error reporting — a HOST of `@omega.js/monitoring`,
|
|
3
|
+
* never a second copy of its policy (#380).
|
|
4
|
+
*
|
|
5
|
+
* What lives here is what only the client can know: when to load the SDK, which
|
|
6
|
+
* release tag this page is (`brand.id@version`), where the signed-in user
|
|
7
|
+
* comes from (the auth storage keys), and the public surface every consumer
|
|
8
|
+
* calls — `omega.sentry().captureException(err)`.
|
|
9
|
+
*
|
|
10
|
+
* Everything else — the send gate, the @omega.js-bundle filter, the PII scrub,
|
|
11
|
+
* the integrations — is the package's, shared with the backend and the desktop
|
|
12
|
+
* main process so no surface can drift.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import monitoring from '../vendor/monitoring/browser.js';
|
|
16
|
+
import monitoringCore from '../vendor/monitoring/core.js';
|
|
17
|
+
import { createLogger } from './logger.js';
|
|
18
|
+
|
|
19
|
+
const logger = createLogger('sentry');
|
|
20
|
+
|
|
21
|
+
class mod {
|
|
22
|
+
constructor(manager) {
|
|
23
|
+
this.manager = manager;
|
|
24
|
+
this.initialized = false;
|
|
25
|
+
this.Sentry = null;
|
|
26
|
+
this.config = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Initialize Sentry error tracking
|
|
31
|
+
* @param {Object} config - the resolved Sentry settings for this surface
|
|
32
|
+
* (the build maps `monitoring.providers.sentry` into `config.sentry.config`)
|
|
33
|
+
* @returns {Promise} Resolves when initialization is complete
|
|
34
|
+
*/
|
|
35
|
+
init(config = {}) {
|
|
36
|
+
// Dynamically imported to keep the SDK out of the initial chunk — and
|
|
37
|
+
// never reached at all when the config carries no DSN (index.js gates on
|
|
38
|
+
// `config.sentry.enabled`, which the build maps from
|
|
39
|
+
// `monitoring.providers.sentry.dsn`).
|
|
40
|
+
return import('@sentry/browser')
|
|
41
|
+
.then((sdk) => {
|
|
42
|
+
this.Sentry = sdk;
|
|
43
|
+
|
|
44
|
+
// Expose globally so page code and devtools can reach the same SDK
|
|
45
|
+
if (typeof window !== 'undefined') {
|
|
46
|
+
window.Sentry = sdk;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.config = monitoring.buildInitOptions({
|
|
50
|
+
Sentry: sdk,
|
|
51
|
+
config,
|
|
52
|
+
release: monitoringCore.releaseTag({
|
|
53
|
+
id: this.manager.config.brand?.id,
|
|
54
|
+
// The host blob's app version when it carries one (@omega.js/extension
|
|
55
|
+
// bakes it, and it is the same key device stats read) — the build stamp
|
|
56
|
+
// is the fallback for a surface that ships no version yet.
|
|
57
|
+
version: this.manager.config.version || this.manager.config.buildTime,
|
|
58
|
+
}),
|
|
59
|
+
environment: this.manager.config.environment,
|
|
60
|
+
isDevelopment: () => this.manager.isDevelopment(),
|
|
61
|
+
getUser: () => {
|
|
62
|
+
const storage = this.manager.storage();
|
|
63
|
+
return {
|
|
64
|
+
uid: storage.get('auth.user.uid', ''),
|
|
65
|
+
email: storage.get('auth.user.email', ''),
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
sdk.init(this.config);
|
|
71
|
+
this.initialized = true;
|
|
72
|
+
|
|
73
|
+
return { initialized: true };
|
|
74
|
+
})
|
|
75
|
+
.catch((error) => {
|
|
76
|
+
logger.error('Failed to initialize:', error);
|
|
77
|
+
throw error;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Capture an exception and send to Sentry
|
|
83
|
+
* Safe to call even if Sentry is not initialized
|
|
84
|
+
* @param {Error} error - The error to capture
|
|
85
|
+
* @param {Object} captureContext - Additional context for the error
|
|
86
|
+
* @returns {string|null} Event ID if successful, null otherwise
|
|
87
|
+
*/
|
|
88
|
+
captureException(error, captureContext) {
|
|
89
|
+
// Log the error
|
|
90
|
+
logger.error('Capturing exception:', error);
|
|
91
|
+
|
|
92
|
+
// Safe to call - won't throw if not initialized
|
|
93
|
+
if (!this.initialized) {
|
|
94
|
+
logger.log('Not initialized, skipping capture');
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Call Sentry to capture the exception
|
|
99
|
+
try {
|
|
100
|
+
return this.Sentry.captureException(error, captureContext);
|
|
101
|
+
} catch (captureError) {
|
|
102
|
+
logger.error('Failed to capture exception:', captureError);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default mod;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { createLogger } from './logger.js';
|
|
2
|
+
import { pathPrefix } from './path-prefix.js';
|
|
3
|
+
|
|
4
|
+
const logger = createLogger('service-worker');
|
|
5
|
+
|
|
6
|
+
class ServiceWorker {
|
|
7
|
+
constructor(manager) {
|
|
8
|
+
this.manager = manager;
|
|
9
|
+
this._registration = null;
|
|
10
|
+
this._messageHandlers = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Check if service workers are supported
|
|
14
|
+
isSupported() {
|
|
15
|
+
return 'serviceWorker' in navigator;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Return promise that resolves when service worker is ready
|
|
19
|
+
async ready() {
|
|
20
|
+
if (!this.isSupported()) {
|
|
21
|
+
throw new Error('Service Workers not supported');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If already registered and active
|
|
25
|
+
if (this._registration?.active) {
|
|
26
|
+
return this._registration;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Wait for service worker to be ready
|
|
30
|
+
const registration = await navigator.serviceWorker.ready;
|
|
31
|
+
return registration;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Register service worker
|
|
35
|
+
async register(options = {}) {
|
|
36
|
+
try {
|
|
37
|
+
if (!this.isSupported()) {
|
|
38
|
+
console.warn('Service Workers are not supported');
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Mount the script and the scope under the page's base path (#360): a
|
|
43
|
+
// worker served at /<prefix>/service-worker.js can only claim
|
|
44
|
+
// /<prefix>/. The prefix rides the script URL as a query param because
|
|
45
|
+
// the worker has no document to read the stamp from — it reads it back
|
|
46
|
+
// off self.location.search. No prefix leaves both untouched.
|
|
47
|
+
const prefix = pathPrefix();
|
|
48
|
+
const configuredPath = options.path || this.manager.config.serviceWorker?.config?.path || '/service-worker.js';
|
|
49
|
+
const swPath = prefix && configuredPath.startsWith('/') && !configuredPath.startsWith('//')
|
|
50
|
+
? `${prefix}${configuredPath}`
|
|
51
|
+
: configuredPath;
|
|
52
|
+
const swUrl = prefix ? `${swPath}?omega-path-prefix=${encodeURIComponent(prefix)}` : swPath;
|
|
53
|
+
const scope = options.scope || `${prefix}/`;
|
|
54
|
+
|
|
55
|
+
// Build config object to pass to service worker
|
|
56
|
+
const config = {
|
|
57
|
+
brand: this.manager.config.brand?.id,
|
|
58
|
+
environment: this.manager.config.environment,
|
|
59
|
+
buildTime: this.manager.config.buildTime,
|
|
60
|
+
firebase: this.manager._resolveFirebaseConfig()
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Register service worker
|
|
64
|
+
const registration = await navigator.serviceWorker.register(swUrl, {
|
|
65
|
+
scope,
|
|
66
|
+
updateViaCache: 'none'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Store registration
|
|
70
|
+
this._registration = registration;
|
|
71
|
+
this.manager.state.serviceWorker = registration;
|
|
72
|
+
|
|
73
|
+
// Wait for service worker to be ready and send config
|
|
74
|
+
await navigator.serviceWorker.ready;
|
|
75
|
+
|
|
76
|
+
// Send config to active service worker
|
|
77
|
+
// Removed due to issues init'ing firebase asynchronously in SW (now config is fetched directly in SW)
|
|
78
|
+
// if (registration.active) {
|
|
79
|
+
// try {
|
|
80
|
+
// this.postMessage({
|
|
81
|
+
// command: 'update-config',
|
|
82
|
+
// payload: config
|
|
83
|
+
// });
|
|
84
|
+
// } catch (error) {
|
|
85
|
+
// console.warn('Could not send config to service worker:', error);
|
|
86
|
+
// }
|
|
87
|
+
// }
|
|
88
|
+
|
|
89
|
+
// Resolve with registration
|
|
90
|
+
return registration;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Service Worker registration failed:', error);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Unregister every service worker claiming this origin. Dev-loop hygiene:
|
|
98
|
+
// a previous project's service worker on the same localhost port would
|
|
99
|
+
// otherwise keep controlling pages and serving its stale caches.
|
|
100
|
+
async unregisterAll() {
|
|
101
|
+
if (!this.isSupported()) {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
107
|
+
|
|
108
|
+
await Promise.all(registrations.map((registration) => registration.unregister()));
|
|
109
|
+
|
|
110
|
+
if (registrations.length > 0) {
|
|
111
|
+
logger.log(`Unregistered ${registrations.length} service worker(s) claiming this origin`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
this._registration = null;
|
|
115
|
+
|
|
116
|
+
return registrations.length;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.warn('Failed to unregister service workers:', error);
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get current registration
|
|
124
|
+
getRegistration() {
|
|
125
|
+
return this._registration;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Post message to service worker
|
|
129
|
+
postMessage(message, options = {}) {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
// Check support
|
|
132
|
+
if (!this.isSupported()) {
|
|
133
|
+
return reject(new Error('Service Workers not supported'));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get active service worker
|
|
137
|
+
const controller = this._registration?.active || navigator.serviceWorker.controller;
|
|
138
|
+
|
|
139
|
+
if (!controller) {
|
|
140
|
+
return reject(new Error('No active service worker'));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Create message channel for two-way communication
|
|
144
|
+
const messageChannel = new MessageChannel();
|
|
145
|
+
const timeout = options.timeout || 5000;
|
|
146
|
+
let timeoutId;
|
|
147
|
+
|
|
148
|
+
// Set up timeout to prevent hanging
|
|
149
|
+
if (timeout > 0) {
|
|
150
|
+
timeoutId = setTimeout(() => {
|
|
151
|
+
messageChannel.port1.close();
|
|
152
|
+
reject(new Error('Service worker message timeout'));
|
|
153
|
+
}, timeout);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Listen for response from service worker
|
|
157
|
+
messageChannel.port1.onmessage = (event) => {
|
|
158
|
+
clearTimeout(timeoutId);
|
|
159
|
+
|
|
160
|
+
if (event.data.error) {
|
|
161
|
+
reject(new Error(event.data.error));
|
|
162
|
+
} else {
|
|
163
|
+
resolve(event.data);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Send message with port for reply
|
|
168
|
+
controller.postMessage(message, [messageChannel.port2]);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Listen for messages from service worker
|
|
173
|
+
onMessage(type, handler) {
|
|
174
|
+
if (!this.isSupported()) {
|
|
175
|
+
return () => {};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Store handler
|
|
179
|
+
if (!this._messageHandlers.has(type)) {
|
|
180
|
+
this._messageHandlers.set(type, new Set());
|
|
181
|
+
}
|
|
182
|
+
this._messageHandlers.get(type).add(handler);
|
|
183
|
+
|
|
184
|
+
// Set up global message listener if not already done
|
|
185
|
+
if (this._messageHandlers.size === 1) {
|
|
186
|
+
navigator.serviceWorker.addEventListener('message', this._handleMessage.bind(this));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Return unsubscribe function
|
|
190
|
+
return () => {
|
|
191
|
+
const handlers = this._messageHandlers.get(type);
|
|
192
|
+
if (handlers) {
|
|
193
|
+
handlers.delete(handler);
|
|
194
|
+
if (handlers.size === 0) {
|
|
195
|
+
this._messageHandlers.delete(type);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Get service worker state
|
|
202
|
+
getState() {
|
|
203
|
+
if (!this._registration) {
|
|
204
|
+
return 'none';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (this._registration.installing) {
|
|
208
|
+
return 'installing';
|
|
209
|
+
} else if (this._registration.waiting) {
|
|
210
|
+
return 'waiting';
|
|
211
|
+
} else if (this._registration.active) {
|
|
212
|
+
return 'active';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return 'unknown';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Private: Handle incoming messages
|
|
219
|
+
_handleMessage(event) {
|
|
220
|
+
const { type, ...data } = event.data || {};
|
|
221
|
+
|
|
222
|
+
if (!type) return;
|
|
223
|
+
|
|
224
|
+
const handlers = this._messageHandlers.get(type);
|
|
225
|
+
if (handlers) {
|
|
226
|
+
handlers.forEach(handler => {
|
|
227
|
+
try {
|
|
228
|
+
handler(data, event);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('Message handler error:', error);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export default ServiceWorker;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import lodash from 'lodash';
|
|
2
|
+
const { get: _get, set: _set } = lodash;
|
|
3
|
+
|
|
4
|
+
class Storage {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.storageKey = '_manager';
|
|
7
|
+
this.pseudoStorage = {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get(path, defaultValue) {
|
|
11
|
+
let usableStorage;
|
|
12
|
+
|
|
13
|
+
// Try to parse the localStorage object
|
|
14
|
+
try {
|
|
15
|
+
usableStorage = JSON.parse(window.localStorage.getItem(this.storageKey) || '{}');
|
|
16
|
+
} catch (e) {
|
|
17
|
+
usableStorage = this.pseudoStorage;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// If there's no path, return the entire storage object
|
|
21
|
+
if (!path) {
|
|
22
|
+
return usableStorage || defaultValue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Return the value at the path
|
|
26
|
+
return _get(usableStorage, path, defaultValue);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(path, value) {
|
|
30
|
+
let usableStorage;
|
|
31
|
+
|
|
32
|
+
// Try to get the current storage
|
|
33
|
+
try {
|
|
34
|
+
usableStorage = this.get();
|
|
35
|
+
} catch (e) {
|
|
36
|
+
usableStorage = this.pseudoStorage;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// If there's no path, replace the entire storage object
|
|
40
|
+
if (!path) {
|
|
41
|
+
usableStorage = value || {};
|
|
42
|
+
} else {
|
|
43
|
+
// Set the value at the path
|
|
44
|
+
_set(usableStorage, path, value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Try to set the localStorage object
|
|
48
|
+
try {
|
|
49
|
+
window.localStorage.setItem(this.storageKey, JSON.stringify(usableStorage));
|
|
50
|
+
} catch (e) {
|
|
51
|
+
this.pseudoStorage = usableStorage;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return usableStorage;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
remove(path) {
|
|
58
|
+
if (!path) {
|
|
59
|
+
this.clear();
|
|
60
|
+
} else {
|
|
61
|
+
this.set(path, undefined);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
clear() {
|
|
66
|
+
try {
|
|
67
|
+
window.localStorage.setItem(this.storageKey, '{}');
|
|
68
|
+
} catch (e) {
|
|
69
|
+
this.pseudoStorage = {};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Session storage methods
|
|
74
|
+
session = {
|
|
75
|
+
get: (path, defaultValue) => {
|
|
76
|
+
let usableStorage;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
usableStorage = JSON.parse(window.sessionStorage.getItem(this.storageKey) || '{}');
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return defaultValue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!path) {
|
|
85
|
+
return usableStorage || defaultValue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return _get(usableStorage, path, defaultValue);
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
set: (path, value) => {
|
|
92
|
+
let usableStorage;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
usableStorage = this.session.get();
|
|
96
|
+
} catch (e) {
|
|
97
|
+
usableStorage = {};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!path) {
|
|
101
|
+
usableStorage = value || {};
|
|
102
|
+
} else {
|
|
103
|
+
_set(usableStorage, path, value);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
window.sessionStorage.setItem(this.storageKey, JSON.stringify(usableStorage));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Silent fail
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return usableStorage;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
remove: (path) => {
|
|
116
|
+
if (!path) {
|
|
117
|
+
this.session.clear();
|
|
118
|
+
} else {
|
|
119
|
+
this.session.set(path, undefined);
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
clear: () => {
|
|
124
|
+
try {
|
|
125
|
+
window.sessionStorage.setItem(this.storageKey, '{}');
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// Silent fail
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export default Storage;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* triggers — the ONE click-trigger registry every OMEGA surface shares
|
|
3
|
+
* ([#16](https://github.com/Omega-JS-Stack/omega/issues/16)).
|
|
4
|
+
*
|
|
5
|
+
* A trigger is markup wiring: a class on an element means "clicking this runs
|
|
6
|
+
* that action", with no per-page JS. Before this module every surface rolled
|
|
7
|
+
* its own delegated `document` click listener with its own naming convention
|
|
8
|
+
* (`.auth-signout-btn` in client, `.auth-signin-btn` in extension,
|
|
9
|
+
* `.uj-password-toggle` in web, twice). Now there is exactly ONE listener and
|
|
10
|
+
* exactly one naming rule:
|
|
11
|
+
*
|
|
12
|
+
* registerTrigger('signout', handler) → the class is `omega-signout`
|
|
13
|
+
*
|
|
14
|
+
* The class is ALWAYS `omega-<name>` — callers never spell it, so it can never
|
|
15
|
+
* drift. A click anywhere inside a trigger element counts (closest()), which is
|
|
16
|
+
* what makes icon-only and label-wrapped buttons work.
|
|
17
|
+
*
|
|
18
|
+
* Who registers what: the client registers the GENERIC actions (sign-out), and
|
|
19
|
+
* each surface registers its own (extension: sign-in opens its auth page; web:
|
|
20
|
+
* the password eye). Registration is what arms the listener, so a surface can
|
|
21
|
+
* register before or after `omega.initialize()` — order never matters.
|
|
22
|
+
*
|
|
23
|
+
* Sibling of `motion` and `icon-renderer`: transport-free, DOM-only, and inert
|
|
24
|
+
* where there is no document (desktop main, the extension service worker).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { createLogger } from './logger.js';
|
|
28
|
+
|
|
29
|
+
const logger = createLogger('triggers');
|
|
30
|
+
|
|
31
|
+
// The one prefix. A trigger named `signout` is the class `omega-signout`.
|
|
32
|
+
const PREFIX = 'omega-';
|
|
33
|
+
|
|
34
|
+
// name → handler. Module-level because the registry IS the singleton: one
|
|
35
|
+
// document, one listener, one table.
|
|
36
|
+
const handlers = new Map();
|
|
37
|
+
|
|
38
|
+
// True once the ONE delegated listener is attached.
|
|
39
|
+
let listening = false;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attach the single delegated listener, once.
|
|
43
|
+
*/
|
|
44
|
+
function ensureListener() {
|
|
45
|
+
if (listening || typeof document === 'undefined') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
listening = true;
|
|
50
|
+
document.addEventListener('click', handleClick);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The one delegated handler: find the innermost trigger element the click
|
|
55
|
+
* happened inside, then run every trigger that element carries.
|
|
56
|
+
* @param {Event} event
|
|
57
|
+
*/
|
|
58
|
+
function handleClick(event) {
|
|
59
|
+
if (!handlers.size) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ONE closest() call over the union selector — the INNERMOST trigger wins,
|
|
64
|
+
// so nesting a trigger inside a trigger is deterministic.
|
|
65
|
+
const selector = [...handlers.keys()].map((name) => `.${PREFIX}${name}`).join(',');
|
|
66
|
+
const element = event.target?.closest?.(selector);
|
|
67
|
+
|
|
68
|
+
if (!element) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// A trigger class means the framework owns this click: no default navigation,
|
|
73
|
+
// no page-level handler behind it. Both legacy auth listeners did exactly
|
|
74
|
+
// this, and the markup contract is now the same everywhere.
|
|
75
|
+
event.preventDefault();
|
|
76
|
+
event.stopPropagation();
|
|
77
|
+
|
|
78
|
+
for (const [name, handler] of handlers) {
|
|
79
|
+
if (!element.classList.contains(`${PREFIX}${name}`)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A throwing trigger is logged and never allowed to swallow the others.
|
|
84
|
+
// Async handlers (sign-out awaits Firebase) hand their failure back the
|
|
85
|
+
// same way.
|
|
86
|
+
try {
|
|
87
|
+
const result = handler(event, element);
|
|
88
|
+
if (typeof result?.catch === 'function') {
|
|
89
|
+
result.catch((error) => logger.error(`Trigger "${name}" failed:`, error));
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
logger.error(`Trigger "${name}" failed:`, error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Register a click trigger. The class it answers to is always `omega-<name>`.
|
|
99
|
+
* Re-registering the same name REPLACES the handler (with a warning) — there is
|
|
100
|
+
* no stacking, so a hot reload or a double boot can never double-fire.
|
|
101
|
+
* @param {string} name - trigger name, e.g. 'signout'
|
|
102
|
+
* @param {Function} handler - called with (event, element)
|
|
103
|
+
*/
|
|
104
|
+
export function registerTrigger(name, handler) {
|
|
105
|
+
if (typeof handler !== 'function') {
|
|
106
|
+
throw new Error(`registerTrigger("${name}") needs a handler function`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (handlers.has(name)) {
|
|
110
|
+
logger.warn(`Trigger "${name}" re-registered — the new handler replaces the old one`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
handlers.set(name, handler);
|
|
114
|
+
ensureListener();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export default registerTrigger;
|