@statezero/core 0.1.66 → 0.1.68
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/dist/core/eventReceivers.d.ts +7 -1
- package/dist/core/eventReceivers.js +76 -21
- package/dist/syncEngine/registries/metricRegistry.js +1 -1
- package/dist/syncEngine/registries/querysetStoreRegistry.js +1 -1
- package/dist/syncEngine/stores/querysetStore.d.ts +1 -2
- package/dist/syncEngine/stores/querysetStore.js +6 -9
- package/package.json +1 -1
- package/readme.md +0 -12
|
@@ -69,11 +69,17 @@ export class PusherEventReceiver {
|
|
|
69
69
|
*/
|
|
70
70
|
constructor(options: PusherReceiverOptions, configKey: string);
|
|
71
71
|
configKey: string;
|
|
72
|
+
connectionTimeoutId: NodeJS.Timeout;
|
|
72
73
|
pusherClient: Pusher;
|
|
73
74
|
formatChannelName: (ns: string) => string;
|
|
74
75
|
namespaceResolver: (modelName: string) => string;
|
|
75
76
|
channels: Map<any, any>;
|
|
76
77
|
eventHandlers: Set<any>;
|
|
78
|
+
/**
|
|
79
|
+
* @private
|
|
80
|
+
* @param {string} reason
|
|
81
|
+
*/
|
|
82
|
+
private _logConnectionError;
|
|
77
83
|
/**
|
|
78
84
|
* Set the namespace resolver function.
|
|
79
85
|
* @param {NamespaceResolver} resolver
|
|
@@ -176,4 +182,4 @@ export type PusherReceiverOptions = {
|
|
|
176
182
|
*/
|
|
177
183
|
namespaceResolver?: NamespaceResolver | undefined;
|
|
178
184
|
};
|
|
179
|
-
import Pusher from
|
|
185
|
+
import Pusher from "pusher-js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Pusher from
|
|
1
|
+
import Pusher from "pusher-js";
|
|
2
2
|
/**
|
|
3
3
|
* Structure of events received from the server.
|
|
4
4
|
* @typedef {Object} ModelEvent
|
|
@@ -19,11 +19,11 @@ import Pusher from 'pusher-js';
|
|
|
19
19
|
* @enum {string}
|
|
20
20
|
*/
|
|
21
21
|
export const EventType = {
|
|
22
|
-
CREATE:
|
|
23
|
-
UPDATE:
|
|
24
|
-
DELETE:
|
|
25
|
-
BULK_UPDATE:
|
|
26
|
-
BULK_DELETE:
|
|
22
|
+
CREATE: "create",
|
|
23
|
+
UPDATE: "update",
|
|
24
|
+
DELETE: "delete",
|
|
25
|
+
BULK_UPDATE: "bulk_update",
|
|
26
|
+
BULK_DELETE: "bulk_delete",
|
|
27
27
|
};
|
|
28
28
|
/**
|
|
29
29
|
* Callback for handling model events.
|
|
@@ -62,18 +62,67 @@ export class PusherEventReceiver {
|
|
|
62
62
|
*/
|
|
63
63
|
constructor(options, configKey) {
|
|
64
64
|
const { clientOptions, formatChannelName, namespaceResolver } = options;
|
|
65
|
+
const CONNECTION_TIMEOUT = 10000; // 10 seconds
|
|
65
66
|
this.configKey = configKey;
|
|
67
|
+
this.connectionTimeoutId = null;
|
|
68
|
+
if (clientOptions.appKey &&
|
|
69
|
+
/^\d+$/.test(clientOptions.appKey) &&
|
|
70
|
+
clientOptions.appKey.length < 15) {
|
|
71
|
+
console.warn(`%c[Pusher Warning] The provided appKey ("${clientOptions.appKey}") looks like a numeric app_id. Pusher requires the alphanumeric key, not the ID. Please verify your configuration for backend: "${this.configKey}".`, "color: orange; font-weight: bold; font-size: 14px;");
|
|
72
|
+
}
|
|
66
73
|
this.pusherClient = new Pusher(clientOptions.appKey, {
|
|
67
74
|
cluster: clientOptions.cluster,
|
|
68
75
|
forceTLS: clientOptions.forceTLS ?? true,
|
|
69
76
|
authEndpoint: clientOptions.authEndpoint,
|
|
70
|
-
auth: { headers: clientOptions.getAuthHeaders?.() || {} }
|
|
77
|
+
auth: { headers: clientOptions.getAuthHeaders?.() || {} },
|
|
71
78
|
});
|
|
72
|
-
this.
|
|
73
|
-
|
|
79
|
+
this.pusherClient.connection.bind("connected", () => {
|
|
80
|
+
console.log(`Pusher client connected successfully for backend: ${this.configKey}.`);
|
|
81
|
+
if (this.connectionTimeoutId) {
|
|
82
|
+
clearTimeout(this.connectionTimeoutId);
|
|
83
|
+
this.connectionTimeoutId = null;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
this.pusherClient.connection.bind("failed", () => {
|
|
87
|
+
this._logConnectionError("Pusher connection explicitly failed.");
|
|
88
|
+
if (this.connectionTimeoutId) {
|
|
89
|
+
clearTimeout(this.connectionTimeoutId);
|
|
90
|
+
this.connectionTimeoutId = null;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
this.connectionTimeoutId = setTimeout(() => {
|
|
94
|
+
if (this.pusherClient.connection.state !== "connected") {
|
|
95
|
+
this._logConnectionError(`Pusher connection timed out after ${CONNECTION_TIMEOUT / 1000} seconds.`);
|
|
96
|
+
}
|
|
97
|
+
}, CONNECTION_TIMEOUT);
|
|
98
|
+
this.formatChannelName = formatChannelName ?? ((ns) => `private-${ns}`);
|
|
99
|
+
this.namespaceResolver = namespaceResolver ?? ((modelName) => modelName);
|
|
74
100
|
this.channels = new Map();
|
|
75
101
|
this.eventHandlers = new Set();
|
|
76
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* @private
|
|
105
|
+
* @param {string} reason
|
|
106
|
+
*/
|
|
107
|
+
_logConnectionError(reason) {
|
|
108
|
+
console.error(`%c
|
|
109
|
+
████████████████████████████████████████████████████████████████
|
|
110
|
+
█ █
|
|
111
|
+
█ PUSHER CONNECTION FAILED for backend: "${this.configKey}" █
|
|
112
|
+
█ █
|
|
113
|
+
████████████████████████████████████████████████████████████████
|
|
114
|
+
%c
|
|
115
|
+
Reason: ${reason}
|
|
116
|
+
|
|
117
|
+
CRITICAL: Real-time updates from the server will NOT be received.
|
|
118
|
+
This application will not reflect remote changes propagated via Pusher.
|
|
119
|
+
|
|
120
|
+
Common causes:
|
|
121
|
+
1. Incorrect 'appKey' or 'cluster' in the configuration.
|
|
122
|
+
2. The 'authEndpoint' is unreachable or returning an error (check network tab).
|
|
123
|
+
3. Network connectivity issues (firewall, offline).
|
|
124
|
+
4. Using an 'app_id' instead of the 'appKey'.`, "background-color: red; color: white; font-weight: bold; font-size: 16px; padding: 10px;", "color: red; font-size: 12px;");
|
|
125
|
+
}
|
|
77
126
|
/**
|
|
78
127
|
* Set the namespace resolver function.
|
|
79
128
|
* @param {NamespaceResolver} resolver
|
|
@@ -92,27 +141,29 @@ export class PusherEventReceiver {
|
|
|
92
141
|
subscribe(namespace) {
|
|
93
142
|
if (this.channels.has(namespace))
|
|
94
143
|
return;
|
|
95
|
-
const channelName = namespace.startsWith(
|
|
144
|
+
const channelName = namespace.startsWith("private-")
|
|
96
145
|
? namespace
|
|
97
146
|
: this.formatChannelName(namespace);
|
|
98
147
|
console.log(`Subscribing to channel: ${channelName} for backend: ${this.configKey}`);
|
|
99
148
|
const channel = this.pusherClient.subscribe(channelName);
|
|
100
|
-
channel.bind(
|
|
149
|
+
channel.bind("pusher:subscription_succeeded", () => {
|
|
101
150
|
console.log(`Subscription succeeded for channel: ${channelName}`);
|
|
102
151
|
});
|
|
103
|
-
channel.bind(
|
|
152
|
+
channel.bind("pusher:subscription_error", (status) => {
|
|
104
153
|
console.error(`Subscription error for channel: ${channelName}. Status:`, status);
|
|
154
|
+
if (status.status === 401 || status.status === 403) {
|
|
155
|
+
console.error(`%cAuthentication failed for channel ${channelName}. Check your authEndpoint and server-side permissions.`, "color: orange; font-weight: bold;");
|
|
156
|
+
}
|
|
105
157
|
});
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
channel.bind(eventType, data => {
|
|
158
|
+
Object.values(EventType).forEach((eventType) => {
|
|
159
|
+
channel.bind(eventType, (data) => {
|
|
109
160
|
const event = {
|
|
110
161
|
...data,
|
|
111
162
|
type: data.event || eventType,
|
|
112
163
|
namespace,
|
|
113
|
-
configKey: this.configKey
|
|
164
|
+
configKey: this.configKey,
|
|
114
165
|
};
|
|
115
|
-
this.eventHandlers.forEach(handler => handler(event));
|
|
166
|
+
this.eventHandlers.forEach((handler) => handler(event));
|
|
116
167
|
});
|
|
117
168
|
});
|
|
118
169
|
this.channels.set(namespace, channel);
|
|
@@ -121,10 +172,10 @@ export class PusherEventReceiver {
|
|
|
121
172
|
const channel = this.channels.get(namespace);
|
|
122
173
|
if (!channel)
|
|
123
174
|
return;
|
|
124
|
-
Object.values(EventType).forEach(eventType => {
|
|
175
|
+
Object.values(EventType).forEach((eventType) => {
|
|
125
176
|
channel.unbind(eventType);
|
|
126
177
|
});
|
|
127
|
-
const channelName = namespace.startsWith(
|
|
178
|
+
const channelName = namespace.startsWith("private-")
|
|
128
179
|
? namespace
|
|
129
180
|
: this.formatChannelName(namespace);
|
|
130
181
|
this.pusherClient.unsubscribe(channelName);
|
|
@@ -134,7 +185,11 @@ export class PusherEventReceiver {
|
|
|
134
185
|
* Disconnect from Pusher.
|
|
135
186
|
*/
|
|
136
187
|
disconnect() {
|
|
137
|
-
|
|
188
|
+
if (this.connectionTimeoutId) {
|
|
189
|
+
clearTimeout(this.connectionTimeoutId);
|
|
190
|
+
this.connectionTimeoutId = null;
|
|
191
|
+
}
|
|
192
|
+
[...this.channels.keys()].forEach((ns) => this.unsubscribe(ns));
|
|
138
193
|
this.pusherClient.disconnect();
|
|
139
194
|
}
|
|
140
195
|
/**
|
|
@@ -187,7 +242,7 @@ export function setEventReceiver(configKey, receiver) {
|
|
|
187
242
|
* @param {string} configKey - The backend configuration key
|
|
188
243
|
* @returns {EventReceiver|null}
|
|
189
244
|
*/
|
|
190
|
-
export function getEventReceiver(configKey =
|
|
245
|
+
export function getEventReceiver(configKey = "default") {
|
|
191
246
|
return eventReceivers.get(configKey);
|
|
192
247
|
}
|
|
193
248
|
/**
|
|
@@ -23,7 +23,7 @@ export class LiveMetric {
|
|
|
23
23
|
*/
|
|
24
24
|
refreshFromDb() {
|
|
25
25
|
const store = metricRegistry.getStore(this.metricType, this.queryset, this.field);
|
|
26
|
-
return store.sync();
|
|
26
|
+
return store.sync(true);
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Getter that always returns the current value from the store
|
|
@@ -95,7 +95,7 @@ export class LiveQueryset {
|
|
|
95
95
|
*/
|
|
96
96
|
refreshFromDb() {
|
|
97
97
|
const store = querysetStoreRegistry.getStore(__classPrivateFieldGet(this, _LiveQueryset_queryset, "f"));
|
|
98
|
-
return store.sync();
|
|
98
|
+
return store.sync(true);
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
101
101
|
* Get the current items from the store
|
|
@@ -7,7 +7,6 @@ export class QuerysetStore {
|
|
|
7
7
|
groundTruthPks: never[];
|
|
8
8
|
isSyncing: boolean;
|
|
9
9
|
lastSync: number | null;
|
|
10
|
-
needsSync: boolean;
|
|
11
10
|
isTemp: any;
|
|
12
11
|
pruneThreshold: any;
|
|
13
12
|
getRootStore: any;
|
|
@@ -46,6 +45,6 @@ export class QuerysetStore {
|
|
|
46
45
|
renderFromRoot(optimistic: boolean | undefined, rootStore: any): any[];
|
|
47
46
|
renderFromData(optimistic?: boolean): any[];
|
|
48
47
|
applyOperation(operation: any, currentPks: any): any;
|
|
49
|
-
sync(): Promise<void>;
|
|
48
|
+
sync(forceFromDb?: boolean): Promise<void>;
|
|
50
49
|
}
|
|
51
50
|
import { Cache } from '../cache/cache.js';
|
|
@@ -15,7 +15,6 @@ export class QuerysetStore {
|
|
|
15
15
|
this.queryset = queryset;
|
|
16
16
|
this.isSyncing = false;
|
|
17
17
|
this.lastSync = null;
|
|
18
|
-
this.needsSync = false;
|
|
19
18
|
this.isTemp = options.isTemp || false;
|
|
20
19
|
this.pruneThreshold = options.pruneThreshold || 10;
|
|
21
20
|
this.getRootStore = options.getRootStore || null;
|
|
@@ -124,6 +123,7 @@ export class QuerysetStore {
|
|
|
124
123
|
}
|
|
125
124
|
async setGroundTruth(groundTruthPks) {
|
|
126
125
|
this.groundTruthPks = Array.isArray(groundTruthPks) ? groundTruthPks : [];
|
|
126
|
+
this.lastSync = Date.now();
|
|
127
127
|
this._emitRenderEvent();
|
|
128
128
|
}
|
|
129
129
|
async setOperations(operations) {
|
|
@@ -202,7 +202,7 @@ export class QuerysetStore {
|
|
|
202
202
|
typeof this.getRootStore === "function" &&
|
|
203
203
|
!this.isTemp) {
|
|
204
204
|
const { isRoot, rootStore } = this.getRootStore(this.queryset);
|
|
205
|
-
if (!isRoot && rootStore) {
|
|
205
|
+
if (!isRoot && rootStore && (rootStore.lastSync || 0) >= (this.lastSync || 0)) {
|
|
206
206
|
pks = this.renderFromRoot(optimistic, rootStore);
|
|
207
207
|
}
|
|
208
208
|
}
|
|
@@ -263,22 +263,21 @@ export class QuerysetStore {
|
|
|
263
263
|
}
|
|
264
264
|
return currentPks;
|
|
265
265
|
}
|
|
266
|
-
async sync() {
|
|
266
|
+
async sync(forceFromDb = false) {
|
|
267
267
|
const id = this.modelClass.modelName;
|
|
268
268
|
if (this.isSyncing) {
|
|
269
269
|
console.warn(`[QuerysetStore ${id}] Already syncing, request ignored.`);
|
|
270
270
|
return;
|
|
271
271
|
}
|
|
272
272
|
// Check if we're delegating to a root store
|
|
273
|
-
if (
|
|
273
|
+
if (!forceFromDb &&
|
|
274
|
+
this.getRootStore &&
|
|
274
275
|
typeof this.getRootStore === "function" &&
|
|
275
276
|
!this.isTemp) {
|
|
276
277
|
const { isRoot, rootStore } = this.getRootStore(this.queryset);
|
|
277
278
|
if (!isRoot && rootStore) {
|
|
278
279
|
// We're delegating to a root store - don't sync, just mark as needing sync
|
|
279
|
-
console.log(`[${id}] Delegating to root store
|
|
280
|
-
this.needsSync = true;
|
|
281
|
-
this.lastSync = null; // Clear last sync since we're not actually syncing
|
|
280
|
+
console.log(`[${id}] Delegating to root store.`);
|
|
282
281
|
this.setOperations(this.getInflightOperations());
|
|
283
282
|
return;
|
|
284
283
|
}
|
|
@@ -301,12 +300,10 @@ export class QuerysetStore {
|
|
|
301
300
|
this.setGroundTruth(data);
|
|
302
301
|
this.setOperations(this.getInflightOperations());
|
|
303
302
|
this.lastSync = Date.now();
|
|
304
|
-
this.needsSync = false;
|
|
305
303
|
console.log(`[${id}] Sync completed.`);
|
|
306
304
|
}
|
|
307
305
|
catch (e) {
|
|
308
306
|
console.error(`[${id}] Failed to sync ground truth:`, e);
|
|
309
|
-
this.needsSync = true; // Mark as needing sync on error
|
|
310
307
|
}
|
|
311
308
|
finally {
|
|
312
309
|
this.isSyncing = false;
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -205,18 +205,6 @@ npx @statezero/core sync
|
|
|
205
205
|
|
|
206
206
|
**🆚 Traditional REST APIs:** Write 90% less boilerplate. Focus on features, not data plumbing.
|
|
207
207
|
|
|
208
|
-
## Pricing
|
|
209
|
-
|
|
210
|
-
StateZero uses a no-rugpull license model:
|
|
211
|
-
|
|
212
|
-
- **$0/month** for companies with revenue up to $3M
|
|
213
|
-
- **$75/month** for companies with revenue up to $7.5M
|
|
214
|
-
- **$200/month** for companies with revenue up to $20M
|
|
215
|
-
- **$500/month** for companies with revenue up to $100M
|
|
216
|
-
- **$1,000/month** for companies with revenue above $100M
|
|
217
|
-
|
|
218
|
-
Lock in your rate forever by signing up early. We can't change your fee or cancel your license.
|
|
219
|
-
|
|
220
208
|
## Get Started
|
|
221
209
|
|
|
222
210
|
Run `pip install statezero` and `npm install @statezero/core` to begin.
|