diff --git a/packages/qwik/src/core/api.md b/packages/qwik/src/core/api.md index 06aa3736bdc..07212c136eb 100644 --- a/packages/qwik/src/core/api.md +++ b/packages/qwik/src/core/api.md @@ -335,13 +335,12 @@ export { DomContainer as _DomContainer } // @public (undocumented) export type EagernessOptions = 'visible' | 'load' | 'idle'; -// Warning: (ae-forgotten-export) The symbol "Task" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ISsrNode" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "Signal_3" needs to be exported by the entry point index.d.ts -// Warning: (ae-internal-missing-underscore) The name "Effect" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export type Effect = Task | _VNode | ISsrNode | Signal_3; +// @internal (undocumented) +export class _EffectData = Record> { + constructor(data: T); + // (undocumented) + data: T; +} // @internal (undocumented) export type _ElementVNode = [ @@ -1132,8 +1131,10 @@ export abstract class _SharedContainer implements Container2 { abstract setContext(host: HostElement, context: ContextId, value: T): void; // (undocumented) abstract setHostProp(host: HostElement, name: string, value: T): void; + // Warning: (ae-forgotten-export) The symbol "Effect" needs to be exported by the entry point index.d.ts + // // (undocumented) - trackSignalValue(signal: Signal_2, subscriber: Effect, property: string, data: any): T; + trackSignalValue(signal: Signal_2, subscriber: Effect, property: string, data: _EffectData): T; } // @public diff --git a/packages/qwik/src/core/index.ts b/packages/qwik/src/core/index.ts index 5f4c2e2a836..a535b08018f 100644 --- a/packages/qwik/src/core/index.ts +++ b/packages/qwik/src/core/index.ts @@ -127,7 +127,6 @@ export { useComputed$, useTask$, useVisibleTask$ } from './use/use-task-dollar'; export { useErrorBoundary } from './use/use-error-boundary'; export type { ErrorBoundaryStore } from './render/error-handling'; export { - type Effect, type ReadonlySignal, type Signal, type ComputedSignal, @@ -138,6 +137,7 @@ export { createComputedQrl, createComputed$, } from './v2/signal/v2-signal.public'; +export { EffectData as _EffectData } from './v2/signal/v2-signal'; ////////////////////////////////////////////////////////////////////////////////////////// // Developer Low-Level API diff --git a/packages/qwik/src/core/use/use-core.ts b/packages/qwik/src/core/use/use-core.ts index ac968c7621f..d9e55ec1950 100644 --- a/packages/qwik/src/core/use/use-core.ts +++ b/packages/qwik/src/core/use/use-core.ts @@ -25,7 +25,11 @@ import type { Container2 } from '../v2/shared/types'; import { vnode_getNode, vnode_isElementVNode, vnode_isVNode } from '../v2/client/vnode'; import { _getQContainerElement } from '../v2/client/dom-container'; import type { ContainerElement } from '../v2/client/types'; -import type { EffectSubscriptions, EffectSubscriptionsProp } from '../v2/signal/v2-signal'; +import type { + EffectData, + EffectSubscriptions, + EffectSubscriptionsProp, +} from '../v2/signal/v2-signal'; declare const document: QwikDocument; @@ -260,12 +264,15 @@ export const trackSignal = ( subscriber: EffectSubscriptions[EffectSubscriptionsProp.EFFECT], property: EffectSubscriptions[EffectSubscriptionsProp.PROPERTY], container: Container2, - data: EffectSubscriptions[EffectSubscriptionsProp.DATA] = null + data?: EffectData ): T => { const previousSubscriber = trackInvocation.$effectSubscriber$; const previousContainer = trackInvocation.$container2$; try { - trackInvocation.$effectSubscriber$ = [subscriber, property, data]; + trackInvocation.$effectSubscriber$ = [subscriber, property]; + if (data) { + trackInvocation.$effectSubscriber$.push(data); + } trackInvocation.$container2$ = container; return invoke(trackInvocation, fn); } finally { diff --git a/packages/qwik/src/core/use/use-task.ts b/packages/qwik/src/core/use/use-task.ts index e3d25cd1c37..0438fe46ff4 100644 --- a/packages/qwik/src/core/use/use-task.ts +++ b/packages/qwik/src/core/use/use-task.ts @@ -354,7 +354,7 @@ export const runTask2 = ( const track: Tracker = (obj: (() => unknown) | object | Signal, prop?: string) => { const ctx = newInvokeContext(); - ctx.$effectSubscriber$ = [task, EffectProperty.COMPONENT, null]; + ctx.$effectSubscriber$ = [task, EffectProperty.COMPONENT]; ctx.$container2$ = container; return invoke(ctx, () => { if (isFunction(obj)) { @@ -556,7 +556,7 @@ export const runResource = ( const track: Tracker = (obj: (() => unknown) | object | Signal, prop?: string) => { const ctx = newInvokeContext(); - ctx.$effectSubscriber$ = [task, EffectProperty.COMPONENT, null]; + ctx.$effectSubscriber$ = [task, EffectProperty.COMPONENT]; ctx.$container2$ = container; return invoke(ctx, () => { if (isFunction(obj)) { diff --git a/packages/qwik/src/core/v2/client/vnode-diff.ts b/packages/qwik/src/core/v2/client/vnode-diff.ts index e5cf171351f..e241584cec9 100644 --- a/packages/qwik/src/core/v2/client/vnode-diff.ts +++ b/packages/qwik/src/core/v2/client/vnode-diff.ts @@ -34,7 +34,7 @@ import { isHtmlAttributeAnEventName, isJsxPropertyAnEventName, } from '../shared/event-names'; -import { ChoreType } from '../shared/scheduler'; +import { ChoreType, type NodePropData } from '../shared/scheduler'; import { hasClassAttr } from '../shared/scoped-styles'; import type { HostElement, @@ -92,7 +92,7 @@ import { type VNodeJournal, } from './vnode'; import { getNewElementNamespaceData } from './vnode-namespace'; -import { WrappedSignal, EffectProperty, isSignal } from '../signal/v2-signal'; +import { WrappedSignal, EffectProperty, isSignal, EffectData } from '../signal/v2-signal'; import type { Signal } from '../signal/v2-signal.public'; import { executeComponent2 } from '../shared/component-execution'; import { isParentSlotProp, isSlotProp } from '../../util/prop'; @@ -629,12 +629,16 @@ export const vnode_diff = ( } if (isSignal(value)) { + const signalData = new EffectData({ + $scopedStyleIdPrefix$: scopedStyleIdPrefix, + $isConst$: true, + }); value = trackSignal( () => (value as Signal).value, vNewNode as ElementVNode, key, container, - scopedStyleIdPrefix + signalData ); } diff --git a/packages/qwik/src/core/v2/shared/component-execution.ts b/packages/qwik/src/core/v2/shared/component-execution.ts index 647945fb052..e81b9dbcfad 100644 --- a/packages/qwik/src/core/v2/shared/component-execution.ts +++ b/packages/qwik/src/core/v2/shared/component-execution.ts @@ -58,7 +58,7 @@ export const executeComponent2 = ( undefined, RenderEvent ); - iCtx.$effectSubscriber$ = [subscriptionHost, EffectProperty.COMPONENT, null]; + iCtx.$effectSubscriber$ = [subscriptionHost, EffectProperty.COMPONENT]; iCtx.$container2$ = container; let componentFn: (props: unknown) => ValueOrPromise; container.ensureProjectionResolved(renderHost); diff --git a/packages/qwik/src/core/v2/shared/scheduler.ts b/packages/qwik/src/core/v2/shared/scheduler.ts index 77368338e3b..7a8bdca6c86 100644 --- a/packages/qwik/src/core/v2/shared/scheduler.ts +++ b/packages/qwik/src/core/v2/shared/scheduler.ts @@ -153,9 +153,13 @@ export interface Chore { $executed$: boolean; } -export interface NodePropPayload { - value: Signal; - scopedStyleIdPrefix: string | null; +export interface NodePropData { + $scopedStyleIdPrefix$: string | null; + $isConst$: boolean; +} + +export interface NodePropPayload extends NodePropData { + $value$: Signal; } export type Scheduler = ReturnType; @@ -372,16 +376,14 @@ export const createScheduler = ( case ChoreType.NODE_PROP: const virtualNode = chore.$host$ as unknown as ElementVNode; const payload = chore.$payload$ as NodePropPayload; - let value: Signal | string = payload.value; - // TODO: temp solution! - let isConst = false; + let value: Signal | string = payload.$value$; if (isSignal(value)) { value = value.value as any; - isConst = true; } + const isConst = payload.$isConst$; const journal = (container as DomContainer).$journal$; const property = chore.$idx$ as string; - value = serializeAttribute(property, value, payload.scopedStyleIdPrefix); + value = serializeAttribute(property, value, payload.$scopedStyleIdPrefix$); if (isConst) { const element = virtualNode[ElementVNodeProps.element] as Element; journal.push(VNodeJournalOpCode.SetAttribute, element, property, value); diff --git a/packages/qwik/src/core/v2/shared/shared-container.ts b/packages/qwik/src/core/v2/shared/shared-container.ts index cd7b7fce902..b6d006c07da 100644 --- a/packages/qwik/src/core/v2/shared/shared-container.ts +++ b/packages/qwik/src/core/v2/shared/shared-container.ts @@ -5,7 +5,7 @@ import type { ContextId } from '../../use/use-context'; import { trackSignal } from '../../use/use-core'; import type { ValueOrPromise } from '../../util/types'; import { version } from '../../version'; -import type { Effect } from '../signal/v2-signal'; +import type { Effect, EffectData } from '../signal/v2-signal'; import type { StreamWriter, SymbolToChunkResolver } from '../ssr/ssr-types'; import type { Scheduler } from './scheduler'; import { createScheduler } from './scheduler'; @@ -42,7 +42,7 @@ export abstract class _SharedContainer implements Container2 { this.$scheduler$ = createScheduler(this, scheduleDrain, journalFlush); } - trackSignalValue(signal: Signal, subscriber: Effect, property: string, data: any): T { + trackSignalValue(signal: Signal, subscriber: Effect, property: string, data: EffectData): T { return trackSignal(() => signal.value, subscriber, property, this, data); } diff --git a/packages/qwik/src/core/v2/shared/shared-serialization.ts b/packages/qwik/src/core/v2/shared/shared-serialization.ts index 2beaa32ab21..ab503f3e379 100644 --- a/packages/qwik/src/core/v2/shared/shared-serialization.ts +++ b/packages/qwik/src/core/v2/shared/shared-serialization.ts @@ -37,6 +37,7 @@ import { EffectSubscriptionsProp, Signal, type EffectSubscriptions, + EffectData, } from '../signal/v2-signal'; import { STORE_ARRAY_PROP, @@ -1029,9 +1030,14 @@ function serializeEffectSubs( const effectSubscription = effects[i]; const effect = effectSubscription[EffectSubscriptionsProp.EFFECT]; const prop = effectSubscription[EffectSubscriptionsProp.PROPERTY]; - const additionalData = effectSubscription[EffectSubscriptionsProp.DATA]; - data += ';' + addRoot(effect) + ' ' + prop + ' ' + addRoot(additionalData); - for (let j = EffectSubscriptionsProp.FIRST_BACK_REF; j < effectSubscription.length; j++) { + data += ';' + addRoot(effect) + ' ' + prop; + let effectSubscriptionDataIndex = EffectSubscriptionsProp.FIRST_BACK_REF_OR_DATA; + const effectSubscriptionData = effectSubscription[effectSubscriptionDataIndex]; + if (effectSubscriptionData instanceof EffectData) { + data += ' |' + addRoot(effectSubscriptionData.data); + effectSubscriptionDataIndex++; + } + for (let j = effectSubscriptionDataIndex; j < effectSubscription.length; j++) { data += ' ' + addRoot(effectSubscription[j]); } } @@ -1172,10 +1178,17 @@ function deserializeSignal2Effect( ) { while (idx < parts.length) { // idx == 1 is the attribute name - const effect = parts[idx++] - .split(' ') - .map((obj, idx) => (idx == 1 ? obj : container.$getObjectById$(obj))); - effects.push(effect as fixMeAny); + const effect = parts[idx++].split(' ').map((obj, idx) => { + if (idx === EffectSubscriptionsProp.PROPERTY) { + return obj; + } else { + if (obj[0] === '|') { + return new EffectData(container.$getObjectById$(parseInt(obj.substring(1)))); + } + return container.$getObjectById$(obj); + } + }) as EffectSubscriptions; + effects.push(effect); } return idx; } diff --git a/packages/qwik/src/core/v2/signal/v2-signal.public.ts b/packages/qwik/src/core/v2/signal/v2-signal.public.ts index 19f270d0055..b2bc16287f5 100644 --- a/packages/qwik/src/core/v2/signal/v2-signal.public.ts +++ b/packages/qwik/src/core/v2/signal/v2-signal.public.ts @@ -7,8 +7,6 @@ import { export { isSignal } from './v2-signal'; -export type { Effect } from './v2-signal'; - /** @public */ export interface ReadonlySignal { readonly value: T; diff --git a/packages/qwik/src/core/v2/signal/v2-signal.ts b/packages/qwik/src/core/v2/signal/v2-signal.ts index 6a57a8b3a17..255b0cdb4d3 100644 --- a/packages/qwik/src/core/v2/signal/v2-signal.ts +++ b/packages/qwik/src/core/v2/signal/v2-signal.ts @@ -23,7 +23,7 @@ import { isPromise } from '../../util/promises'; import { qDev } from '../../util/qdev'; import type { VNode } from '../client/types'; import { vnode_getProp, vnode_isVirtualVNode, vnode_isVNode, vnode_setProp } from '../client/vnode'; -import { ChoreType, type NodePropPayload } from '../shared/scheduler'; +import { ChoreType, type NodePropData, type NodePropPayload } from '../shared/scheduler'; import type { Container2, HostElement, fixMeAny } from '../shared/types'; import type { ISsrNode } from '../ssr/ssr-types'; import type { Signal as ISignal, ReadonlySignal } from './v2-signal.public'; @@ -77,11 +77,18 @@ export const isSignal = (value: any): value is ISignal => { * - `Task`: `useTask`, `useVisibleTask`, `useResource` * - `VNode` and `ISsrNode`: Either a component or `` * - `Signal2`: A derived signal which contains a computation function. - * - * @internal */ export type Effect = Task | VNode | ISsrNode | Signal; +/** @internal */ +export class EffectData = Record> { + data: T; + + constructor(data: T) { + this.data = data; + } +} + /** * An effect plus a list of subscriptions effect depends on. * @@ -121,11 +128,12 @@ export type Effect = Task | VNode | ISsrNode | Signal; export type EffectSubscriptions = [ Effect, // EffectSubscriptionsProp.EFFECT string, // EffectSubscriptionsProp.PROPERTY - any | null, // EffectSubscriptionsProp.DATA - ...// NOTE even thought this is shown as `...(string|Signal2)` - // it is a list of strings followed by a list of signals (not intermingled) + ...// NOTE even thought this is shown as `...(string|Signal)` + // it is a list of strings followed by optional EffectData + // and a list of signals (not intermingled) ( - | string // List of properties (Only used with Store2 (not with Signal2)) + | EffectData // only used at the start + | string // List of properties (Only used with Store (not with Signal)) | Signal | TargetType // List of signals to release )[], @@ -133,8 +141,7 @@ export type EffectSubscriptions = [ export const enum EffectSubscriptionsProp { EFFECT = 0, PROPERTY = 1, - DATA = 2, - FIRST_BACK_REF = 3, + FIRST_BACK_REF_OR_DATA = 2, } export const enum EffectProperty { COMPONENT = ':', @@ -359,13 +366,15 @@ export const triggerEffects = ( container.$scheduler$(ChoreType.NODE_DIFF, host, target, signal as fixMeAny); } else { const host: HostElement = effect as any; - const scopedStyleIdPrefix: string | null = - effectSubscriptions[EffectSubscriptionsProp.DATA]; - const payload: NodePropPayload = { - value: signal as Signal, - scopedStyleIdPrefix, - }; - container.$scheduler$(ChoreType.NODE_PROP, host, property, payload); + let effectData = effectSubscriptions[EffectSubscriptionsProp.FIRST_BACK_REF_OR_DATA]; + if (effectData instanceof EffectData) { + effectData = effectData as EffectData; + const payload: NodePropPayload = { + ...effectData.data, + $value$: signal, + }; + container.$scheduler$(ChoreType.NODE_PROP, host, property, payload); + } } }; effects.forEach(scheduleEffect); @@ -440,7 +449,7 @@ export class ComputedSignal extends Signal { const ctx = tryGetInvokeContext(); assertDefined(computeQrl, 'Signal is marked as dirty, but no compute function is provided.'); const previousEffectSubscription = ctx?.$effectSubscriber$; - ctx && (ctx.$effectSubscriber$ = [this, EffectProperty.VNODE, null]); + ctx && (ctx.$effectSubscriber$ = [this, EffectProperty.VNODE]); assertTrue( !!computeQrl.resolved, 'Computed signals must run sync. Expected the QRL to be resolved at this point.' diff --git a/packages/qwik/src/core/v2/signal/v2-signal.unit.tsx b/packages/qwik/src/core/v2/signal/v2-signal.unit.tsx index a445486cf2b..c30bc15cc26 100644 --- a/packages/qwik/src/core/v2/signal/v2-signal.unit.tsx +++ b/packages/qwik/src/core/v2/signal/v2-signal.unit.tsx @@ -168,7 +168,7 @@ describe('v2-signal', () => { } else { const ctx = newInvokeContext(); ctx.$container2$ = container; - const subscriber: EffectSubscriptions = [task, EffectProperty.COMPONENT, null, ctx]; + const subscriber: EffectSubscriptions = [task, EffectProperty.COMPONENT, ctx]; ctx.$effectSubscriber$ = subscriber; return invoke(ctx, qrl.getFn(ctx)); } diff --git a/packages/qwik/src/server/qwik-types.ts b/packages/qwik/src/server/qwik-types.ts index 36de93033c2..2a4024fcc4c 100644 --- a/packages/qwik/src/server/qwik-types.ts +++ b/packages/qwik/src/server/qwik-types.ts @@ -35,3 +35,4 @@ export type { export type { ResolvedManifest, SymbolMapper } from '../optimizer/src/types'; export type { SymbolToChunkResolver } from '../core/v2/ssr/ssr-types'; export type { fixMeAny } from '../core/v2/shared/types'; +export type { NodePropData } from '../core/v2/shared/scheduler'; diff --git a/packages/qwik/src/server/v2-ssr-container.ts b/packages/qwik/src/server/v2-ssr-container.ts index cd39dc0a152..e17bee991fb 100644 --- a/packages/qwik/src/server/v2-ssr-container.ts +++ b/packages/qwik/src/server/v2-ssr-container.ts @@ -6,6 +6,7 @@ import { _walkJSX, isSignal, type JSXNode, + _EffectData as EffectData, } from '@builder.io/qwik'; import { isDev } from '@builder.io/qwik/build'; import type { ResolvedManifest } from '@builder.io/qwik/optimizer'; @@ -57,6 +58,7 @@ import { type ISsrNode, type JSXChildren, type JSXOutput, + type NodePropData, type SerializationContext, type SsrAttrKey, type SsrAttrValue, @@ -1022,7 +1024,7 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { } } - private writeAttrs(tag: string, attrs: SsrAttrs, immutable: boolean): string | undefined { + private writeAttrs(tag: string, attrs: SsrAttrs, isConst: boolean): string | undefined { let innerHTML: string | undefined = undefined; if (attrs.length) { for (let i = 0; i < attrs.length; i++) { @@ -1057,7 +1059,11 @@ class SSRContainer extends _SharedContainer implements ISSRContainer { if (isSignal(value)) { const lastNode = this.getLastNode(); - value = this.trackSignalValue(value, lastNode, key, styleScopedId); + const signalData = new EffectData({ + $scopedStyleIdPrefix$: styleScopedId, + $isConst$: isConst, + }); + value = this.trackSignalValue(value, lastNode, key, signalData); } if (key === dangerouslySetInnerHTML) {