@objectstack/client 4.0.2 → 4.0.4
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +15 -0
- package/dist/index.d.mts +84 -3
- package/dist/index.d.ts +84 -3
- package/dist/index.js +162 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +161 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -12
- package/src/client.hono.test.ts +11 -3
- package/src/index.ts +58 -28
- package/src/realtime-api.ts +208 -0
- package/tests/integration/01-discovery.test.ts +5 -5
package/dist/index.mjs
CHANGED
|
@@ -2,6 +2,135 @@
|
|
|
2
2
|
import { isFilterAST } from "@objectstack/spec/data";
|
|
3
3
|
import { createLogger } from "@objectstack/core";
|
|
4
4
|
|
|
5
|
+
// src/realtime-api.ts
|
|
6
|
+
var RealtimeAPI = class {
|
|
7
|
+
constructor(baseUrl, token) {
|
|
8
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
9
|
+
this.eventBuffer = [];
|
|
10
|
+
this._baseUrl = baseUrl;
|
|
11
|
+
this._token = token;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Subscribe to metadata events
|
|
15
|
+
* Returns an unsubscribe function
|
|
16
|
+
*/
|
|
17
|
+
subscribeMetadata(type, callback, options) {
|
|
18
|
+
const subscriptionId = `metadata-${type}-${Date.now()}`;
|
|
19
|
+
this.subscriptions.set(subscriptionId, {
|
|
20
|
+
filter: {
|
|
21
|
+
type,
|
|
22
|
+
packageId: options?.packageId,
|
|
23
|
+
eventTypes: [
|
|
24
|
+
`metadata.${type}.created`,
|
|
25
|
+
`metadata.${type}.updated`,
|
|
26
|
+
`metadata.${type}.deleted`
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
handler: (event) => {
|
|
30
|
+
if (event.type.startsWith("metadata.")) {
|
|
31
|
+
callback(event);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
this.startPolling();
|
|
36
|
+
return () => {
|
|
37
|
+
this.subscriptions.delete(subscriptionId);
|
|
38
|
+
if (this.subscriptions.size === 0) {
|
|
39
|
+
this.stopPolling();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Subscribe to data record events
|
|
45
|
+
* Returns an unsubscribe function
|
|
46
|
+
*/
|
|
47
|
+
subscribeData(object, callback, options) {
|
|
48
|
+
const subscriptionId = `data-${object}-${Date.now()}`;
|
|
49
|
+
this.subscriptions.set(subscriptionId, {
|
|
50
|
+
filter: {
|
|
51
|
+
type: object,
|
|
52
|
+
recordId: options?.recordId,
|
|
53
|
+
eventTypes: [
|
|
54
|
+
"data.record.created",
|
|
55
|
+
"data.record.updated",
|
|
56
|
+
"data.record.deleted"
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
handler: (event) => {
|
|
60
|
+
if (event.type.startsWith("data.") && event.object === object) {
|
|
61
|
+
if (!options?.recordId || event.payload?.recordId === options.recordId) {
|
|
62
|
+
callback(event);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
this.startPolling();
|
|
68
|
+
return () => {
|
|
69
|
+
this.subscriptions.delete(subscriptionId);
|
|
70
|
+
if (this.subscriptions.size === 0) {
|
|
71
|
+
this.stopPolling();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Emit an event to all matching subscriptions (client-side only)
|
|
77
|
+
* This is used for in-process event delivery
|
|
78
|
+
*/
|
|
79
|
+
emitEvent(event) {
|
|
80
|
+
for (const sub of this.subscriptions.values()) {
|
|
81
|
+
const matchesType = !sub.filter.type || event.type.includes(sub.filter.type) || event.object === sub.filter.type;
|
|
82
|
+
const matchesEventType = !sub.filter.eventTypes?.length || sub.filter.eventTypes.includes(event.type);
|
|
83
|
+
const matchesPackage = !sub.filter.packageId || event.payload?.packageId === sub.filter.packageId;
|
|
84
|
+
if (matchesType && matchesEventType && matchesPackage) {
|
|
85
|
+
try {
|
|
86
|
+
sub.handler(event);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error("Error in realtime event handler:", error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Start polling for events (fallback mechanism)
|
|
95
|
+
* In production, this would be replaced with WebSocket/SSE
|
|
96
|
+
*/
|
|
97
|
+
startPolling() {
|
|
98
|
+
if (this.pollInterval) return;
|
|
99
|
+
this.pollInterval = setInterval(() => {
|
|
100
|
+
while (this.eventBuffer.length > 0) {
|
|
101
|
+
const event = this.eventBuffer.shift();
|
|
102
|
+
if (event) {
|
|
103
|
+
this.emitEvent(event);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}, 2e3);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Stop polling for events
|
|
110
|
+
*/
|
|
111
|
+
stopPolling() {
|
|
112
|
+
if (this.pollInterval) {
|
|
113
|
+
clearInterval(this.pollInterval);
|
|
114
|
+
this.pollInterval = void 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Internal method to buffer events from server
|
|
119
|
+
* This would be called by WebSocket/SSE handlers in production
|
|
120
|
+
*/
|
|
121
|
+
_bufferEvent(event) {
|
|
122
|
+
this.eventBuffer.push(event);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Disconnect and clean up all subscriptions
|
|
126
|
+
*/
|
|
127
|
+
disconnect() {
|
|
128
|
+
this.stopPolling();
|
|
129
|
+
this.subscriptions.clear();
|
|
130
|
+
this.eventBuffer = [];
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
5
134
|
// src/query-builder.ts
|
|
6
135
|
var FilterBuilder = class {
|
|
7
136
|
constructor() {
|
|
@@ -506,6 +635,15 @@ var ObjectStackClient = class {
|
|
|
506
635
|
* Authentication Services
|
|
507
636
|
*/
|
|
508
637
|
this.auth = {
|
|
638
|
+
/**
|
|
639
|
+
* Get authentication configuration
|
|
640
|
+
* Returns available auth providers and features
|
|
641
|
+
*/
|
|
642
|
+
getConfig: async () => {
|
|
643
|
+
const route = this.getRoute("auth");
|
|
644
|
+
const res = await this.fetch(`${this.baseUrl}${route}/config`);
|
|
645
|
+
return this.unwrapResponse(res);
|
|
646
|
+
},
|
|
509
647
|
/**
|
|
510
648
|
* Login with email and password
|
|
511
649
|
* Uses better-auth endpoint: POST /sign-in/email
|
|
@@ -1472,6 +1610,7 @@ var ObjectStackClient = class {
|
|
|
1472
1610
|
level: config.debug ? "debug" : "info",
|
|
1473
1611
|
format: "pretty"
|
|
1474
1612
|
});
|
|
1613
|
+
this.realtimeAPI = new RealtimeAPI(this.baseUrl, this.token);
|
|
1475
1614
|
this.logger.debug("ObjectStack client created", { baseUrl: this.baseUrl });
|
|
1476
1615
|
}
|
|
1477
1616
|
/**
|
|
@@ -1482,6 +1621,18 @@ var ObjectStackClient = class {
|
|
|
1482
1621
|
try {
|
|
1483
1622
|
let data;
|
|
1484
1623
|
try {
|
|
1624
|
+
const discoveryUrl = `${this.baseUrl}/api/v1/discovery`;
|
|
1625
|
+
this.logger.debug("Probing protocol-standard discovery endpoint", { url: discoveryUrl });
|
|
1626
|
+
const res = await this.fetchImpl(discoveryUrl);
|
|
1627
|
+
if (res.ok) {
|
|
1628
|
+
const body = await res.json();
|
|
1629
|
+
data = body.data || body;
|
|
1630
|
+
this.logger.debug("Discovered via /api/v1/discovery");
|
|
1631
|
+
}
|
|
1632
|
+
} catch (e) {
|
|
1633
|
+
this.logger.debug("Protocol-standard discovery probe failed", { error: e.message });
|
|
1634
|
+
}
|
|
1635
|
+
if (!data) {
|
|
1485
1636
|
let wellKnownUrl;
|
|
1486
1637
|
try {
|
|
1487
1638
|
const url = new URL(this.baseUrl);
|
|
@@ -1489,22 +1640,10 @@ var ObjectStackClient = class {
|
|
|
1489
1640
|
} catch {
|
|
1490
1641
|
wellKnownUrl = "/.well-known/objectstack";
|
|
1491
1642
|
}
|
|
1492
|
-
this.logger.debug("
|
|
1643
|
+
this.logger.debug("Falling back to .well-known discovery", { url: wellKnownUrl });
|
|
1493
1644
|
const res = await this.fetchImpl(wellKnownUrl);
|
|
1494
|
-
if (res.ok) {
|
|
1495
|
-
const body = await res.json();
|
|
1496
|
-
data = body.data || body;
|
|
1497
|
-
this.logger.debug("Discovered via .well-known");
|
|
1498
|
-
}
|
|
1499
|
-
} catch (e) {
|
|
1500
|
-
this.logger.debug("Standard discovery probe failed", { error: e.message });
|
|
1501
|
-
}
|
|
1502
|
-
if (!data) {
|
|
1503
|
-
const fallbackUrl = `${this.baseUrl}/api/v1/discovery`;
|
|
1504
|
-
this.logger.debug("Falling back to standard discovery endpoint", { url: fallbackUrl });
|
|
1505
|
-
const res = await this.fetchImpl(fallbackUrl);
|
|
1506
1645
|
if (!res.ok) {
|
|
1507
|
-
throw new Error(`Failed to connect to ${
|
|
1646
|
+
throw new Error(`Failed to connect to ${wellKnownUrl}: ${res.statusText}`);
|
|
1508
1647
|
}
|
|
1509
1648
|
const body = await res.json();
|
|
1510
1649
|
data = body.data || body;
|
|
@@ -1542,6 +1681,13 @@ var ObjectStackClient = class {
|
|
|
1542
1681
|
}
|
|
1543
1682
|
return result;
|
|
1544
1683
|
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Event Subscription API
|
|
1686
|
+
* Provides real-time event subscriptions for metadata and data changes
|
|
1687
|
+
*/
|
|
1688
|
+
get events() {
|
|
1689
|
+
return this.realtimeAPI;
|
|
1690
|
+
}
|
|
1545
1691
|
/**
|
|
1546
1692
|
* Private Helpers
|
|
1547
1693
|
*/
|
|
@@ -1645,6 +1791,7 @@ export {
|
|
|
1645
1791
|
FilterBuilder,
|
|
1646
1792
|
ObjectStackClient,
|
|
1647
1793
|
QueryBuilder,
|
|
1794
|
+
RealtimeAPI,
|
|
1648
1795
|
createFilter,
|
|
1649
1796
|
createQuery
|
|
1650
1797
|
};
|