@arcote.tech/arc-cli 0.7.13 → 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 CHANGED
@@ -14720,7 +14720,7 @@ class DataStorage {
14720
14720
  }
14721
14721
  }
14722
14722
 
14723
- class ScopedStore {
14723
+ class ScopedStore2 {
14724
14724
  #inner;
14725
14725
  #restrictions;
14726
14726
  #canWrite;
@@ -15082,7 +15082,7 @@ function resolveQueryChange(currentResult, event3, options) {
15082
15082
  }
15083
15083
  return false;
15084
15084
  }
15085
- const shouldBeInResult = checkItemMatchesWhere(event3.item, options.where);
15085
+ const shouldBeInResult = checkItemMatchesWhere2(event3.item, options.where);
15086
15086
  if (isInCurrentResult && shouldBeInResult) {
15087
15087
  const newResult = currentResult.toSpliced(index, 1, event3.item);
15088
15088
  return applyOrderByAndLimit(newResult, options);
@@ -15094,7 +15094,7 @@ function resolveQueryChange(currentResult, event3, options) {
15094
15094
  }
15095
15095
  return false;
15096
15096
  }
15097
- function checkItemMatchesWhere(item, where) {
15097
+ function checkItemMatchesWhere2(item, where) {
15098
15098
  if (!where) {
15099
15099
  return true;
15100
15100
  }
@@ -15683,7 +15683,7 @@ class StreamingStore {
15683
15683
  find(options = {}) {
15684
15684
  let results = Array.from(this.data.values());
15685
15685
  if (options.where) {
15686
- results = results.filter((item) => checkItemMatchesWhere(item, options.where));
15686
+ results = results.filter((item) => checkItemMatchesWhere2(item, options.where));
15687
15687
  }
15688
15688
  return applyOrderByAndLimit(results, options);
15689
15689
  }
@@ -18862,7 +18862,7 @@ var init_dist = __esm(() => {
18862
18862
  throw new Error(`Scope violation: access denied to store "${storeName}" (not declared in query/mutate)`);
18863
18863
  }
18864
18864
  const inner = this.#inner.getStore(storeName);
18865
- return new ScopedStore(inner, this.#restrictions, permission === "read-write");
18865
+ return new ScopedStore2(inner, this.#restrictions, permission === "read-write");
18866
18866
  }
18867
18867
  fork() {
18868
18868
  return this.#inner.fork();
@@ -22112,7 +22112,7 @@ class DataStorage2 {
22112
22112
  }
22113
22113
  }
22114
22114
 
22115
- class ScopedStore2 {
22115
+ class ScopedStore3 {
22116
22116
  #inner;
22117
22117
  #restrictions;
22118
22118
  #canWrite;
@@ -22474,7 +22474,7 @@ function resolveQueryChange2(currentResult, event3, options) {
22474
22474
  }
22475
22475
  return false;
22476
22476
  }
22477
- const shouldBeInResult = checkItemMatchesWhere2(event3.item, options.where);
22477
+ const shouldBeInResult = checkItemMatchesWhere3(event3.item, options.where);
22478
22478
  if (isInCurrentResult && shouldBeInResult) {
22479
22479
  const newResult = currentResult.toSpliced(index, 1, event3.item);
22480
22480
  return applyOrderByAndLimit2(newResult, options);
@@ -22486,7 +22486,7 @@ function resolveQueryChange2(currentResult, event3, options) {
22486
22486
  }
22487
22487
  return false;
22488
22488
  }
22489
- function checkItemMatchesWhere2(item, where) {
22489
+ function checkItemMatchesWhere3(item, where) {
22490
22490
  if (!where) {
22491
22491
  return true;
22492
22492
  }
@@ -23075,7 +23075,7 @@ class StreamingStore2 {
23075
23075
  find(options = {}) {
23076
23076
  let results = Array.from(this.data.values());
23077
23077
  if (options.where) {
23078
- results = results.filter((item) => checkItemMatchesWhere2(item, options.where));
23078
+ results = results.filter((item) => checkItemMatchesWhere3(item, options.where));
23079
23079
  }
23080
23080
  return applyOrderByAndLimit2(results, options);
23081
23081
  }
@@ -24689,7 +24689,7 @@ var init_dist2 = __esm(() => {
24689
24689
  throw new Error(`Scope violation: access denied to store "${storeName}" (not declared in query/mutate)`);
24690
24690
  }
24691
24691
  const inner = this.#inner.getStore(storeName);
24692
- return new ScopedStore2(inner, this.#restrictions, permission === "read-write");
24692
+ return new ScopedStore3(inner, this.#restrictions, permission === "read-write");
24693
24693
  }
24694
24694
  fork() {
24695
24695
  return this.#inner.fork();
@@ -37858,8 +37858,8 @@ async function digQuery(server, type, name) {
37858
37858
  }
37859
37859
 
37860
37860
  // src/deploy/deploy-env.ts
37861
- var HEALTH_RETRIES = 10;
37862
- var HEALTH_DELAY_MS = 1000;
37861
+ var HEALTH_RETRIES = 30;
37862
+ var HEALTH_DELAY_MS = 2000;
37863
37863
  async function updateEnvDeployment(opts) {
37864
37864
  const { target, cfg, env: env2, fullRef } = opts;
37865
37865
  const upperEnv = env2.toUpperCase().replace(/-/g, "_");
@@ -39950,88 +39950,6 @@ function queryHandler(ch) {
39950
39950
  }
39951
39951
  };
39952
39952
  }
39953
- var streamIdCounter = 0;
39954
- var streamConnections = new Map;
39955
- function streamHandler(ch) {
39956
- return (_req, url, ctx) => {
39957
- if (!url.pathname.startsWith("/stream/") || _req.method !== "GET")
39958
- return null;
39959
- const viewName = url.pathname.split("/stream/")[1];
39960
- if (!viewName) {
39961
- return new Response("Invalid stream path", {
39962
- status: 400,
39963
- headers: ctx.corsHeaders
39964
- });
39965
- }
39966
- const findOptions = {};
39967
- const whereParam = url.searchParams.get("where");
39968
- if (whereParam) {
39969
- try {
39970
- findOptions.where = JSON.parse(whereParam);
39971
- } catch {
39972
- return new Response("Invalid 'where' parameter", {
39973
- status: 400,
39974
- headers: ctx.corsHeaders
39975
- });
39976
- }
39977
- }
39978
- const orderByParam = url.searchParams.get("orderBy");
39979
- if (orderByParam) {
39980
- try {
39981
- findOptions.orderBy = JSON.parse(orderByParam);
39982
- } catch {
39983
- return new Response("Invalid 'orderBy' parameter", {
39984
- status: 400,
39985
- headers: ctx.corsHeaders
39986
- });
39987
- }
39988
- }
39989
- const limitParam = url.searchParams.get("limit");
39990
- if (limitParam)
39991
- findOptions.limit = parseInt(limitParam, 10);
39992
- const streamId = `stream_${++streamIdCounter}_${Date.now()}`;
39993
- const rawToken = ctx.rawToken;
39994
- const stream2 = new ReadableStream({
39995
- start(controller) {
39996
- const scoped = new ScopedModel2(ch.getModel(), "stream");
39997
- if (rawToken)
39998
- scoped.setToken(rawToken);
39999
- const descriptor = { element: viewName, method: "find", args: [findOptions] };
40000
- const sendData = async () => {
40001
- try {
40002
- const data = await scoped.callQuery(descriptor);
40003
- controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: "data", data })}
40004
-
40005
- `));
40006
- } catch {
40007
- unsubscribe();
40008
- }
40009
- };
40010
- sendData();
40011
- const unsubscribe = ch.getEventPublisher().subscribe("*", () => sendData());
40012
- streamConnections.set(streamId, { id: streamId, controller, unsubscribe });
40013
- controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: "connected", streamId })}
40014
-
40015
- `));
40016
- },
40017
- cancel() {
40018
- const conn = streamConnections.get(streamId);
40019
- if (conn) {
40020
- conn.unsubscribe();
40021
- streamConnections.delete(streamId);
40022
- }
40023
- }
40024
- });
40025
- return new Response(stream2, {
40026
- headers: {
40027
- ...ctx.corsHeaders,
40028
- "Content-Type": "text/event-stream",
40029
- "Cache-Control": "no-cache",
40030
- Connection: "keep-alive"
40031
- }
40032
- });
40033
- };
40034
- }
40035
39953
  function eventSyncHandler(ch) {
40036
39954
  return async (req, url, ctx) => {
40037
39955
  if (url.pathname !== "/sync/events" || req.method !== "POST")
@@ -40132,25 +40050,71 @@ function arcHttpHandlers(ch, cm) {
40132
40050
  healthHandler(cm),
40133
40051
  commandHandler(ch),
40134
40052
  queryHandler(ch),
40135
- streamHandler(ch),
40136
40053
  eventSyncHandler(ch),
40137
40054
  routeHandler(ch)
40138
40055
  ];
40139
40056
  }
40140
- function cleanupStreams() {
40141
- for (const conn of streamConnections.values())
40142
- conn.unsubscribe();
40143
- streamConnections.clear();
40144
- }
40145
40057
  // ../host/src/middleware/ws.ts
40146
- import { ScopedModel as ScopedModel3 } from "@arcote.tech/arc";
40147
- var clientViewSubs = new Map;
40058
+ import {
40059
+ ScopedModel as ScopedModel3,
40060
+ ScopedStore,
40061
+ checkItemMatchesWhere
40062
+ } from "@arcote.tech/arc";
40063
+ var viewSubscribers = new Map;
40148
40064
  function cleanupClientSubs(clientId) {
40149
- const subs = clientViewSubs.get(clientId);
40150
- if (subs) {
40151
- for (const unsub of subs.values())
40152
- unsub();
40153
- clientViewSubs.delete(clientId);
40065
+ const prefix = `${clientId}:`;
40066
+ for (const subs of viewSubscribers.values()) {
40067
+ for (const key of subs.keys()) {
40068
+ if (key.startsWith(prefix))
40069
+ subs.delete(key);
40070
+ }
40071
+ }
40072
+ }
40073
+ function filterChangeForSubscriber(change, restrictions) {
40074
+ const newMatches = change.newRow !== null && (restrictions === null || checkItemMatchesWhere(change.newRow, restrictions));
40075
+ if (newMatches) {
40076
+ return { type: "set", id: change.id, item: change.newRow };
40077
+ }
40078
+ const oldMatches = change.oldRow !== null && (restrictions === null || checkItemMatchesWhere(change.oldRow, restrictions));
40079
+ if (oldMatches) {
40080
+ return { type: "delete", id: change.id, item: null };
40081
+ }
40082
+ return null;
40083
+ }
40084
+ function broadcastViewChanges(committed, connectionManager) {
40085
+ const byStore = new Map;
40086
+ for (const change of committed) {
40087
+ let list = byStore.get(change.store);
40088
+ if (!list) {
40089
+ list = [];
40090
+ byStore.set(change.store, list);
40091
+ }
40092
+ list.push(change);
40093
+ }
40094
+ for (const [viewName, changes] of byStore) {
40095
+ const subs = viewSubscribers.get(viewName);
40096
+ if (!subs || subs.size === 0)
40097
+ continue;
40098
+ for (const sub of subs.values()) {
40099
+ const filtered = [];
40100
+ for (const change of changes) {
40101
+ const wireChange = filterChangeForSubscriber(change, sub.restrictions);
40102
+ if (wireChange)
40103
+ filtered.push(wireChange);
40104
+ }
40105
+ if (filtered.length === 0)
40106
+ continue;
40107
+ if (sub.buffer) {
40108
+ sub.buffer.push(...filtered);
40109
+ continue;
40110
+ }
40111
+ connectionManager.sendToClient(sub.clientId, {
40112
+ type: "view-changes",
40113
+ element: viewName,
40114
+ scope: sub.scope,
40115
+ changes: filtered
40116
+ });
40117
+ }
40154
40118
  }
40155
40119
  }
40156
40120
  function scopeAuthHandler() {
@@ -40176,6 +40140,8 @@ function syncEventsHandler() {
40176
40140
  for (const c2 of ctx.connectionManager.getAllClients()) {
40177
40141
  if (c2.id === client.id)
40178
40142
  continue;
40143
+ if (!c2.wantsEventSync)
40144
+ continue;
40179
40145
  const clientTokens = ctx.connectionManager.getAllScopeTokens(c2.id);
40180
40146
  const authorized = filterEventsForTokens(clientTokens, persisted, ctx.contextHandler.getEventDefinitions());
40181
40147
  if (authorized.length > 0) {
@@ -40198,6 +40164,7 @@ function requestSyncHandler() {
40198
40164
  return async (client, message, ctx) => {
40199
40165
  if (message.type !== "request-sync")
40200
40166
  return false;
40167
+ client.wantsEventSync = true;
40201
40168
  const allTokens = ctx.connectionManager.getAllScopeTokens(client.id);
40202
40169
  const token = allTokens.length > 0 ? allTokens[0] : null;
40203
40170
  const events = await ctx.contextHandler.getEventsSince(message.lastHostEventId, token);
@@ -40246,74 +40213,82 @@ function executeCommandHandler() {
40246
40213
  return true;
40247
40214
  };
40248
40215
  }
40249
- function querySubscriptionHandler() {
40216
+ function viewSubscriptionHandler() {
40250
40217
  return async (client, message, ctx) => {
40251
- if (message.type === "subscribe-query") {
40252
- const { subscriptionId, descriptor, scope } = message;
40253
- const scopeToken = scope ? client.scopeTokens.get(scope) : null;
40218
+ if (message.type === "subscribe-view") {
40219
+ const { element: elementName } = message;
40220
+ const scope = message.scope ?? "default";
40221
+ const scopeToken = client.scopeTokens.get(scope) ?? null;
40254
40222
  let rawToken = scopeToken?.raw ?? null;
40255
40223
  if (!rawToken && client.scopeTokens.size > 0) {
40256
40224
  rawToken = client.scopeTokens.values().next().value.raw;
40257
40225
  }
40258
- const scoped = new ScopedModel3(ctx.contextHandler.getModel(), scope ?? "default");
40226
+ const scoped = new ScopedModel3(ctx.contextHandler.getModel(), scope);
40259
40227
  if (rawToken)
40260
40228
  scoped.setToken(rawToken);
40261
- let lastResultJson = "";
40262
- const sendData = async () => {
40263
- try {
40264
- const data = await scoped.callQuery(descriptor);
40265
- const json = JSON.stringify(data ?? null);
40266
- if (json === lastResultJson)
40267
- return;
40268
- lastResultJson = json;
40269
- ctx.connectionManager.sendToClient(client.id, {
40270
- type: "query-data",
40271
- subscriptionId,
40272
- data: data ?? null
40273
- });
40274
- } catch (err3) {
40275
- console.error(`[Arc] Query subscription error:`, err3);
40276
- }
40277
- };
40278
- sendData();
40279
- const element = ctx.contextHandler.getModel().context.get(descriptor.element);
40280
- const handlers = element?.getHandlers?.();
40281
- const eventTypes = handlers ? Object.keys(handlers) : [];
40282
- let debounceTimer;
40283
- const debouncedSend = () => {
40284
- if (debounceTimer)
40285
- clearTimeout(debounceTimer);
40286
- debounceTimer = setTimeout(() => sendData(), 50);
40287
- };
40288
- const unsubscribes = [];
40289
- if (eventTypes.length > 0) {
40290
- for (const eventType of eventTypes) {
40291
- unsubscribes.push(ctx.contextHandler.getEventPublisher().subscribe(eventType, debouncedSend));
40292
- }
40293
- } else {
40294
- unsubscribes.push(ctx.contextHandler.getEventPublisher().subscribe("*", debouncedSend));
40229
+ const element = ctx.contextHandler.getModel().context.get(elementName);
40230
+ if (!element || typeof element.getRestrictionsFor !== "function") {
40231
+ ctx.connectionManager.sendToClient(client.id, {
40232
+ type: "error",
40233
+ message: `Cannot subscribe to view "${elementName}": unknown element`
40234
+ });
40235
+ return true;
40295
40236
  }
40296
- const cleanup = () => {
40297
- if (debounceTimer)
40298
- clearTimeout(debounceTimer);
40299
- for (const unsub of unsubscribes)
40300
- unsub();
40237
+ const { restrictions, denied } = element.getRestrictionsFor(scoped.getAdapters());
40238
+ const subKey = `${client.id}:${scope}`;
40239
+ if (denied) {
40240
+ viewSubscribers.get(elementName)?.delete(subKey);
40241
+ ctx.connectionManager.sendToClient(client.id, {
40242
+ type: "view-snapshot",
40243
+ element: elementName,
40244
+ scope,
40245
+ items: []
40246
+ });
40247
+ return true;
40248
+ }
40249
+ const subscriber = {
40250
+ clientId: client.id,
40251
+ scope,
40252
+ restrictions,
40253
+ buffer: []
40301
40254
  };
40302
- if (!clientViewSubs.has(client.id)) {
40303
- clientViewSubs.set(client.id, new Map);
40255
+ if (!viewSubscribers.has(elementName)) {
40256
+ viewSubscribers.set(elementName, new Map);
40257
+ }
40258
+ viewSubscribers.get(elementName).set(subKey, subscriber);
40259
+ try {
40260
+ const store = ctx.contextHandler.getDataStorage().getStore(elementName);
40261
+ const items = restrictions ? await new ScopedStore(store, restrictions, false).find({}) : await store.find({});
40262
+ ctx.connectionManager.sendToClient(client.id, {
40263
+ type: "view-snapshot",
40264
+ element: elementName,
40265
+ scope,
40266
+ items
40267
+ });
40268
+ } catch (err3) {
40269
+ viewSubscribers.get(elementName)?.delete(subKey);
40270
+ console.error(`[Arc] View subscription error (${elementName}):`, err3);
40271
+ ctx.connectionManager.sendToClient(client.id, {
40272
+ type: "error",
40273
+ message: `Failed to subscribe to view "${elementName}"`
40274
+ });
40275
+ return true;
40276
+ }
40277
+ const buffered = subscriber.buffer;
40278
+ subscriber.buffer = null;
40279
+ if (buffered.length > 0) {
40280
+ ctx.connectionManager.sendToClient(client.id, {
40281
+ type: "view-changes",
40282
+ element: elementName,
40283
+ scope,
40284
+ changes: buffered
40285
+ });
40304
40286
  }
40305
- clientViewSubs.get(client.id).set(subscriptionId, cleanup);
40306
40287
  return true;
40307
40288
  }
40308
- if (message.type === "unsubscribe-query") {
40309
- const subs = clientViewSubs.get(client.id);
40310
- if (subs) {
40311
- const unsub = subs.get(message.subscriptionId);
40312
- if (unsub) {
40313
- unsub();
40314
- subs.delete(message.subscriptionId);
40315
- }
40316
- }
40289
+ if (message.type === "unsubscribe-view") {
40290
+ const scope = message.scope ?? "default";
40291
+ viewSubscribers.get(message.element)?.delete(`${client.id}:${scope}`);
40317
40292
  return true;
40318
40293
  }
40319
40294
  return false;
@@ -40325,7 +40300,7 @@ function arcWsHandlers() {
40325
40300
  syncEventsHandler(),
40326
40301
  requestSyncHandler(),
40327
40302
  executeCommandHandler(),
40328
- querySubscriptionHandler()
40303
+ viewSubscriptionHandler()
40329
40304
  ];
40330
40305
  }
40331
40306
  // ../host/src/create-server.ts
@@ -40338,6 +40313,7 @@ async function createArcServer(config) {
40338
40313
  const cronScheduler = new CronScheduler(contextHandler);
40339
40314
  cronScheduler.start();
40340
40315
  const connectionManager = new ConnectionManager;
40316
+ contextHandler.getEventPublisher().onViewChanges((changes) => broadcastViewChanges(changes, connectionManager));
40341
40317
  function buildCorsHeaders(req) {
40342
40318
  const origin = req?.headers.get("Origin") || "*";
40343
40319
  return {
@@ -40508,7 +40484,6 @@ async function createArcServer(config) {
40508
40484
  cronScheduler,
40509
40485
  stop: () => {
40510
40486
  cronScheduler.stop();
40511
- cleanupStreams();
40512
40487
  server.stop();
40513
40488
  }
40514
40489
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-cli",
3
- "version": "0.7.13",
3
+ "version": "0.7.15",
4
4
  "description": "CLI tool for Arc framework",
5
5
  "module": "index.ts",
6
6
  "main": "dist/index.js",
@@ -12,13 +12,13 @@
12
12
  "build": "bun build --target=bun ./src/index.ts --outdir=dist --external @arcote.tech/arc --external @arcote.tech/arc-ds --external @arcote.tech/arc-react --external @arcote.tech/platform --external '@opentelemetry/*' && chmod +x dist/index.js"
13
13
  },
14
14
  "dependencies": {
15
- "@arcote.tech/arc": "^0.7.13",
16
- "@arcote.tech/arc-ds": "^0.7.13",
17
- "@arcote.tech/arc-react": "^0.7.13",
18
- "@arcote.tech/arc-host": "^0.7.13",
19
- "@arcote.tech/arc-adapter-db-sqlite": "^0.7.13",
20
- "@arcote.tech/arc-adapter-db-postgres": "^0.7.13",
21
- "@arcote.tech/arc-otel": "^0.7.13",
15
+ "@arcote.tech/arc": "^0.7.15",
16
+ "@arcote.tech/arc-ds": "^0.7.15",
17
+ "@arcote.tech/arc-react": "^0.7.15",
18
+ "@arcote.tech/arc-host": "^0.7.15",
19
+ "@arcote.tech/arc-adapter-db-sqlite": "^0.7.15",
20
+ "@arcote.tech/arc-adapter-db-postgres": "^0.7.15",
21
+ "@arcote.tech/arc-otel": "^0.7.15",
22
22
  "@opentelemetry/api": "^1.9.0",
23
23
  "@opentelemetry/api-logs": "^0.57.0",
24
24
  "@opentelemetry/core": "^1.30.0",
@@ -31,7 +31,7 @@
31
31
  "@opentelemetry/sdk-trace-base": "^1.30.0",
32
32
  "@opentelemetry/sdk-trace-node": "^1.30.0",
33
33
  "@opentelemetry/semantic-conventions": "^1.27.0",
34
- "@arcote.tech/platform": "^0.7.13",
34
+ "@arcote.tech/platform": "^0.7.15",
35
35
  "@clack/prompts": "^0.9.0",
36
36
  "commander": "^11.1.0",
37
37
  "chokidar": "^3.5.3",
@@ -34,8 +34,13 @@ export interface UpdateEnvDeploymentResult {
34
34
  redeployed: boolean;
35
35
  }
36
36
 
37
- const HEALTH_RETRIES = 10;
38
- const HEALTH_DELAY_MS = 1000;
37
+ // Budżet health checku liczony pod ZIMNY START świeżego obrazu: ładowanie
38
+ // server context + dużego host.js + podłączenie telemetrii potrafi przekroczyć
39
+ // ~10-20s, przez co poprzednie 10×1s dawało false-negatywy ("health check did
40
+ // not pass within retries") mimo że kontener wstawał poprawnie (RestartCount=0).
41
+ // 30×2s ≈ 60s ściany (plus narzut SSH na próbę) — z zapasem na cold start.
42
+ const HEALTH_RETRIES = 30;
43
+ const HEALTH_DELAY_MS = 2000;
39
44
 
40
45
  export async function updateEnvDeployment(
41
46
  opts: UpdateEnvDeploymentOptions,
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  cleanupClientSubs,
3
- cleanupStreams,
4
3
  createArcServer,
5
4
  ContextHandler,
6
5
  ConnectionManager,