@sylphx/lens-server 1.11.3 → 2.0.1

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.
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @sylphx/lens-server - DataLoader
3
+ *
4
+ * Simple batching utility for N+1 prevention in field resolution.
5
+ * Batches multiple load calls within a single microtask.
6
+ */
7
+
8
+ /**
9
+ * DataLoader batches multiple load calls within a single microtask.
10
+ * Used internally for efficient field resolution.
11
+ */
12
+ export class DataLoader<K, V> {
13
+ private batch: Map<K, { resolve: (v: V | null) => void; reject: (e: Error) => void }[]> =
14
+ new Map();
15
+ private scheduled = false;
16
+
17
+ constructor(private batchFn: (keys: K[]) => Promise<(V | null)[]>) {}
18
+
19
+ async load(key: K): Promise<V | null> {
20
+ return new Promise((resolve, reject) => {
21
+ const existing = this.batch.get(key);
22
+ if (existing) {
23
+ existing.push({ resolve, reject });
24
+ } else {
25
+ this.batch.set(key, [{ resolve, reject }]);
26
+ }
27
+
28
+ if (!this.scheduled) {
29
+ this.scheduled = true;
30
+ queueMicrotask(() => this.flush());
31
+ }
32
+ });
33
+ }
34
+
35
+ private async flush(): Promise<void> {
36
+ this.scheduled = false;
37
+ const batch = this.batch;
38
+ this.batch = new Map();
39
+
40
+ const keys = Array.from(batch.keys());
41
+ if (keys.length === 0) return;
42
+
43
+ try {
44
+ const results = await this.batchFn(keys);
45
+ let i = 0;
46
+ for (const [_key, callbacks] of batch) {
47
+ const result = results[i++];
48
+ for (const { resolve } of callbacks) {
49
+ resolve(result);
50
+ }
51
+ }
52
+ } catch (error) {
53
+ for (const [, callbacks] of batch) {
54
+ for (const { reject } of callbacks) {
55
+ reject(error instanceof Error ? error : new Error(String(error)));
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @sylphx/lens-server - Selection
3
+ *
4
+ * Field selection logic for query results.
5
+ */
6
+
7
+ import type { SelectionObject } from "./types.js";
8
+
9
+ /**
10
+ * Apply field selection to data.
11
+ * Recursively filters data to only include selected fields.
12
+ *
13
+ * @param data - The data to filter
14
+ * @param select - Selection object specifying which fields to include
15
+ * @returns Filtered data with only selected fields
16
+ */
17
+ export function applySelection(data: unknown, select: SelectionObject): unknown {
18
+ if (!data) return data;
19
+
20
+ if (Array.isArray(data)) {
21
+ return data.map((item) => applySelection(item, select));
22
+ }
23
+
24
+ if (typeof data !== "object") return data;
25
+
26
+ const obj = data as Record<string, unknown>;
27
+ const result: Record<string, unknown> = {};
28
+
29
+ // Always include id
30
+ if ("id" in obj) result.id = obj.id;
31
+
32
+ for (const [key, value] of Object.entries(select)) {
33
+ if (!(key in obj)) continue;
34
+
35
+ if (value === true) {
36
+ result[key] = obj[key];
37
+ } else if (typeof value === "object" && value !== null) {
38
+ const nestedSelect = "select" in value ? value.select : value;
39
+ result[key] = applySelection(obj[key], nestedSelect as SelectionObject);
40
+ }
41
+ }
42
+
43
+ return result;
44
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * @sylphx/lens-server - Server Types
3
+ *
4
+ * Type definitions for Lens server configuration and operations.
5
+ */
6
+
7
+ import type {
8
+ ContextValue,
9
+ EntityDef,
10
+ InferRouterContext,
11
+ MutationDef,
12
+ OptimisticDSL,
13
+ QueryDef,
14
+ Resolvers,
15
+ RouterDef,
16
+ } from "@sylphx/lens-core";
17
+ import type { PluginManager, ServerPlugin } from "../plugin/types.js";
18
+
19
+ // =============================================================================
20
+ // Selection Types
21
+ // =============================================================================
22
+
23
+ /** Selection object type for nested field selection */
24
+ export interface SelectionObject {
25
+ [key: string]: boolean | SelectionObject | { select: SelectionObject };
26
+ }
27
+
28
+ // =============================================================================
29
+ // Map Types
30
+ // =============================================================================
31
+
32
+ /** Entity map type */
33
+ export type EntitiesMap = Record<string, EntityDef<string, any>>;
34
+
35
+ /** Queries map type */
36
+ export type QueriesMap = Record<string, QueryDef<unknown, unknown>>;
37
+
38
+ /** Mutations map type */
39
+ export type MutationsMap = Record<string, MutationDef<unknown, unknown>>;
40
+
41
+ // =============================================================================
42
+ // Operation Metadata
43
+ // =============================================================================
44
+
45
+ /** Operation metadata for handshake */
46
+ export interface OperationMeta {
47
+ type: "query" | "mutation" | "subscription";
48
+ optimistic?: OptimisticDSL;
49
+ }
50
+
51
+ /** Nested operations structure for handshake */
52
+ export type OperationsMap = {
53
+ [key: string]: OperationMeta | OperationsMap;
54
+ };
55
+
56
+ // =============================================================================
57
+ // Logger
58
+ // =============================================================================
59
+
60
+ /** Logger interface */
61
+ export interface LensLogger {
62
+ info?: (message: string, ...args: unknown[]) => void;
63
+ warn?: (message: string, ...args: unknown[]) => void;
64
+ error?: (message: string, ...args: unknown[]) => void;
65
+ }
66
+
67
+ // =============================================================================
68
+ // Server Configuration
69
+ // =============================================================================
70
+
71
+ /** Server configuration */
72
+ export interface LensServerConfig<
73
+ TContext extends ContextValue = ContextValue,
74
+ TRouter extends RouterDef = RouterDef,
75
+ > {
76
+ /** Entity definitions */
77
+ entities?: EntitiesMap | undefined;
78
+ /** Router definition (namespaced operations) */
79
+ router?: TRouter | undefined;
80
+ /** Query definitions (flat) */
81
+ queries?: QueriesMap | undefined;
82
+ /** Mutation definitions (flat) */
83
+ mutations?: MutationsMap | undefined;
84
+ /** Field resolvers array */
85
+ resolvers?: Resolvers | undefined;
86
+ /** Logger for server messages (default: silent) */
87
+ logger?: LensLogger | undefined;
88
+ /** Context factory */
89
+ context?: ((req?: unknown) => TContext | Promise<TContext>) | undefined;
90
+ /** Server version */
91
+ version?: string | undefined;
92
+ /**
93
+ * Server-level plugins for subscription lifecycle and state management.
94
+ * Plugins are processed at the server level, not adapter level.
95
+ */
96
+ plugins?: ServerPlugin[] | undefined;
97
+ }
98
+
99
+ // =============================================================================
100
+ // Server Metadata
101
+ // =============================================================================
102
+
103
+ /** Server metadata for transport handshake */
104
+ export interface ServerMetadata {
105
+ version: string;
106
+ operations: OperationsMap;
107
+ }
108
+
109
+ // =============================================================================
110
+ // Operations
111
+ // =============================================================================
112
+
113
+ /** Operation for execution */
114
+ export interface LensOperation {
115
+ path: string;
116
+ input?: unknown;
117
+ }
118
+
119
+ /** Result from operation execution */
120
+ export interface LensResult<T = unknown> {
121
+ data?: T;
122
+ error?: Error;
123
+ }
124
+
125
+ // =============================================================================
126
+ // Client Communication
127
+ // =============================================================================
128
+
129
+ /**
130
+ * Client send function type for subscription updates.
131
+ */
132
+ export type ClientSendFn = (message: unknown) => void;
133
+
134
+ /** WebSocket interface for adapters */
135
+ export interface WebSocketLike {
136
+ send(data: string): void;
137
+ close(): void;
138
+ onmessage?: ((event: { data: string }) => void) | null;
139
+ onclose?: (() => void) | null;
140
+ onerror?: ((error: unknown) => void) | null;
141
+ }
142
+
143
+ // =============================================================================
144
+ // Server Interface
145
+ // =============================================================================
146
+
147
+ /**
148
+ * Lens server interface
149
+ *
150
+ * Core methods:
151
+ * - getMetadata() - Server metadata for transport handshake
152
+ * - execute() - Execute any operation
153
+ *
154
+ * Subscription support (used by adapters):
155
+ * - addClient() / removeClient() - Client connection management
156
+ * - subscribe() / unsubscribe() - Subscription lifecycle
157
+ * - send() - Send data to client (runs through plugin hooks)
158
+ * - broadcast() - Broadcast to all entity subscribers
159
+ * - handleReconnect() - Handle client reconnection
160
+ *
161
+ * The server handles all business logic including state management (via plugins).
162
+ * Handlers are pure protocol translators that call these methods.
163
+ */
164
+ export interface LensServer {
165
+ /** Get server metadata for transport handshake */
166
+ getMetadata(): ServerMetadata;
167
+ /** Execute operation - auto-detects query vs mutation */
168
+ execute(op: LensOperation): Promise<LensResult>;
169
+
170
+ // =========================================================================
171
+ // Subscription Support (Optional - used by WS/SSE handlers)
172
+ // =========================================================================
173
+
174
+ /**
175
+ * Register a client connection.
176
+ * Call when a client connects via WebSocket/SSE.
177
+ */
178
+ addClient(clientId: string, send: ClientSendFn): Promise<boolean>;
179
+
180
+ /**
181
+ * Remove a client connection.
182
+ * Call when a client disconnects.
183
+ */
184
+ removeClient(clientId: string, subscriptionCount: number): void;
185
+
186
+ /**
187
+ * Subscribe a client to an entity.
188
+ * Runs plugin hooks and sets up state tracking (if clientState is enabled).
189
+ */
190
+ subscribe(ctx: import("../plugin/types.js").SubscribeContext): Promise<boolean>;
191
+
192
+ /**
193
+ * Unsubscribe a client from an entity.
194
+ * Runs plugin hooks and cleans up state tracking.
195
+ */
196
+ unsubscribe(ctx: import("../plugin/types.js").UnsubscribeContext): void;
197
+
198
+ /**
199
+ * Send data to a client for a specific subscription.
200
+ * Runs through plugin hooks (beforeSend/afterSend) for optimization.
201
+ */
202
+ send(
203
+ clientId: string,
204
+ subscriptionId: string,
205
+ entity: string,
206
+ entityId: string,
207
+ data: Record<string, unknown>,
208
+ isInitial: boolean,
209
+ ): Promise<void>;
210
+
211
+ /**
212
+ * Broadcast data to all subscribers of an entity.
213
+ * Runs through plugin hooks for each subscriber.
214
+ */
215
+ broadcast(entity: string, entityId: string, data: Record<string, unknown>): Promise<void>;
216
+
217
+ /**
218
+ * Handle a reconnection request from a client.
219
+ * Uses plugin hooks (onReconnect) for reconnection logic.
220
+ */
221
+ handleReconnect(
222
+ ctx: import("../plugin/types.js").ReconnectContext,
223
+ ): Promise<import("../plugin/types.js").ReconnectHookResult[] | null>;
224
+
225
+ /**
226
+ * Update subscribed fields for a client's subscription.
227
+ * Runs plugin hooks (onUpdateFields) to sync state.
228
+ */
229
+ updateFields(ctx: import("../plugin/types.js").UpdateFieldsContext): Promise<void>;
230
+
231
+ /**
232
+ * Get the plugin manager for direct hook access.
233
+ */
234
+ getPluginManager(): PluginManager;
235
+ }
236
+
237
+ // =============================================================================
238
+ // Type Inference
239
+ // =============================================================================
240
+
241
+ import type { FieldType } from "@sylphx/lens-core";
242
+
243
+ export type InferInput<T> =
244
+ T extends QueryDef<infer I, any> ? I : T extends MutationDef<infer I, any> ? I : never;
245
+
246
+ export type InferOutput<T> =
247
+ T extends QueryDef<any, infer O>
248
+ ? O
249
+ : T extends MutationDef<any, infer O>
250
+ ? O
251
+ : T extends FieldType<infer F>
252
+ ? F
253
+ : never;
254
+
255
+ export type InferApi<T> = T extends { _types: infer Types } ? Types : never;
256
+
257
+ export type ServerConfigWithInferredContext<
258
+ TRouter extends RouterDef,
259
+ Q extends QueriesMap = QueriesMap,
260
+ M extends MutationsMap = MutationsMap,
261
+ > = {
262
+ router: TRouter;
263
+ entities?: EntitiesMap;
264
+ queries?: Q;
265
+ mutations?: M;
266
+ resolvers?: Resolvers;
267
+ logger?: LensLogger;
268
+ context?: () => InferRouterContext<TRouter> | Promise<InferRouterContext<TRouter>>;
269
+ version?: string;
270
+ /** Server-level plugins (clientState, etc.) */
271
+ plugins?: ServerPlugin[];
272
+ };
273
+
274
+ export type ServerConfigLegacy<
275
+ TContext extends ContextValue,
276
+ Q extends QueriesMap = QueriesMap,
277
+ M extends MutationsMap = MutationsMap,
278
+ > = {
279
+ router?: RouterDef | undefined;
280
+ entities?: EntitiesMap;
281
+ queries?: Q;
282
+ mutations?: M;
283
+ resolvers?: Resolvers;
284
+ logger?: LensLogger;
285
+ context?: () => TContext | Promise<TContext>;
286
+ version?: string;
287
+ /** Server-level plugins (clientState, etc.) */
288
+ plugins?: ServerPlugin[];
289
+ };
@@ -1,73 +1,96 @@
1
1
  /**
2
- * @sylphx/lens-server - SSE Transport Adapter
2
+ * @sylphx/lens-server - SSE Handler
3
3
  *
4
- * Thin transport adapter for Server-Sent Events.
5
- * Connects SSE streams to GraphStateManager.
4
+ * Pure transport handler for Server-Sent Events.
5
+ * No state management - just handles SSE connection lifecycle and message sending.
6
6
  */
7
7
 
8
- import type { GraphStateManager, StateClient } from "../state/graph-state-manager.js";
9
-
10
8
  // =============================================================================
11
9
  // Types
12
10
  // =============================================================================
13
11
 
14
12
  /** SSE handler configuration */
15
13
  export interface SSEHandlerConfig {
16
- /** GraphStateManager instance (required) */
17
- stateManager: GraphStateManager;
18
14
  /** Heartbeat interval in ms (default: 30000) */
19
15
  heartbeatInterval?: number;
16
+ /** Called when a client connects */
17
+ onConnect?: (client: SSEClient) => void;
18
+ /** Called when a client disconnects */
19
+ onDisconnect?: (clientId: string) => void;
20
20
  }
21
21
 
22
- /** SSE client info */
23
- export interface SSEClientInfo {
22
+ /** SSE client handle for sending messages */
23
+ export interface SSEClient {
24
+ /** Unique client ID */
24
25
  id: string;
25
- connectedAt: number;
26
+ /** Send a message to this client */
27
+ send: (message: unknown) => void;
28
+ /** Send a named event to this client */
29
+ sendEvent: (event: string, data: unknown) => void;
30
+ /** Close this client's connection */
31
+ close: () => void;
26
32
  }
27
33
 
28
34
  // =============================================================================
29
- // SSE Handler (Transport Adapter)
35
+ // SSE Handler
30
36
  // =============================================================================
31
37
 
32
38
  /**
33
- * SSE transport adapter for GraphStateManager.
39
+ * Pure SSE transport handler.
34
40
  *
35
- * This is a thin adapter that:
36
- * - Creates SSE connections
37
- * - Registers clients with GraphStateManager
38
- * - Forwards updates to SSE streams
41
+ * This handler ONLY manages:
42
+ * - SSE connection lifecycle
43
+ * - Message sending to clients
44
+ * - Heartbeat keepalive
39
45
  *
40
- * All state/subscription logic is handled by GraphStateManager.
46
+ * It does NOT know about:
47
+ * - State management
48
+ * - Subscriptions
49
+ * - Plugins
41
50
  *
42
51
  * @example
43
52
  * ```typescript
44
- * const stateManager = new GraphStateManager();
45
- * const sse = new SSEHandler({ stateManager });
53
+ * const sse = new SSEHandler({
54
+ * onConnect: (client) => {
55
+ * console.log('Client connected:', client.id);
56
+ * // Register with your state management here
57
+ * },
58
+ * onDisconnect: (clientId) => {
59
+ * console.log('Client disconnected:', clientId);
60
+ * // Cleanup your state management here
61
+ * },
62
+ * });
46
63
  *
47
64
  * // Handle SSE connection
48
65
  * app.get('/events', (req) => sse.handleConnection(req));
49
66
  *
50
- * // Subscribe via separate endpoint or message
51
- * stateManager.subscribe(clientId, "Post", "123", "*");
67
+ * // Send message to specific client
68
+ * sse.send(clientId, { type: 'update', data: {...} });
52
69
  * ```
53
70
  */
54
71
  export class SSEHandler {
55
- private stateManager: GraphStateManager;
56
72
  private heartbeatInterval: number;
73
+ private onConnectCallback: ((client: SSEClient) => void) | undefined;
74
+ private onDisconnectCallback: ((clientId: string) => void) | undefined;
57
75
  private clients = new Map<
58
76
  string,
59
- { controller: ReadableStreamDefaultController; heartbeat: ReturnType<typeof setInterval> }
77
+ {
78
+ controller: ReadableStreamDefaultController;
79
+ heartbeat: ReturnType<typeof setInterval>;
80
+ encoder: TextEncoder;
81
+ }
60
82
  >();
61
83
  private clientCounter = 0;
62
84
 
63
- constructor(config: SSEHandlerConfig) {
64
- this.stateManager = config.stateManager;
85
+ constructor(config: SSEHandlerConfig = {}) {
65
86
  this.heartbeatInterval = config.heartbeatInterval ?? 30000;
87
+ this.onConnectCallback = config.onConnect;
88
+ this.onDisconnectCallback = config.onDisconnect;
66
89
  }
67
90
 
68
91
  /**
69
- * Handle new SSE connection
70
- * Returns a Response with SSE stream
92
+ * Handle new SSE connection.
93
+ * Returns a Response with SSE stream.
71
94
  */
72
95
  handleConnection(_req?: Request): Response {
73
96
  const clientId = `sse_${++this.clientCounter}_${Date.now()}`;
@@ -75,26 +98,6 @@ export class SSEHandler {
75
98
 
76
99
  const stream = new ReadableStream({
77
100
  start: (controller) => {
78
- // Register with GraphStateManager
79
- const stateClient: StateClient = {
80
- id: clientId,
81
- send: (msg) => {
82
- try {
83
- const data = `data: ${JSON.stringify(msg)}\n\n`;
84
- controller.enqueue(encoder.encode(data));
85
- } catch {
86
- // Connection closed
87
- this.removeClient(clientId);
88
- }
89
- },
90
- };
91
- this.stateManager.addClient(stateClient);
92
-
93
- // Send connected event
94
- controller.enqueue(
95
- encoder.encode(`event: connected\ndata: ${JSON.stringify({ clientId })}\n\n`),
96
- );
97
-
98
101
  // Setup heartbeat
99
102
  const heartbeat = setInterval(() => {
100
103
  try {
@@ -105,7 +108,21 @@ export class SSEHandler {
105
108
  }, this.heartbeatInterval);
106
109
 
107
110
  // Track client
108
- this.clients.set(clientId, { controller, heartbeat });
111
+ this.clients.set(clientId, { controller, heartbeat, encoder });
112
+
113
+ // Send connected event
114
+ controller.enqueue(
115
+ encoder.encode(`event: connected\ndata: ${JSON.stringify({ clientId })}\n\n`),
116
+ );
117
+
118
+ // Create client handle and notify
119
+ const client: SSEClient = {
120
+ id: clientId,
121
+ send: (message: unknown) => this.send(clientId, message),
122
+ sendEvent: (event: string, data: unknown) => this.sendEvent(clientId, event, data),
123
+ close: () => this.closeClient(clientId),
124
+ };
125
+ this.onConnectCallback?.(client);
109
126
  },
110
127
  cancel: () => {
111
128
  this.removeClient(clientId);
@@ -123,19 +140,62 @@ export class SSEHandler {
123
140
  }
124
141
 
125
142
  /**
126
- * Remove client and cleanup
143
+ * Send a message to a specific client.
144
+ */
145
+ send(clientId: string, message: unknown): boolean {
146
+ const client = this.clients.get(clientId);
147
+ if (!client) return false;
148
+
149
+ try {
150
+ const data = `data: ${JSON.stringify(message)}\n\n`;
151
+ client.controller.enqueue(client.encoder.encode(data));
152
+ return true;
153
+ } catch {
154
+ this.removeClient(clientId);
155
+ return false;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Send a named event to a specific client.
161
+ */
162
+ sendEvent(clientId: string, event: string, data: unknown): boolean {
163
+ const client = this.clients.get(clientId);
164
+ if (!client) return false;
165
+
166
+ try {
167
+ const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
168
+ client.controller.enqueue(client.encoder.encode(message));
169
+ return true;
170
+ } catch {
171
+ this.removeClient(clientId);
172
+ return false;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Broadcast a message to all connected clients.
178
+ */
179
+ broadcast(message: unknown): void {
180
+ for (const clientId of this.clients.keys()) {
181
+ this.send(clientId, message);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Remove client and cleanup.
127
187
  */
128
188
  private removeClient(clientId: string): void {
129
189
  const client = this.clients.get(clientId);
130
190
  if (client) {
131
191
  clearInterval(client.heartbeat);
132
192
  this.clients.delete(clientId);
193
+ this.onDisconnectCallback?.(clientId);
133
194
  }
134
- this.stateManager.removeClient(clientId);
135
195
  }
136
196
 
137
197
  /**
138
- * Close specific client connection
198
+ * Close specific client connection.
139
199
  */
140
200
  closeClient(clientId: string): void {
141
201
  const client = this.clients.get(clientId);
@@ -150,21 +210,28 @@ export class SSEHandler {
150
210
  }
151
211
 
152
212
  /**
153
- * Get connected client count
213
+ * Get connected client count.
154
214
  */
155
215
  getClientCount(): number {
156
216
  return this.clients.size;
157
217
  }
158
218
 
159
219
  /**
160
- * Get connected client IDs
220
+ * Get connected client IDs.
161
221
  */
162
222
  getClientIds(): string[] {
163
223
  return Array.from(this.clients.keys());
164
224
  }
165
225
 
166
226
  /**
167
- * Close all connections
227
+ * Check if a client is connected.
228
+ */
229
+ hasClient(clientId: string): boolean {
230
+ return this.clients.has(clientId);
231
+ }
232
+
233
+ /**
234
+ * Close all connections.
168
235
  */
169
236
  closeAll(): void {
170
237
  for (const clientId of this.clients.keys()) {
@@ -178,8 +245,8 @@ export class SSEHandler {
178
245
  // =============================================================================
179
246
 
180
247
  /**
181
- * Create SSE handler (transport adapter)
248
+ * Create SSE handler (pure transport).
182
249
  */
183
- export function createSSEHandler(config: SSEHandlerConfig): SSEHandler {
250
+ export function createSSEHandler(config: SSEHandlerConfig = {}): SSEHandler {
184
251
  return new SSEHandler(config);
185
252
  }
@@ -1,16 +1,14 @@
1
1
  /**
2
2
  * @sylphx/lens-server - State Management
3
3
  *
4
- * Server-side state management for reactive data sync.
4
+ * State management is handled by the storage layer.
5
+ * See: packages/server/src/storage/
6
+ *
7
+ * Available storage adapters:
8
+ * - memoryStorage() - In-memory (default)
9
+ * - redisStorage() - Redis via ioredis
10
+ * - upstashStorage() - Upstash Redis HTTP
11
+ * - vercelKVStorage() - Vercel KV
5
12
  */
6
13
 
7
- export {
8
- createGraphStateManager,
9
- type EntityKey,
10
- GraphStateManager,
11
- type GraphStateManagerConfig,
12
- type StateClient,
13
- type StateFullMessage,
14
- type StateUpdateMessage,
15
- type Subscription,
16
- } from "./graph-state-manager.js";
14
+ // No exports - state management moved to storage layer