@arcote.tech/arc-host 0.7.14 → 0.7.15
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/index.js +131 -157
- package/dist/index.js.map +5 -5
- package/dist/src/create-server.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/middleware/http.d.ts +0 -5
- package/dist/src/middleware/http.d.ts.map +1 -1
- package/dist/src/middleware/index.d.ts +2 -2
- package/dist/src/middleware/index.d.ts.map +1 -1
- package/dist/src/middleware/view-subscription.integration.test.d.ts +2 -0
- package/dist/src/middleware/view-subscription.integration.test.d.ts.map +1 -0
- package/dist/src/middleware/ws.d.ts +30 -1
- package/dist/src/middleware/ws.d.ts.map +1 -1
- package/dist/src/middleware/ws.test.d.ts +2 -0
- package/dist/src/middleware/ws.test.d.ts.map +1 -0
- package/dist/src/types.d.ts +29 -13
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/create-server.ts +8 -2
- package/src/index.ts +2 -3
- package/src/middleware/http.ts +0 -119
- package/src/middleware/index.ts +2 -3
- package/src/middleware/view-subscription.integration.test.ts +307 -0
- package/src/middleware/ws.test.ts +95 -0
- package/src/middleware/ws.ts +203 -77
- package/src/types.ts +25 -9
package/dist/index.js
CHANGED
|
@@ -4677,88 +4677,6 @@ function queryHandler(ch) {
|
|
|
4677
4677
|
}
|
|
4678
4678
|
};
|
|
4679
4679
|
}
|
|
4680
|
-
var streamIdCounter = 0;
|
|
4681
|
-
var streamConnections = new Map;
|
|
4682
|
-
function streamHandler(ch) {
|
|
4683
|
-
return (_req, url, ctx) => {
|
|
4684
|
-
if (!url.pathname.startsWith("/stream/") || _req.method !== "GET")
|
|
4685
|
-
return null;
|
|
4686
|
-
const viewName = url.pathname.split("/stream/")[1];
|
|
4687
|
-
if (!viewName) {
|
|
4688
|
-
return new Response("Invalid stream path", {
|
|
4689
|
-
status: 400,
|
|
4690
|
-
headers: ctx.corsHeaders
|
|
4691
|
-
});
|
|
4692
|
-
}
|
|
4693
|
-
const findOptions = {};
|
|
4694
|
-
const whereParam = url.searchParams.get("where");
|
|
4695
|
-
if (whereParam) {
|
|
4696
|
-
try {
|
|
4697
|
-
findOptions.where = JSON.parse(whereParam);
|
|
4698
|
-
} catch {
|
|
4699
|
-
return new Response("Invalid 'where' parameter", {
|
|
4700
|
-
status: 400,
|
|
4701
|
-
headers: ctx.corsHeaders
|
|
4702
|
-
});
|
|
4703
|
-
}
|
|
4704
|
-
}
|
|
4705
|
-
const orderByParam = url.searchParams.get("orderBy");
|
|
4706
|
-
if (orderByParam) {
|
|
4707
|
-
try {
|
|
4708
|
-
findOptions.orderBy = JSON.parse(orderByParam);
|
|
4709
|
-
} catch {
|
|
4710
|
-
return new Response("Invalid 'orderBy' parameter", {
|
|
4711
|
-
status: 400,
|
|
4712
|
-
headers: ctx.corsHeaders
|
|
4713
|
-
});
|
|
4714
|
-
}
|
|
4715
|
-
}
|
|
4716
|
-
const limitParam = url.searchParams.get("limit");
|
|
4717
|
-
if (limitParam)
|
|
4718
|
-
findOptions.limit = parseInt(limitParam, 10);
|
|
4719
|
-
const streamId = `stream_${++streamIdCounter}_${Date.now()}`;
|
|
4720
|
-
const rawToken = ctx.rawToken;
|
|
4721
|
-
const stream = new ReadableStream({
|
|
4722
|
-
start(controller) {
|
|
4723
|
-
const scoped = new ScopedModel2(ch.getModel(), "stream");
|
|
4724
|
-
if (rawToken)
|
|
4725
|
-
scoped.setToken(rawToken);
|
|
4726
|
-
const descriptor = { element: viewName, method: "find", args: [findOptions] };
|
|
4727
|
-
const sendData = async () => {
|
|
4728
|
-
try {
|
|
4729
|
-
const data = await scoped.callQuery(descriptor);
|
|
4730
|
-
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: "data", data })}
|
|
4731
|
-
|
|
4732
|
-
`));
|
|
4733
|
-
} catch {
|
|
4734
|
-
unsubscribe();
|
|
4735
|
-
}
|
|
4736
|
-
};
|
|
4737
|
-
sendData();
|
|
4738
|
-
const unsubscribe = ch.getEventPublisher().subscribe("*", () => sendData());
|
|
4739
|
-
streamConnections.set(streamId, { id: streamId, controller, unsubscribe });
|
|
4740
|
-
controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: "connected", streamId })}
|
|
4741
|
-
|
|
4742
|
-
`));
|
|
4743
|
-
},
|
|
4744
|
-
cancel() {
|
|
4745
|
-
const conn = streamConnections.get(streamId);
|
|
4746
|
-
if (conn) {
|
|
4747
|
-
conn.unsubscribe();
|
|
4748
|
-
streamConnections.delete(streamId);
|
|
4749
|
-
}
|
|
4750
|
-
}
|
|
4751
|
-
});
|
|
4752
|
-
return new Response(stream, {
|
|
4753
|
-
headers: {
|
|
4754
|
-
...ctx.corsHeaders,
|
|
4755
|
-
"Content-Type": "text/event-stream",
|
|
4756
|
-
"Cache-Control": "no-cache",
|
|
4757
|
-
Connection: "keep-alive"
|
|
4758
|
-
}
|
|
4759
|
-
});
|
|
4760
|
-
};
|
|
4761
|
-
}
|
|
4762
4680
|
function eventSyncHandler(ch) {
|
|
4763
4681
|
return async (req, url, ctx) => {
|
|
4764
4682
|
if (url.pathname !== "/sync/events" || req.method !== "POST")
|
|
@@ -4859,25 +4777,71 @@ function arcHttpHandlers(ch, cm) {
|
|
|
4859
4777
|
healthHandler(cm),
|
|
4860
4778
|
commandHandler(ch),
|
|
4861
4779
|
queryHandler(ch),
|
|
4862
|
-
streamHandler(ch),
|
|
4863
4780
|
eventSyncHandler(ch),
|
|
4864
4781
|
routeHandler(ch)
|
|
4865
4782
|
];
|
|
4866
4783
|
}
|
|
4867
|
-
function cleanupStreams() {
|
|
4868
|
-
for (const conn of streamConnections.values())
|
|
4869
|
-
conn.unsubscribe();
|
|
4870
|
-
streamConnections.clear();
|
|
4871
|
-
}
|
|
4872
4784
|
// src/middleware/ws.ts
|
|
4873
|
-
import {
|
|
4874
|
-
|
|
4785
|
+
import {
|
|
4786
|
+
ScopedModel as ScopedModel3,
|
|
4787
|
+
ScopedStore,
|
|
4788
|
+
checkItemMatchesWhere
|
|
4789
|
+
} from "@arcote.tech/arc";
|
|
4790
|
+
var viewSubscribers = new Map;
|
|
4875
4791
|
function cleanupClientSubs(clientId) {
|
|
4876
|
-
const
|
|
4877
|
-
|
|
4878
|
-
for (const
|
|
4879
|
-
|
|
4880
|
-
|
|
4792
|
+
const prefix = `${clientId}:`;
|
|
4793
|
+
for (const subs of viewSubscribers.values()) {
|
|
4794
|
+
for (const key of subs.keys()) {
|
|
4795
|
+
if (key.startsWith(prefix))
|
|
4796
|
+
subs.delete(key);
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
function filterChangeForSubscriber(change, restrictions) {
|
|
4801
|
+
const newMatches = change.newRow !== null && (restrictions === null || checkItemMatchesWhere(change.newRow, restrictions));
|
|
4802
|
+
if (newMatches) {
|
|
4803
|
+
return { type: "set", id: change.id, item: change.newRow };
|
|
4804
|
+
}
|
|
4805
|
+
const oldMatches = change.oldRow !== null && (restrictions === null || checkItemMatchesWhere(change.oldRow, restrictions));
|
|
4806
|
+
if (oldMatches) {
|
|
4807
|
+
return { type: "delete", id: change.id, item: null };
|
|
4808
|
+
}
|
|
4809
|
+
return null;
|
|
4810
|
+
}
|
|
4811
|
+
function broadcastViewChanges(committed, connectionManager) {
|
|
4812
|
+
const byStore = new Map;
|
|
4813
|
+
for (const change of committed) {
|
|
4814
|
+
let list = byStore.get(change.store);
|
|
4815
|
+
if (!list) {
|
|
4816
|
+
list = [];
|
|
4817
|
+
byStore.set(change.store, list);
|
|
4818
|
+
}
|
|
4819
|
+
list.push(change);
|
|
4820
|
+
}
|
|
4821
|
+
for (const [viewName, changes] of byStore) {
|
|
4822
|
+
const subs = viewSubscribers.get(viewName);
|
|
4823
|
+
if (!subs || subs.size === 0)
|
|
4824
|
+
continue;
|
|
4825
|
+
for (const sub of subs.values()) {
|
|
4826
|
+
const filtered = [];
|
|
4827
|
+
for (const change of changes) {
|
|
4828
|
+
const wireChange = filterChangeForSubscriber(change, sub.restrictions);
|
|
4829
|
+
if (wireChange)
|
|
4830
|
+
filtered.push(wireChange);
|
|
4831
|
+
}
|
|
4832
|
+
if (filtered.length === 0)
|
|
4833
|
+
continue;
|
|
4834
|
+
if (sub.buffer) {
|
|
4835
|
+
sub.buffer.push(...filtered);
|
|
4836
|
+
continue;
|
|
4837
|
+
}
|
|
4838
|
+
connectionManager.sendToClient(sub.clientId, {
|
|
4839
|
+
type: "view-changes",
|
|
4840
|
+
element: viewName,
|
|
4841
|
+
scope: sub.scope,
|
|
4842
|
+
changes: filtered
|
|
4843
|
+
});
|
|
4844
|
+
}
|
|
4881
4845
|
}
|
|
4882
4846
|
}
|
|
4883
4847
|
function scopeAuthHandler() {
|
|
@@ -4903,6 +4867,8 @@ function syncEventsHandler() {
|
|
|
4903
4867
|
for (const c of ctx.connectionManager.getAllClients()) {
|
|
4904
4868
|
if (c.id === client.id)
|
|
4905
4869
|
continue;
|
|
4870
|
+
if (!c.wantsEventSync)
|
|
4871
|
+
continue;
|
|
4906
4872
|
const clientTokens = ctx.connectionManager.getAllScopeTokens(c.id);
|
|
4907
4873
|
const authorized = filterEventsForTokens(clientTokens, persisted, ctx.contextHandler.getEventDefinitions());
|
|
4908
4874
|
if (authorized.length > 0) {
|
|
@@ -4925,6 +4891,7 @@ function requestSyncHandler() {
|
|
|
4925
4891
|
return async (client, message, ctx) => {
|
|
4926
4892
|
if (message.type !== "request-sync")
|
|
4927
4893
|
return false;
|
|
4894
|
+
client.wantsEventSync = true;
|
|
4928
4895
|
const allTokens = ctx.connectionManager.getAllScopeTokens(client.id);
|
|
4929
4896
|
const token = allTokens.length > 0 ? allTokens[0] : null;
|
|
4930
4897
|
const events = await ctx.contextHandler.getEventsSince(message.lastHostEventId, token);
|
|
@@ -4973,74 +4940,82 @@ function executeCommandHandler() {
|
|
|
4973
4940
|
return true;
|
|
4974
4941
|
};
|
|
4975
4942
|
}
|
|
4976
|
-
function
|
|
4943
|
+
function viewSubscriptionHandler() {
|
|
4977
4944
|
return async (client, message, ctx) => {
|
|
4978
|
-
if (message.type === "subscribe-
|
|
4979
|
-
const {
|
|
4980
|
-
const
|
|
4945
|
+
if (message.type === "subscribe-view") {
|
|
4946
|
+
const { element: elementName } = message;
|
|
4947
|
+
const scope = message.scope ?? "default";
|
|
4948
|
+
const scopeToken = client.scopeTokens.get(scope) ?? null;
|
|
4981
4949
|
let rawToken = scopeToken?.raw ?? null;
|
|
4982
4950
|
if (!rawToken && client.scopeTokens.size > 0) {
|
|
4983
4951
|
rawToken = client.scopeTokens.values().next().value.raw;
|
|
4984
4952
|
}
|
|
4985
|
-
const scoped = new ScopedModel3(ctx.contextHandler.getModel(), scope
|
|
4953
|
+
const scoped = new ScopedModel3(ctx.contextHandler.getModel(), scope);
|
|
4986
4954
|
if (rawToken)
|
|
4987
4955
|
scoped.setToken(rawToken);
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
lastResultJson = json;
|
|
4996
|
-
ctx.connectionManager.sendToClient(client.id, {
|
|
4997
|
-
type: "query-data",
|
|
4998
|
-
subscriptionId,
|
|
4999
|
-
data: data ?? null
|
|
5000
|
-
});
|
|
5001
|
-
} catch (err) {
|
|
5002
|
-
console.error(`[Arc] Query subscription error:`, err);
|
|
5003
|
-
}
|
|
5004
|
-
};
|
|
5005
|
-
sendData();
|
|
5006
|
-
const element = ctx.contextHandler.getModel().context.get(descriptor.element);
|
|
5007
|
-
const handlers = element?.getHandlers?.();
|
|
5008
|
-
const eventTypes = handlers ? Object.keys(handlers) : [];
|
|
5009
|
-
let debounceTimer;
|
|
5010
|
-
const debouncedSend = () => {
|
|
5011
|
-
if (debounceTimer)
|
|
5012
|
-
clearTimeout(debounceTimer);
|
|
5013
|
-
debounceTimer = setTimeout(() => sendData(), 50);
|
|
5014
|
-
};
|
|
5015
|
-
const unsubscribes = [];
|
|
5016
|
-
if (eventTypes.length > 0) {
|
|
5017
|
-
for (const eventType of eventTypes) {
|
|
5018
|
-
unsubscribes.push(ctx.contextHandler.getEventPublisher().subscribe(eventType, debouncedSend));
|
|
5019
|
-
}
|
|
5020
|
-
} else {
|
|
5021
|
-
unsubscribes.push(ctx.contextHandler.getEventPublisher().subscribe("*", debouncedSend));
|
|
4956
|
+
const element = ctx.contextHandler.getModel().context.get(elementName);
|
|
4957
|
+
if (!element || typeof element.getRestrictionsFor !== "function") {
|
|
4958
|
+
ctx.connectionManager.sendToClient(client.id, {
|
|
4959
|
+
type: "error",
|
|
4960
|
+
message: `Cannot subscribe to view "${elementName}": unknown element`
|
|
4961
|
+
});
|
|
4962
|
+
return true;
|
|
5022
4963
|
}
|
|
5023
|
-
const
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
4964
|
+
const { restrictions, denied } = element.getRestrictionsFor(scoped.getAdapters());
|
|
4965
|
+
const subKey = `${client.id}:${scope}`;
|
|
4966
|
+
if (denied) {
|
|
4967
|
+
viewSubscribers.get(elementName)?.delete(subKey);
|
|
4968
|
+
ctx.connectionManager.sendToClient(client.id, {
|
|
4969
|
+
type: "view-snapshot",
|
|
4970
|
+
element: elementName,
|
|
4971
|
+
scope,
|
|
4972
|
+
items: []
|
|
4973
|
+
});
|
|
4974
|
+
return true;
|
|
4975
|
+
}
|
|
4976
|
+
const subscriber = {
|
|
4977
|
+
clientId: client.id,
|
|
4978
|
+
scope,
|
|
4979
|
+
restrictions,
|
|
4980
|
+
buffer: []
|
|
5028
4981
|
};
|
|
5029
|
-
if (!
|
|
5030
|
-
|
|
4982
|
+
if (!viewSubscribers.has(elementName)) {
|
|
4983
|
+
viewSubscribers.set(elementName, new Map);
|
|
4984
|
+
}
|
|
4985
|
+
viewSubscribers.get(elementName).set(subKey, subscriber);
|
|
4986
|
+
try {
|
|
4987
|
+
const store = ctx.contextHandler.getDataStorage().getStore(elementName);
|
|
4988
|
+
const items = restrictions ? await new ScopedStore(store, restrictions, false).find({}) : await store.find({});
|
|
4989
|
+
ctx.connectionManager.sendToClient(client.id, {
|
|
4990
|
+
type: "view-snapshot",
|
|
4991
|
+
element: elementName,
|
|
4992
|
+
scope,
|
|
4993
|
+
items
|
|
4994
|
+
});
|
|
4995
|
+
} catch (err) {
|
|
4996
|
+
viewSubscribers.get(elementName)?.delete(subKey);
|
|
4997
|
+
console.error(`[Arc] View subscription error (${elementName}):`, err);
|
|
4998
|
+
ctx.connectionManager.sendToClient(client.id, {
|
|
4999
|
+
type: "error",
|
|
5000
|
+
message: `Failed to subscribe to view "${elementName}"`
|
|
5001
|
+
});
|
|
5002
|
+
return true;
|
|
5003
|
+
}
|
|
5004
|
+
const buffered = subscriber.buffer;
|
|
5005
|
+
subscriber.buffer = null;
|
|
5006
|
+
if (buffered.length > 0) {
|
|
5007
|
+
ctx.connectionManager.sendToClient(client.id, {
|
|
5008
|
+
type: "view-changes",
|
|
5009
|
+
element: elementName,
|
|
5010
|
+
scope,
|
|
5011
|
+
changes: buffered
|
|
5012
|
+
});
|
|
5031
5013
|
}
|
|
5032
|
-
clientViewSubs.get(client.id).set(subscriptionId, cleanup);
|
|
5033
5014
|
return true;
|
|
5034
5015
|
}
|
|
5035
|
-
if (message.type === "unsubscribe-
|
|
5036
|
-
const
|
|
5037
|
-
|
|
5038
|
-
const unsub = subs.get(message.subscriptionId);
|
|
5039
|
-
if (unsub) {
|
|
5040
|
-
unsub();
|
|
5041
|
-
subs.delete(message.subscriptionId);
|
|
5042
|
-
}
|
|
5043
|
-
}
|
|
5016
|
+
if (message.type === "unsubscribe-view") {
|
|
5017
|
+
const scope = message.scope ?? "default";
|
|
5018
|
+
viewSubscribers.get(message.element)?.delete(`${client.id}:${scope}`);
|
|
5044
5019
|
return true;
|
|
5045
5020
|
}
|
|
5046
5021
|
return false;
|
|
@@ -5052,7 +5027,7 @@ function arcWsHandlers() {
|
|
|
5052
5027
|
syncEventsHandler(),
|
|
5053
5028
|
requestSyncHandler(),
|
|
5054
5029
|
executeCommandHandler(),
|
|
5055
|
-
|
|
5030
|
+
viewSubscriptionHandler()
|
|
5056
5031
|
];
|
|
5057
5032
|
}
|
|
5058
5033
|
// src/create-server.ts
|
|
@@ -5065,6 +5040,7 @@ async function createArcServer(config) {
|
|
|
5065
5040
|
const cronScheduler = new CronScheduler(contextHandler);
|
|
5066
5041
|
cronScheduler.start();
|
|
5067
5042
|
const connectionManager = new ConnectionManager;
|
|
5043
|
+
contextHandler.getEventPublisher().onViewChanges((changes) => broadcastViewChanges(changes, connectionManager));
|
|
5068
5044
|
function buildCorsHeaders(req) {
|
|
5069
5045
|
const origin = req?.headers.get("Origin") || "*";
|
|
5070
5046
|
return {
|
|
@@ -5235,7 +5211,6 @@ async function createArcServer(config) {
|
|
|
5235
5211
|
cronScheduler,
|
|
5236
5212
|
stop: () => {
|
|
5237
5213
|
cronScheduler.stop();
|
|
5238
|
-
cleanupStreams();
|
|
5239
5214
|
server.stop();
|
|
5240
5215
|
}
|
|
5241
5216
|
};
|
|
@@ -5248,12 +5223,11 @@ function hostLiveModel(context, dbAdapterFactory) {
|
|
|
5248
5223
|
});
|
|
5249
5224
|
}
|
|
5250
5225
|
export {
|
|
5226
|
+
viewSubscriptionHandler,
|
|
5251
5227
|
syncEventsHandler,
|
|
5252
|
-
streamHandler,
|
|
5253
5228
|
scopeAuthHandler,
|
|
5254
5229
|
routeHandler,
|
|
5255
5230
|
requestSyncHandler,
|
|
5256
|
-
querySubscriptionHandler,
|
|
5257
5231
|
queryHandler,
|
|
5258
5232
|
hostLiveModel,
|
|
5259
5233
|
healthHandler,
|
|
@@ -5263,10 +5237,10 @@ export {
|
|
|
5263
5237
|
eventSyncHandler,
|
|
5264
5238
|
createArcServer,
|
|
5265
5239
|
commandHandler,
|
|
5266
|
-
cleanupStreams,
|
|
5267
5240
|
cleanupClientSubs,
|
|
5268
5241
|
canTokenReceiveEvent,
|
|
5269
5242
|
canTokenEmitEvent,
|
|
5243
|
+
broadcastViewChanges,
|
|
5270
5244
|
arcWsHandlers,
|
|
5271
5245
|
arcHttpHandlers,
|
|
5272
5246
|
CronScheduler,
|
|
@@ -5274,5 +5248,5 @@ export {
|
|
|
5274
5248
|
ConnectionManager
|
|
5275
5249
|
};
|
|
5276
5250
|
|
|
5277
|
-
//# debugId=
|
|
5251
|
+
//# debugId=094B6B350E52DB8A64756E2164756E21
|
|
5278
5252
|
//# sourceMappingURL=index.js.map
|