@arcote.tech/arc 0.3.5 → 0.3.6

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.
@@ -4,6 +4,7 @@
4
4
  * Extends Wire to provide command-specific functionality:
5
5
  * - Execute commands remotely on the server
6
6
  * - Handle command request/response serialization
7
+ * - Automatic FormData handling for file uploads
7
8
  */
8
9
  import { Wire } from "./wire";
9
10
  export declare class CommandWire extends Wire {
@@ -16,6 +17,15 @@ export declare class CommandWire extends Wire {
16
17
  * @returns Command result from server
17
18
  */
18
19
  executeCommand(name: string, params: any): Promise<any>;
20
+ /**
21
+ * Check if object contains any File instances (recursively)
22
+ */
23
+ private containsFile;
24
+ /**
25
+ * Convert params object to FormData for file uploads
26
+ * Files are appended directly, other values are JSON-stringified
27
+ */
28
+ private toFormData;
19
29
  /**
20
30
  * Register a command handler on the server side
21
31
  * Called during init() for server environment
@@ -23,6 +23,7 @@ export interface ReceivedEvent {
23
23
  type EventWireState = "disconnected" | "connecting" | "connected";
24
24
  export declare class EventWire {
25
25
  private readonly baseUrl;
26
+ private instanceId;
26
27
  private ws;
27
28
  private state;
28
29
  private token;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Tree module
3
+ *
4
+ * Provides hierarchical data structures with multiple node types
5
+ */
6
+ export * from "./tree";
7
+ export * from "./tree-context";
8
+ export * from "./tree-data";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,98 @@
1
+ import type { ArcObjectAny } from "../../elements/object";
2
+ import type { $type } from "../../utils/types/get-type";
3
+ import type { ArcTreeData } from "./tree-data";
4
+ /**
5
+ * Tree node as returned from queries
6
+ * Includes node data plus tree metadata
7
+ */
8
+ export interface TreeNode<NodeTypes extends Record<string, ArcObjectAny> = Record<string, ArcObjectAny>, T extends keyof NodeTypes = keyof NodeTypes> {
9
+ _id: string;
10
+ type: T;
11
+ parentId: string | null;
12
+ order: number;
13
+ treeId: string;
14
+ data: T extends keyof NodeTypes ? $type<NodeTypes[T]> : never;
15
+ }
16
+ /**
17
+ * Tree edge record
18
+ */
19
+ export interface TreeEdge {
20
+ _id: string;
21
+ childId: string;
22
+ parentId: string | null;
23
+ childType: string;
24
+ order: number;
25
+ treeId: string;
26
+ }
27
+ /**
28
+ * Options for tree find queries
29
+ */
30
+ export interface TreeFindOptions<NodeTypes extends Record<string, ArcObjectAny>> {
31
+ /** Filter by specific node types */
32
+ types?: (keyof NodeTypes)[];
33
+ /** Filter by parent ID (null for root nodes) */
34
+ parentId?: string | null;
35
+ /** Filter by tree ID (for multi-tenant) */
36
+ treeId?: string;
37
+ /** Sort order */
38
+ orderBy?: {
39
+ order: "asc" | "desc";
40
+ };
41
+ /** Limit results */
42
+ limit?: number;
43
+ /** Recursive depth (for findDescendants) */
44
+ depth?: number;
45
+ }
46
+ /**
47
+ * Query context for reading tree data
48
+ */
49
+ export interface TreeQueryContext<Data extends ArcTreeData> {
50
+ /** Find nodes with optional filtering */
51
+ find(options?: TreeFindOptions<Data["nodeTypes"]>): Promise<TreeNode<Data["nodeTypes"]>[]>;
52
+ /** Find single node by ID */
53
+ findOne(id: string): Promise<TreeNode<Data["nodeTypes"]> | undefined>;
54
+ /** Find direct children of a node */
55
+ findChildren(parentId: string | null, options?: {
56
+ types?: (keyof Data["nodeTypes"])[];
57
+ treeId?: string;
58
+ }): Promise<TreeNode<Data["nodeTypes"]>[]>;
59
+ /** Find ancestors (path to root) */
60
+ findAncestors(nodeId: string): Promise<TreeNode<Data["nodeTypes"]>[]>;
61
+ /** Find descendants recursively */
62
+ findDescendants(nodeId: string, depth?: number): Promise<TreeNode<Data["nodeTypes"]>[]>;
63
+ }
64
+ /**
65
+ * Mutation context for modifying tree data
66
+ */
67
+ export interface TreeMutationContext<Data extends ArcTreeData> {
68
+ /** Create a new node */
69
+ create<T extends keyof Data["nodeTypes"]>(type: T, data: $type<Data["nodeTypes"][T]>, options?: {
70
+ parentId?: string | null;
71
+ order?: number;
72
+ treeId?: string;
73
+ }): Promise<string>;
74
+ /** Update node data */
75
+ update<T extends keyof Data["nodeTypes"]>(id: string, data: Partial<$type<Data["nodeTypes"][T]>>): Promise<void>;
76
+ /** Delete node (with cascade) */
77
+ delete(id: string): Promise<void>;
78
+ /** Move node to new parent/position */
79
+ move(id: string, options: {
80
+ parentId?: string | null;
81
+ order?: number;
82
+ }): Promise<void>;
83
+ }
84
+ /**
85
+ * Serialized tree format for export/import
86
+ */
87
+ export interface SerializedTree<Data extends ArcTreeData> {
88
+ name: string;
89
+ treeId: string;
90
+ nodes: {
91
+ [K in keyof Data["nodeTypes"]]?: Array<{
92
+ _id: string;
93
+ data: $type<Data["nodeTypes"][K]>;
94
+ }>;
95
+ };
96
+ edges: TreeEdge[];
97
+ }
98
+ //# sourceMappingURL=tree-context.d.ts.map
@@ -0,0 +1,43 @@
1
+ import type { ArcObjectAny } from "../../elements/object";
2
+ import type { ArcTokenAny } from "../../token/token";
3
+ /**
4
+ * Protection function for tree access control
5
+ * Returns read/write conditions based on token params
6
+ */
7
+ export type TreeProtectionFn<T extends ArcTokenAny> = (params: T extends {
8
+ params: infer P;
9
+ } ? P : never) => {
10
+ read?: Record<string, any> | false;
11
+ write?: Record<string, any> | false;
12
+ } | false;
13
+ /**
14
+ * Tree protection configuration
15
+ */
16
+ export interface TreeProtection<T extends ArcTokenAny = ArcTokenAny> {
17
+ token: T;
18
+ protectionFn: TreeProtectionFn<T>;
19
+ }
20
+ /**
21
+ * Node type definition within a tree
22
+ */
23
+ export interface TreeNodeTypeDefinition<Name extends string = string, Schema extends ArcObjectAny = ArcObjectAny> {
24
+ name: Name;
25
+ schema: Schema;
26
+ }
27
+ /**
28
+ * Data structure for ArcTree configuration
29
+ * Follows Arc's factory pattern with immutable data
30
+ */
31
+ export interface ArcTreeData {
32
+ /** Unique tree name */
33
+ name: string;
34
+ /** Optional description for documentation */
35
+ description?: string;
36
+ /** Map of node type names to their schemas */
37
+ nodeTypes: Record<string, ArcObjectAny>;
38
+ /** Token-based protections */
39
+ protections: TreeProtection[];
40
+ /** Tree ID field name for multi-tenant support */
41
+ treeIdField?: string;
42
+ }
43
+ //# sourceMappingURL=tree-data.d.ts.map
@@ -0,0 +1,142 @@
1
+ import type { DatabaseStoreSchema } from "../../data-storage/database-store";
2
+ import { ArcObject, type ArcObjectAny, type ArcRawShape } from "../../elements/object";
3
+ import type { ModelAdapters } from "../../model/model-adapters";
4
+ import type { ArcTokenAny } from "../../token/token";
5
+ import type { Merge } from "../../utils";
6
+ import { ArcContextElement } from "../context-element";
7
+ import type { SerializedTree, TreeMutationContext, TreeQueryContext } from "./tree-context";
8
+ import type { ArcTreeData, TreeProtectionFn } from "./tree-data";
9
+ /**
10
+ * Arc Tree - Hierarchical data structure with multiple node types
11
+ *
12
+ * Trees enable defining hierarchical data where each node type has its own
13
+ * dedicated view/table. The tree provides a unified query interface that
14
+ * joins these views based on parent-child relationships.
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const contentTree = tree("contentTree")
19
+ * .nodeType("texts", { text: string() })
20
+ * .nodeType("brollIdeas", { idea: string() })
21
+ * .nodeType("graphicIdeas", { idea: string() });
22
+ * ```
23
+ */
24
+ export declare class ArcTree<const Data extends ArcTreeData> extends ArcContextElement<Data["name"]> {
25
+ private readonly data;
26
+ constructor(data: Data);
27
+ /**
28
+ * Get tree name
29
+ */
30
+ get treeName(): Data["name"];
31
+ /**
32
+ * Get node types configuration
33
+ */
34
+ get nodeTypes(): Data["nodeTypes"];
35
+ /**
36
+ * Set tree description for documentation
37
+ */
38
+ description<const Desc extends string>(description: Desc): ArcTree<Merge<Data, {
39
+ description: Desc;
40
+ }>>;
41
+ /**
42
+ * Define a node type with its schema
43
+ * Each node type gets its own dedicated view/table
44
+ *
45
+ * @param typeName - Unique name for this node type within the tree
46
+ * @param schema - Schema defining the node's data fields
47
+ */
48
+ nodeType<TypeName extends string, Schema extends ArcRawShape | ArcObjectAny>(typeName: TypeName, schema: Schema): ArcTree<Merge<Data, {
49
+ nodeTypes: Data["nodeTypes"] & { [K in TypeName]: ArcObject<ArcRawShape, [{
50
+ name: "type";
51
+ validator: (value: any) => false | {
52
+ current: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
53
+ expected: "object";
54
+ };
55
+ }, {
56
+ name: "schema";
57
+ validator: (value: any) => false | {
58
+ [x: string]: unknown;
59
+ };
60
+ }]> | (Schema & ArcObject<any, any>); };
61
+ }>>;
62
+ /**
63
+ * Add token-based protection to this tree
64
+ * Defines read/write access conditions based on token params
65
+ */
66
+ protectBy<T extends ArcTokenAny>(token: T, protectionFn: TreeProtectionFn<T>): ArcTree<Data>;
67
+ /**
68
+ * Check if tree has protections
69
+ */
70
+ get hasProtections(): boolean;
71
+ /**
72
+ * Get all protection configurations
73
+ */
74
+ get protections(): import("./tree-data").TreeProtection<ArcTokenAny>[];
75
+ /**
76
+ * Generate all context elements (events, views) for this tree
77
+ * These should be registered in the Arc context
78
+ */
79
+ getElements(): ArcContextElement<any>[];
80
+ /**
81
+ * Generate query context for this tree
82
+ * Provides read access to tree data
83
+ */
84
+ queryContext(adapters: ModelAdapters): TreeQueryContext<Data>;
85
+ /**
86
+ * Generate mutation context for this tree
87
+ * Provides write access to tree data
88
+ */
89
+ mutateContext(adapters: ModelAdapters): TreeMutationContext<Data>;
90
+ /**
91
+ * Generate database store schema for this tree
92
+ * Returns schemas for all node type views + edges view
93
+ */
94
+ databaseStoreSchema(): DatabaseStoreSchema;
95
+ /**
96
+ * Serialize tree data to JSON format
97
+ * Exports nodes grouped by type and edges array
98
+ *
99
+ * @param adapters - Model adapters for data access
100
+ * @param treeId - Tree ID to serialize (defaults to "default")
101
+ */
102
+ serialize(adapters: ModelAdapters, treeId?: string): Promise<SerializedTree<Data>>;
103
+ /**
104
+ * Serialize a subtree starting from a specific node
105
+ * Includes only the specified node and its descendants
106
+ *
107
+ * @param adapters - Model adapters for data access
108
+ * @param nodeId - Root node ID of the subtree
109
+ */
110
+ serializeSubtree(adapters: ModelAdapters, nodeId: string): Promise<SerializedTree<Data>>;
111
+ /**
112
+ * Deserialize tree data from JSON format
113
+ * Validates node data against schemas and reconstructs relationships
114
+ *
115
+ * @param adapters - Model adapters for data access
116
+ * @param serialized - Serialized tree data
117
+ */
118
+ deserialize(adapters: ModelAdapters, serialized: SerializedTree<Data>): Promise<void>;
119
+ }
120
+ /**
121
+ * Create a new tree with the given name
122
+ *
123
+ * @param name - Unique tree name
124
+ * @returns New tree instance ready for configuration
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * const contentTree = tree("contentTree")
129
+ * .nodeType("texts", { text: string() })
130
+ * .nodeType("brollIdeas", { idea: string() });
131
+ * ```
132
+ */
133
+ export declare function tree<const Name extends string>(name: Name): ArcTree<{
134
+ name: Name;
135
+ nodeTypes: {};
136
+ protections: [];
137
+ }>;
138
+ /**
139
+ * Type alias for any tree (used in collections)
140
+ */
141
+ export type ArcTreeAny = ArcTree<any>;
142
+ //# sourceMappingURL=tree.d.ts.map
package/dist/index.js CHANGED
@@ -84,9 +84,19 @@ class CommandWire extends Wire {
84
84
  super(baseUrl);
85
85
  }
86
86
  async executeCommand(name, params) {
87
+ const hasFile = this.containsFile(params);
88
+ let body;
89
+ let headers;
90
+ if (hasFile) {
91
+ body = this.toFormData(params);
92
+ } else {
93
+ body = JSON.stringify(params);
94
+ headers = { "Content-Type": "application/json" };
95
+ }
87
96
  const response = await this.fetch(`/command/${name}`, {
88
97
  method: "POST",
89
- body: JSON.stringify(params)
98
+ body,
99
+ headers
90
100
  });
91
101
  if (!response.ok) {
92
102
  const errorText = await response.text();
@@ -94,10 +104,35 @@ class CommandWire extends Wire {
94
104
  }
95
105
  return await response.json();
96
106
  }
107
+ containsFile(obj) {
108
+ if (obj instanceof File || obj instanceof Blob)
109
+ return true;
110
+ if (obj === null || typeof obj !== "object")
111
+ return false;
112
+ if (Array.isArray(obj))
113
+ return obj.some((v) => this.containsFile(v));
114
+ return Object.values(obj).some((v) => this.containsFile(v));
115
+ }
116
+ toFormData(params) {
117
+ const formData = new FormData;
118
+ for (const [key, value] of Object.entries(params)) {
119
+ if (value instanceof File) {
120
+ formData.append(key, value, value.name);
121
+ } else if (value instanceof Blob) {
122
+ formData.append(key, value);
123
+ } else {
124
+ formData.append(key, JSON.stringify(value));
125
+ }
126
+ }
127
+ return formData;
128
+ }
97
129
  }
98
130
  // src/adapters/event-wire.ts
131
+ var eventWireInstanceCounter = 0;
132
+
99
133
  class EventWire {
100
134
  baseUrl;
135
+ instanceId;
101
136
  ws = null;
102
137
  state = "disconnected";
103
138
  token = null;
@@ -109,6 +144,7 @@ class EventWire {
109
144
  syncRequested = false;
110
145
  constructor(baseUrl) {
111
146
  this.baseUrl = baseUrl;
147
+ this.instanceId = ++eventWireInstanceCounter;
112
148
  }
113
149
  setAuthToken(token) {
114
150
  this.token = token;
@@ -127,16 +163,26 @@ class EventWire {
127
163
  if (this.state !== "disconnected")
128
164
  return;
129
165
  this.state = "connecting";
130
- const wsUrl = this.baseUrl.replace(/^http/, "ws");
166
+ let wsUrl;
167
+ if (this.baseUrl.startsWith("http://") || this.baseUrl.startsWith("https://")) {
168
+ wsUrl = this.baseUrl.replace(/^http/, "ws");
169
+ } else {
170
+ const protocol = typeof window !== "undefined" && window.location.protocol === "https:" ? "wss:" : "ws:";
171
+ const host = typeof window !== "undefined" ? window.location.host : "localhost";
172
+ wsUrl = `${protocol}//${host}${this.baseUrl}`;
173
+ }
131
174
  const tokenParam = this.token ? `?token=${this.token}` : "";
132
175
  const url = `${wsUrl}/ws${tokenParam}`;
133
176
  try {
134
177
  this.ws = new WebSocket(url);
135
178
  this.ws.onopen = () => {
136
- this.state = "connected";
137
- console.log(`EventWire connected`);
138
- this.requestSync();
139
- this.flushPendingEvents();
179
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
180
+ this.state = "connected";
181
+ this.requestSync();
182
+ this.flushPendingEvents();
183
+ } else {
184
+ console.log(`[EventWire] onopen called but ws is not OPEN, readyState:`, this.ws?.readyState);
185
+ }
140
186
  };
141
187
  this.ws.onmessage = (event) => {
142
188
  try {
@@ -146,7 +192,7 @@ class EventWire {
146
192
  console.error("EventWire: Failed to parse message", err);
147
193
  }
148
194
  };
149
- this.ws.onclose = () => {
195
+ this.ws.onclose = (event) => {
150
196
  this.state = "disconnected";
151
197
  this.ws = null;
152
198
  console.log("EventWire disconnected");
@@ -154,9 +200,14 @@ class EventWire {
154
200
  };
155
201
  this.ws.onerror = (err) => {
156
202
  console.error("EventWire error:", err);
203
+ if (this.state === "connecting") {
204
+ this.state = "disconnected";
205
+ this.ws = null;
206
+ }
157
207
  };
158
208
  } catch (err) {
159
209
  this.state = "disconnected";
210
+ this.ws = null;
160
211
  console.error("EventWire: Failed to connect", err);
161
212
  this.scheduleReconnect();
162
213
  }
@@ -174,19 +225,28 @@ class EventWire {
174
225
  this.syncRequested = false;
175
226
  }
176
227
  syncEvents(events) {
177
- if (this.state !== "connected" || !this.ws) {
228
+ const isActuallyConnected = this.ws && this.ws.readyState === WebSocket.OPEN;
229
+ if (!isActuallyConnected) {
230
+ if (this.state === "connected" && !this.ws) {
231
+ this.state = "disconnected";
232
+ }
178
233
  this.pendingEvents.push(...events);
234
+ if (this.state === "disconnected") {
235
+ this.connect();
236
+ }
179
237
  return;
180
238
  }
181
- this.ws.send(JSON.stringify({
182
- type: "sync-events",
183
- events: events.map((e) => ({
184
- localId: e.localId,
185
- type: e.type,
186
- payload: e.payload,
187
- createdAt: e.createdAt
188
- }))
189
- }));
239
+ if (this.ws) {
240
+ this.ws.send(JSON.stringify({
241
+ type: "sync-events",
242
+ events: events.map((e) => ({
243
+ localId: e.localId,
244
+ type: e.type,
245
+ payload: e.payload,
246
+ createdAt: e.createdAt
247
+ }))
248
+ }));
249
+ }
190
250
  }
191
251
  onEvent(callback) {
192
252
  this.onEventCallback = callback;
@@ -0,0 +1,4 @@
1
+ export { ArcTelemetry, type ObservabilityMode, type TelemetryConfig, } from "./telemetry";
2
+ export { initBrowserTelemetry } from "./telemetry-browser";
3
+ export { initServerTelemetry } from "./telemetry-server";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ import { ArcTelemetry, type TelemetryConfig } from "./telemetry";
2
+ /**
3
+ * Initialize OpenTelemetry for browser environment
4
+ */
5
+ export declare function initBrowserTelemetry(config: TelemetryConfig): ArcTelemetry;
6
+ //# sourceMappingURL=telemetry-browser.d.ts.map
@@ -0,0 +1,6 @@
1
+ import { ArcTelemetry, type TelemetryConfig } from "./telemetry";
2
+ /**
3
+ * Initialize OpenTelemetry for Node.js/Bun server environment
4
+ */
5
+ export declare function initServerTelemetry(config: TelemetryConfig): ArcTelemetry;
6
+ //# sourceMappingURL=telemetry-server.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { type Span } from "@opentelemetry/api";
2
+ export type ObservabilityMode = "development" | "production" | "disabled";
3
+ export interface TelemetryConfig {
4
+ serviceName: string;
5
+ environment: "client" | "server";
6
+ endpoint?: string;
7
+ enabled?: boolean;
8
+ sampleRate?: number;
9
+ mode?: ObservabilityMode;
10
+ includePayloads?: boolean;
11
+ debug?: boolean;
12
+ }
13
+ /**
14
+ * Arc Telemetry - Wrapper around OpenTelemetry for Arc Framework
15
+ *
16
+ * Provides simplified API for tracing commands, events, and views
17
+ */
18
+ export declare class ArcTelemetry {
19
+ private tracer;
20
+ readonly config: TelemetryConfig;
21
+ constructor(config: TelemetryConfig);
22
+ /**
23
+ * Set the tracer instance (called by init functions)
24
+ */
25
+ setTracer(tracer: any): void;
26
+ /**
27
+ * Start a span with automatic error handling
28
+ */
29
+ startSpan<T>(name: string, fn: (span: Span) => Promise<T>, attributes?: Record<string, any>): Promise<T>;
30
+ /**
31
+ * Get current active span
32
+ */
33
+ getCurrentSpan(): Span | undefined;
34
+ /**
35
+ * Add attributes to current span
36
+ */
37
+ addAttributes(attributes: Record<string, any>): void;
38
+ /**
39
+ * Create a child span (must be manually ended)
40
+ */
41
+ createChildSpan(name: string, attributes?: Record<string, any>): Span | undefined;
42
+ /**
43
+ * Serialize value for span attributes (handles objects, arrays, etc)
44
+ */
45
+ private serializeValue;
46
+ /**
47
+ * Check if payloads should be included based on mode
48
+ */
49
+ shouldIncludePayloads(): boolean;
50
+ }
51
+ //# sourceMappingURL=telemetry.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc",
3
3
  "type": "module",
4
- "version": "0.3.5",
4
+ "version": "0.3.6",
5
5
  "private": false,
6
6
  "author": "Przemysław Krasiński [arcote.tech]",
7
7
  "description": "Arc framework core rewrite with improved event emission and type safety",