@kubohiroya/turbowarp-extension-data-editor 0.2.0 → 0.2.2
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/README.md +9 -6
- package/dist/build-info.json +3 -3
- package/dist/fixed-input-branches.d.ts +12 -0
- package/dist/fixed-input-branches.js +180 -0
- package/dist/fixed-input-editor.d.ts +39 -0
- package/dist/fixed-input-editor.js +538 -0
- package/dist/fixed-input-schema.d.ts +15 -0
- package/dist/fixed-input-schema.js +21 -0
- package/dist/fixed-input-snapshot.d.ts +7 -0
- package/dist/fixed-input-snapshot.js +180 -0
- package/dist/fixed-input-types.d.ts +138 -0
- package/dist/fixed-input-types.js +23 -0
- package/dist/fixed-input-view.d.ts +14 -0
- package/dist/fixed-input-view.js +168 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/react.css +168 -0
- package/dist/react.d.ts +8 -0
- package/dist/react.js +19 -2
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +45 -7
- package/dist/rjsf-templates.d.ts +11 -0
- package/dist/rjsf-templates.js +129 -0
- package/dist/rjsf.d.ts +24 -1
- package/dist/rjsf.js +35 -3
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +1 -0
- package/dist/session.d.ts +3 -1
- package/dist/session.js +205 -7
- package/dist/turbowarp/bridge.d.ts +2 -1
- package/dist/turbowarp/bridge.js +37 -5
- package/dist/turbowarp/fixed-fields.d.ts +82 -0
- package/dist/turbowarp/fixed-fields.js +311 -0
- package/dist/turbowarp/fixed-transaction.d.ts +18 -0
- package/dist/turbowarp/fixed-transaction.js +460 -0
- package/dist/turbowarp/host.d.ts +3 -8
- package/dist/turbowarp/host.js +68 -1
- package/dist/turbowarp.d.ts +4 -0
- package/dist/turbowarp.js +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/view.js +4 -0
- package/dist/webpack4/index.js +22 -5
- package/dist/webpack4/metafile.json +592 -299
- package/dist/webpack4/react.js +20 -6
- package/dist/webpack4/react.js.map +2 -2
- package/dist/webpack4/rjsf.js +450 -60
- package/dist/webpack4/rjsf.js.map +4 -4
- package/dist/webpack4/schema.js +6 -3
- package/dist/webpack4/{shared-KQOUMWJ4.js → shared-75QIWLZY.js} +1129 -1244
- package/dist/webpack4/{shared-KQOUMWJ4.js.map → shared-75QIWLZY.js.map} +4 -4
- package/dist/webpack4/shared-FYUILHED.js +1 -0
- package/dist/webpack4/shared-GQLPPRT2.js +210 -0
- package/dist/webpack4/shared-GQLPPRT2.js.map +7 -0
- package/dist/webpack4/shared-P2JBRZFP.js +2206 -0
- package/dist/webpack4/shared-P2JBRZFP.js.map +7 -0
- package/dist/webpack4/turbowarp.js +913 -9
- package/dist/webpack4/turbowarp.js.map +4 -4
- package/docs/api.md +7 -0
- package/docs/fixed-input-contracts.md +77 -0
- package/docs/package-distribution.md +5 -0
- package/docs/stage-2-publication.ja.md +6 -0
- package/package.json +2 -1
- package/dist/webpack4/shared-CQB3NG2A.js +0 -1
- package/dist/webpack4/shared-MRPGBJIH.js +0 -75
- package/dist/webpack4/shared-MRPGBJIH.js.map +0 -7
- package/dist/webpack4/shared-N6YQE2X6.js +0 -1029
- package/dist/webpack4/shared-N6YQE2X6.js.map +0 -7
- /package/dist/webpack4/{shared-CQB3NG2A.js.map → shared-FYUILHED.js.map} +0 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { BlockEditorConflictError, FIXED_INPUTS_CAPABILITY } from '../index.js';
|
|
2
|
+
import { captureFixedInputDeclaration, captureFixedSnapshot, sameFixedSnapshot } from '../fixed-input-snapshot.js';
|
|
3
|
+
import { initializeFixedFields, inspectFixedArgument, prepareFixedChange, restoreFixedField, captureFixedFieldState } from './fixed-fields.js';
|
|
4
|
+
import { snapshotJson } from '../schema-json.js';
|
|
5
|
+
import { validateFixedBranchChanges } from '../fixed-input-branches.js';
|
|
6
|
+
const own = (object, name) => {
|
|
7
|
+
const descriptor = object && Object.getOwnPropertyDescriptor(object, name);
|
|
8
|
+
if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
9
|
+
throw new TypeError('Expected an own data property.');
|
|
10
|
+
}
|
|
11
|
+
return descriptor.value;
|
|
12
|
+
};
|
|
13
|
+
const inheritedData = (object, key) => {
|
|
14
|
+
for (let item = object; item; item = Object.getPrototypeOf(item)) {
|
|
15
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
16
|
+
if (descriptor) {
|
|
17
|
+
if (!('value' in descriptor))
|
|
18
|
+
throw new TypeError('Unsupported undo configuration.');
|
|
19
|
+
return descriptor.value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
};
|
|
24
|
+
const ensureNativeHistory = stack => {
|
|
25
|
+
if (!Array.isArray(stack) || !Object.isExtensible(stack) ||
|
|
26
|
+
Object.getOwnPropertySymbols(stack).length || !Object.getOwnPropertyDescriptor(stack, 'length').writable) {
|
|
27
|
+
throw new TypeError('Unsupported undo history.');
|
|
28
|
+
}
|
|
29
|
+
if (Object.getOwnPropertyNames(stack).length !== stack.length + 1)
|
|
30
|
+
throw new TypeError('Unsupported undo history.');
|
|
31
|
+
for (let index = 0; index < stack.length; index++) {
|
|
32
|
+
const descriptor = Object.getOwnPropertyDescriptor(stack, String(index));
|
|
33
|
+
if (!descriptor || !('value' in descriptor) || !descriptor.writable || !descriptor.configurable) {
|
|
34
|
+
throw new TypeError('Unsupported undo history.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const copyHistory = stack => {
|
|
39
|
+
const copy = [];
|
|
40
|
+
for (let index = 0; index < stack.length; index++)
|
|
41
|
+
copy.push(own(stack, String(index)));
|
|
42
|
+
return copy;
|
|
43
|
+
};
|
|
44
|
+
const replaceHistory = (stack, values) => {
|
|
45
|
+
stack.length = 0;
|
|
46
|
+
for (const value of values)
|
|
47
|
+
Array.prototype.push.call(stack, value);
|
|
48
|
+
};
|
|
49
|
+
const conflict = () => { throw new BlockEditorConflictError(); };
|
|
50
|
+
const aborted = Object.getOwnPropertyDescriptor(AbortSignal.prototype, 'aborted').get;
|
|
51
|
+
const isAborted = signal => aborted.call(signal);
|
|
52
|
+
const optionalOwn = (object, name) => {
|
|
53
|
+
const descriptor = object && Object.getOwnPropertyDescriptor(object, name);
|
|
54
|
+
if (descriptor && !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
|
|
55
|
+
throw new TypeError('VM state must contain data properties.');
|
|
56
|
+
}
|
|
57
|
+
return descriptor && descriptor.value;
|
|
58
|
+
};
|
|
59
|
+
const stateToken = (value, identify, seen = new Set()) => {
|
|
60
|
+
if (typeof value === 'number')
|
|
61
|
+
return ['number', Object.is(value, -0) ? '-0' : String(value)];
|
|
62
|
+
if (value === undefined)
|
|
63
|
+
return ['undefined'];
|
|
64
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
65
|
+
return value;
|
|
66
|
+
if (typeof value !== 'object' || seen.has(value) || seen.size > 64)
|
|
67
|
+
throw new TypeError('Unsupported VM state.');
|
|
68
|
+
seen.add(value);
|
|
69
|
+
const result = Object.getOwnPropertyNames(value).sort().map(key => [key, stateToken(own(value, key), identify, seen)]);
|
|
70
|
+
seen.delete(value);
|
|
71
|
+
return ['object', identify(value), result];
|
|
72
|
+
};
|
|
73
|
+
const untouchedState = (block, vmBlock, target, bindings, identify) => {
|
|
74
|
+
const omitted = new Map();
|
|
75
|
+
for (const binding of bindings.values()) {
|
|
76
|
+
if (!omitted.has(binding.vmBlock))
|
|
77
|
+
omitted.set(binding.vmBlock, new Set());
|
|
78
|
+
omitted.get(binding.vmBlock).add(binding.fieldName);
|
|
79
|
+
}
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
const records = [];
|
|
82
|
+
const capture = item => {
|
|
83
|
+
if (!item || seen.has(item))
|
|
84
|
+
return;
|
|
85
|
+
seen.add(item);
|
|
86
|
+
const fields = item.fields || {};
|
|
87
|
+
records.push([identify(item), ['id', 'opcode', 'parent', 'next', 'inputs', 'mutation', 'shadow', 'topLevel']
|
|
88
|
+
.map(key => [key, stateToken(optionalOwn(item, key), identify)]), identify(fields), Object.keys(fields).sort()
|
|
89
|
+
.map(key => {
|
|
90
|
+
var _a;
|
|
91
|
+
const field = own(fields, key);
|
|
92
|
+
if (!((_a = omitted.get(item)) === null || _a === void 0 ? void 0 : _a.has(key)))
|
|
93
|
+
return [key, stateToken(field, identify)];
|
|
94
|
+
return [key, identify(field), Object.getOwnPropertyNames(field).sort()
|
|
95
|
+
.filter(property => property !== 'value').map(property => [property, stateToken(own(field, property), identify)])];
|
|
96
|
+
})]);
|
|
97
|
+
};
|
|
98
|
+
capture(vmBlock);
|
|
99
|
+
const connections = [];
|
|
100
|
+
for (const input of block.inputList) {
|
|
101
|
+
const connected = input.connection && input.connection.targetBlock();
|
|
102
|
+
connections.push([input.name || '', identify(input), input.connection ? identify(input.connection) : null, connected ? [connected.id, connected.type, identify(connected)] : null]);
|
|
103
|
+
if (connected)
|
|
104
|
+
capture(target.blocks.getBlock(connected.id));
|
|
105
|
+
}
|
|
106
|
+
const statementConnections = ['nextConnection', 'previousConnection', 'outputConnection'].map(name => {
|
|
107
|
+
const connection = block[name];
|
|
108
|
+
const connected = connection && connection.targetBlock();
|
|
109
|
+
return [name, connection ? identify(connection) : null,
|
|
110
|
+
connection && connection.targetConnection ? identify(connection.targetConnection) : null,
|
|
111
|
+
connected ? [connected.id, connected.type, identify(connected)] : null];
|
|
112
|
+
});
|
|
113
|
+
return JSON.stringify([records, connections, statementConnections]);
|
|
114
|
+
};
|
|
115
|
+
const sameDeclaration = (left, right) => JSON.stringify(captureFixedInputDeclaration(left)) ===
|
|
116
|
+
JSON.stringify(captureFixedInputDeclaration(right));
|
|
117
|
+
// Fixed inputs deliberately use a separate transaction from the legacy TEXT adapter.
|
|
118
|
+
export default function createFixedInputPort({ ScratchBlocks, workspace, vm, registry, identify, getState, begin, markEvent, finish, onError, subscribeScope }) {
|
|
119
|
+
initializeFixedFields(ScratchBlocks);
|
|
120
|
+
const inspect = (reference, suppliedDeclaration) => {
|
|
121
|
+
const state = getState();
|
|
122
|
+
const target = vm.editingTarget;
|
|
123
|
+
if (state.disposed || !target || target.id !== reference.targetId || workspace.isFlyout ||
|
|
124
|
+
workspace.options.readOnly || (workspace.isDragging && workspace.isDragging()))
|
|
125
|
+
return null;
|
|
126
|
+
const block = workspace.getBlockById(reference.blockId);
|
|
127
|
+
if (!block || block.isInFlyout || block.isShadow() || !block.isEditable())
|
|
128
|
+
return null;
|
|
129
|
+
const descriptor = registry.resolve(block.type);
|
|
130
|
+
const declaration = registry.resolveFixed(block.type);
|
|
131
|
+
if (!descriptor || !declaration || !sameDeclaration(declaration, suppliedDeclaration))
|
|
132
|
+
return null;
|
|
133
|
+
const vmBlock = target.blocks.getBlock(block.id);
|
|
134
|
+
const nextBlock = block.nextConnection && block.nextConnection.targetBlock();
|
|
135
|
+
if (!vmBlock || (nextBlock ? nextBlock.id : null) !== (vmBlock.next || null) || vmBlock.opcode !== block.type ||
|
|
136
|
+
(block.getParent() ? block.getParent().id : null) !== (vmBlock.parent || null))
|
|
137
|
+
return null;
|
|
138
|
+
const arguments_ = Object.create(null);
|
|
139
|
+
const inputs = Object.create(null);
|
|
140
|
+
const bindings = new Map();
|
|
141
|
+
const signature = [state.revision, identify(target), identify(block), identify(vmBlock), identify(declaration)];
|
|
142
|
+
for (const [role, names] of [['editable', declaration.editableArguments], ['readonly', declaration.readOnlyArguments]]) {
|
|
143
|
+
for (const argument of names) {
|
|
144
|
+
const item = inspectFixedArgument({ ScratchBlocks, block, vmBlock, target, argument, role,
|
|
145
|
+
identify, extensionId: descriptor.extensionId });
|
|
146
|
+
if (!item)
|
|
147
|
+
return null;
|
|
148
|
+
arguments_[argument] = item.slot;
|
|
149
|
+
inputs[argument] = item.metadata;
|
|
150
|
+
if (role === 'editable' && item.binding)
|
|
151
|
+
bindings.set(argument, item.binding);
|
|
152
|
+
signature.push(argument, item.signatureParts);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const untouched = untouchedState(block, vmBlock, target, bindings, identify);
|
|
156
|
+
signature.push(untouched);
|
|
157
|
+
const snapshot = captureFixedSnapshot({ targetId: target.id, blockId: block.id, opcode: block.type,
|
|
158
|
+
revision: JSON.stringify(signature), arguments: arguments_, fixedContext: {
|
|
159
|
+
contractId: declaration.contractId, contractRevision: declaration.contractRevision,
|
|
160
|
+
generation: String(state.generation), readOnlyArguments: declaration.readOnlyArguments,
|
|
161
|
+
dependencies: declaration.dependencies, inputs
|
|
162
|
+
} }, reference, declaration);
|
|
163
|
+
return { snapshot, bindings, declaration, target, block, vmBlock, state, untouched, runtime: vm.runtime };
|
|
164
|
+
};
|
|
165
|
+
const commit = request => {
|
|
166
|
+
var _a;
|
|
167
|
+
const expectedInput = own(request, 'expected');
|
|
168
|
+
const changesInput = own(request, 'changes');
|
|
169
|
+
const signal = own(request, 'signal');
|
|
170
|
+
try {
|
|
171
|
+
const contextInput = own(request, 'context');
|
|
172
|
+
const context = snapshotJson({ consumerId: own(contextInput, 'consumerId'),
|
|
173
|
+
nowMs: own(contextInput, 'nowMs'), settings: own(contextInput, 'settings') });
|
|
174
|
+
if (typeof context.consumerId !== 'string' || !context.consumerId ||
|
|
175
|
+
typeof context.nowMs !== 'number' || !Number.isFinite(context.nowMs)) {
|
|
176
|
+
throw new TypeError('Invalid fixed validation context.');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (_b) {
|
|
180
|
+
throw new TypeError('Invalid fixed validation context.');
|
|
181
|
+
}
|
|
182
|
+
if (isAborted(signal))
|
|
183
|
+
conflict();
|
|
184
|
+
const expected = captureFixedSnapshot(expectedInput);
|
|
185
|
+
const current = inspect(expected, registry.resolveFixed(expected.opcode));
|
|
186
|
+
if (!current || !sameFixedSnapshot(expected, current.snapshot))
|
|
187
|
+
conflict();
|
|
188
|
+
const declaration = current.declaration;
|
|
189
|
+
captureFixedSnapshot(expected, expected, declaration);
|
|
190
|
+
if (!Array.isArray(changesInput))
|
|
191
|
+
throw new TypeError('Expected a fixed argument patch array.');
|
|
192
|
+
const prepared = [];
|
|
193
|
+
const seenNames = new Set();
|
|
194
|
+
const seenFields = new Set();
|
|
195
|
+
for (let i = 0; i < changesInput.length; i++) {
|
|
196
|
+
const change = own(changesInput, String(i));
|
|
197
|
+
const name = own(change, 'argument');
|
|
198
|
+
const value = own(change, 'value');
|
|
199
|
+
if (typeof name !== 'string' || seenNames.has(name) ||
|
|
200
|
+
!declaration.editableArguments.includes(name) || !current.bindings.has(name)) {
|
|
201
|
+
throw new TypeError('Only distinct editable standard fields may be changed.');
|
|
202
|
+
}
|
|
203
|
+
seenNames.add(name);
|
|
204
|
+
const binding = current.bindings.get(name);
|
|
205
|
+
if (seenFields.has(binding.field))
|
|
206
|
+
throw new TypeError('Duplicate field write.');
|
|
207
|
+
seenFields.add(binding.field);
|
|
208
|
+
if (Object.is(value, binding.value))
|
|
209
|
+
continue;
|
|
210
|
+
prepared.push({ ...prepareFixedChange(binding, value), argument: name });
|
|
211
|
+
}
|
|
212
|
+
if (validateFixedBranchChanges(declaration, expected, prepared.map(item => ({ argument: item.argument, value: item.next }))).length) {
|
|
213
|
+
throw new TypeError('Fixed branch changes require a valid canonical branch policy.');
|
|
214
|
+
}
|
|
215
|
+
const guard = () => {
|
|
216
|
+
const state = getState();
|
|
217
|
+
if (isAborted(signal) || state.disposed || state.generation !== current.state.generation ||
|
|
218
|
+
state.externalRevision !== current.state.externalRevision || vm.editingTarget !== current.target ||
|
|
219
|
+
vm.runtime !== current.runtime ||
|
|
220
|
+
registry.resolveFixed(expected.opcode) !== declaration)
|
|
221
|
+
conflict();
|
|
222
|
+
};
|
|
223
|
+
guard();
|
|
224
|
+
const beforeWrite = inspect(expected, declaration);
|
|
225
|
+
if (!beforeWrite || !sameFixedSnapshot(expected, beforeWrite.snapshot))
|
|
226
|
+
conflict();
|
|
227
|
+
guard();
|
|
228
|
+
if (!prepared.length)
|
|
229
|
+
return;
|
|
230
|
+
const Events = ScratchBlocks.Events;
|
|
231
|
+
if (!Events.isEnabled() || !Events.recordUndo || Events.getGroup() ||
|
|
232
|
+
!Array.isArray(workspace.undoStack_) || !Array.isArray(workspace.redoStack_)) {
|
|
233
|
+
throw new Error('The workspace is busy or its undo API is unsupported.');
|
|
234
|
+
}
|
|
235
|
+
const undoStack = workspace.undoStack_;
|
|
236
|
+
const redoStack = workspace.redoStack_;
|
|
237
|
+
ensureNativeHistory(undoStack);
|
|
238
|
+
ensureNativeHistory(redoStack);
|
|
239
|
+
const configuredMaxUndo = inheritedData(workspace, 'MAX_UNDO');
|
|
240
|
+
const maxUndo = configuredMaxUndo === undefined ? Infinity : configuredMaxUndo;
|
|
241
|
+
if (maxUndo !== Infinity && (!Number.isSafeInteger(maxUndo) || maxUndo < 0)) {
|
|
242
|
+
throw new TypeError('Unsupported undo configuration.');
|
|
243
|
+
}
|
|
244
|
+
const undo = copyHistory(undoStack);
|
|
245
|
+
const redo = copyHistory(redoStack);
|
|
246
|
+
const oldGroup = Events.getGroup();
|
|
247
|
+
const runtime = current.runtime;
|
|
248
|
+
const dirty = runtime.emitProjectChanged;
|
|
249
|
+
const dirtyDescriptor = Object.getOwnPropertyDescriptor(runtime, 'emitProjectChanged');
|
|
250
|
+
const savedVmFields = prepared.map(item => ({ binding: item, block: item.vmBlock, name: item.fieldName,
|
|
251
|
+
fields: item.vmBlock.fields, field: item.vmBlock.fields[item.fieldName],
|
|
252
|
+
descriptors: Object.getOwnPropertyDescriptors(item.vmBlock.fields[item.fieldName]) }));
|
|
253
|
+
let succeeded = false;
|
|
254
|
+
let failure;
|
|
255
|
+
let activeEvent = null;
|
|
256
|
+
const pendingHistory = [];
|
|
257
|
+
const externalWrites = new Map();
|
|
258
|
+
const byField = new Map(prepared.map(item => [JSON.stringify([item.block.id, item.fieldName]), item]));
|
|
259
|
+
let externalDirty = false;
|
|
260
|
+
const ownedEvents = new WeakSet();
|
|
261
|
+
const originalDelivery = workspace.fireChangeListener;
|
|
262
|
+
const deliveryDescriptor = Object.getOwnPropertyDescriptor(workspace, 'fireChangeListener');
|
|
263
|
+
const stableScope = () => {
|
|
264
|
+
const state = getState();
|
|
265
|
+
return state.generation === current.state.generation && vm.editingTarget === current.target && vm.runtime === runtime;
|
|
266
|
+
};
|
|
267
|
+
let begun = false;
|
|
268
|
+
try {
|
|
269
|
+
runtime.emitProjectChanged = () => {
|
|
270
|
+
if (!activeEvent || !ownedEvents.has(activeEvent) || !stableScope())
|
|
271
|
+
externalDirty = true;
|
|
272
|
+
};
|
|
273
|
+
workspace.fireChangeListener = function (event) {
|
|
274
|
+
const previous = activeEvent;
|
|
275
|
+
const external = event && !event.isUiEvent && !ownedEvents.has(event);
|
|
276
|
+
const edited = external && event.type === 'change' && event.element === 'field' &&
|
|
277
|
+
byField.get(JSON.stringify([event.blockId, event.name]));
|
|
278
|
+
if (edited && !externalWrites.has(edited.field)) {
|
|
279
|
+
// An external undo must not revive this editor's uncommitted candidate.
|
|
280
|
+
if (Object.is(event.oldValue, edited.next))
|
|
281
|
+
event.oldValue = edited.value;
|
|
282
|
+
externalWrites.set(edited.field, null);
|
|
283
|
+
}
|
|
284
|
+
activeEvent = event;
|
|
285
|
+
try {
|
|
286
|
+
return originalDelivery.call(this, event);
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
if (edited) {
|
|
290
|
+
const fields = edited.vmBlock.fields;
|
|
291
|
+
const field = fields && fields[edited.fieldName];
|
|
292
|
+
externalWrites.set(edited.field, { binding: captureFixedFieldState(edited),
|
|
293
|
+
fields, field, descriptors: field && Object.getOwnPropertyDescriptors(field) });
|
|
294
|
+
}
|
|
295
|
+
activeEvent = previous;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
Events.setGroup(true);
|
|
299
|
+
Events.disable();
|
|
300
|
+
begin();
|
|
301
|
+
begun = true;
|
|
302
|
+
guard();
|
|
303
|
+
for (const item of prepared) {
|
|
304
|
+
guard();
|
|
305
|
+
item.field.setValue(item.next);
|
|
306
|
+
if (!Object.is(item.field.getValue(), item.next))
|
|
307
|
+
throw new Error('The field rejected its value.');
|
|
308
|
+
guard();
|
|
309
|
+
}
|
|
310
|
+
for (const item of prepared) {
|
|
311
|
+
guard();
|
|
312
|
+
const event = new Events.Change(item.block, 'field', item.fieldName, item.value, item.next);
|
|
313
|
+
ownedEvents.add(event);
|
|
314
|
+
markEvent(event);
|
|
315
|
+
// Stage only editor-owned undo records; external events retain normal semantics.
|
|
316
|
+
const recordUndo = event.recordUndo;
|
|
317
|
+
event.recordUndo = false;
|
|
318
|
+
try {
|
|
319
|
+
workspace.fireChangeListener(event);
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
event.recordUndo = recordUndo;
|
|
323
|
+
}
|
|
324
|
+
if (recordUndo)
|
|
325
|
+
pendingHistory.push(event);
|
|
326
|
+
guard();
|
|
327
|
+
}
|
|
328
|
+
const after = inspect(expected, declaration);
|
|
329
|
+
if (!after || after.untouched !== current.untouched)
|
|
330
|
+
conflict();
|
|
331
|
+
const wantedArguments = { ...expected.arguments };
|
|
332
|
+
for (const item of prepared)
|
|
333
|
+
wantedArguments[item.argument] = { kind: 'literal', value: item.next };
|
|
334
|
+
if (!sameFixedSnapshot({ ...expected, revision: after.snapshot.revision, arguments: wantedArguments }, after.snapshot))
|
|
335
|
+
conflict();
|
|
336
|
+
guard();
|
|
337
|
+
if (workspace.undoStack_ !== undoStack || workspace.redoStack_ !== redoStack ||
|
|
338
|
+
undoStack.length !== undo.length || redoStack.length !== redo.length ||
|
|
339
|
+
undo.some((entry, index) => undoStack[index] !== entry) ||
|
|
340
|
+
redo.some((entry, index) => redoStack[index] !== entry))
|
|
341
|
+
conflict();
|
|
342
|
+
ensureNativeHistory(undoStack);
|
|
343
|
+
ensureNativeHistory(redoStack);
|
|
344
|
+
// This section contains no vendor callbacks; both native arrays are writable dense data.
|
|
345
|
+
try {
|
|
346
|
+
for (const event of pendingHistory)
|
|
347
|
+
Array.prototype.push.call(undoStack, event);
|
|
348
|
+
if (undoStack.length > maxUndo) {
|
|
349
|
+
Array.prototype.copyWithin.call(undoStack, 0, undoStack.length - maxUndo);
|
|
350
|
+
undoStack.length = maxUndo;
|
|
351
|
+
}
|
|
352
|
+
redoStack.length = 0;
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
replaceHistory(undoStack, undo);
|
|
356
|
+
replaceHistory(redoStack, redo);
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
succeeded = true;
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
failure = error;
|
|
363
|
+
for (const item of begun ? prepared : []) {
|
|
364
|
+
try {
|
|
365
|
+
restoreFixedField(((_a = externalWrites.get(item.field)) === null || _a === void 0 ? void 0 : _a.binding) || item);
|
|
366
|
+
}
|
|
367
|
+
catch (restoreError) {
|
|
368
|
+
// Continue restoring every independent field before reporting a failure.
|
|
369
|
+
try {
|
|
370
|
+
onError(restoreError);
|
|
371
|
+
}
|
|
372
|
+
catch ( /* Observer failures do not interrupt rollback. */_c) { /* Observer failures do not interrupt rollback. */ }
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const saved of begun ? savedVmFields : []) {
|
|
376
|
+
const outside = externalWrites.get(saved.binding.field);
|
|
377
|
+
const fields = saved.block.fields;
|
|
378
|
+
if (!fields)
|
|
379
|
+
continue;
|
|
380
|
+
const field = outside ? outside.field : saved.field;
|
|
381
|
+
const descriptors = outside ? outside.descriptors : saved.descriptors;
|
|
382
|
+
if (!field) {
|
|
383
|
+
delete fields[saved.name];
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
fields[saved.name] = field;
|
|
387
|
+
for (const key of Object.keys(field))
|
|
388
|
+
if (!Object.hasOwn(descriptors, key))
|
|
389
|
+
delete field[key];
|
|
390
|
+
Object.defineProperties(field, descriptors);
|
|
391
|
+
}
|
|
392
|
+
if (begun && typeof current.target.blocks.resetCache === 'function')
|
|
393
|
+
current.target.blocks.resetCache();
|
|
394
|
+
// Pending editor events never entered either history. External histories are authoritative.
|
|
395
|
+
}
|
|
396
|
+
finally {
|
|
397
|
+
try {
|
|
398
|
+
if (begun)
|
|
399
|
+
finish(succeeded);
|
|
400
|
+
}
|
|
401
|
+
finally {
|
|
402
|
+
try {
|
|
403
|
+
if (!Events.isEnabled())
|
|
404
|
+
Events.enable();
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
try {
|
|
408
|
+
Events.setGroup(oldGroup);
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
if (deliveryDescriptor)
|
|
412
|
+
Object.defineProperty(workspace, 'fireChangeListener', deliveryDescriptor);
|
|
413
|
+
else
|
|
414
|
+
delete workspace.fireChangeListener;
|
|
415
|
+
if (dirtyDescriptor)
|
|
416
|
+
Object.defineProperty(runtime, 'emitProjectChanged', dirtyDescriptor);
|
|
417
|
+
else
|
|
418
|
+
delete runtime.emitProjectChanged;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (succeeded || externalDirty) {
|
|
424
|
+
try {
|
|
425
|
+
dirty.call(runtime);
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
try {
|
|
429
|
+
onError(error);
|
|
430
|
+
}
|
|
431
|
+
catch ( /* Dirty observers do not change the transaction result. */_d) { /* Dirty observers do not change the transaction result. */ }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (!succeeded)
|
|
435
|
+
throw failure;
|
|
436
|
+
};
|
|
437
|
+
return Object.freeze({ capability: FIXED_INPUTS_CAPABILITY,
|
|
438
|
+
readBlock: (reference, declaration) => { var _a; return ((_a = inspect(reference, declaration)) === null || _a === void 0 ? void 0 : _a.snapshot) || null; }, commit,
|
|
439
|
+
subscribeInvalidation: (reference, declaration, listener) => {
|
|
440
|
+
const captured = inspect(reference, declaration);
|
|
441
|
+
if (!captured || typeof subscribeScope !== 'function') {
|
|
442
|
+
listener(new BlockEditorConflictError());
|
|
443
|
+
return () => { };
|
|
444
|
+
}
|
|
445
|
+
let active = true;
|
|
446
|
+
const check = () => {
|
|
447
|
+
const state = getState();
|
|
448
|
+
if (!active || (!state.disposed && state.generation === captured.state.generation &&
|
|
449
|
+
vm.editingTarget === captured.target && vm.runtime === captured.runtime &&
|
|
450
|
+
workspace.getBlockById(reference.blockId) === captured.block &&
|
|
451
|
+
captured.target.blocks.getBlock(reference.blockId) === captured.vmBlock &&
|
|
452
|
+
registry.resolveFixed(captured.snapshot.opcode) === captured.declaration))
|
|
453
|
+
return;
|
|
454
|
+
active = false;
|
|
455
|
+
listener(new BlockEditorConflictError());
|
|
456
|
+
};
|
|
457
|
+
const unsubscribe = subscribeScope(check);
|
|
458
|
+
return () => { active = false; unsubscribe(); };
|
|
459
|
+
} });
|
|
460
|
+
}
|
package/dist/turbowarp/host.d.ts
CHANGED
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
export default createBlockEditorHost;
|
|
2
|
-
declare function createBlockEditorHost({ ScratchBlocks, workspace, vm, registry, enableDescendants, onError }: {
|
|
2
|
+
declare function createBlockEditorHost({ ScratchBlocks, workspace, vm, registry, enableDescendants, enableFixedInputs, onError }: {
|
|
3
3
|
ScratchBlocks: any;
|
|
4
4
|
workspace: any;
|
|
5
5
|
vm: any;
|
|
6
6
|
registry: any;
|
|
7
7
|
enableDescendants?: boolean | undefined;
|
|
8
|
+
enableFixedInputs?: boolean | undefined;
|
|
8
9
|
onError?: ((...data: any[]) => void) | undefined;
|
|
9
10
|
}): {
|
|
10
11
|
isCommitting: () => boolean;
|
|
11
|
-
readBlock: (reference: any) =>
|
|
12
|
-
targetId: any;
|
|
13
|
-
blockId: any;
|
|
14
|
-
opcode: any;
|
|
15
|
-
revision: string;
|
|
16
|
-
arguments: {};
|
|
17
|
-
} | null;
|
|
12
|
+
readBlock: (reference: any) => any;
|
|
18
13
|
commit: ({ expected, changes }: {
|
|
19
14
|
expected: any;
|
|
20
15
|
changes: any;
|
package/dist/turbowarp/host.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { BlockEditorConflictError, DESCENDANT_EDITING_CAPABILITY } from '../index.js';
|
|
2
2
|
import collectBlockEditorSubtree from './subtree.js';
|
|
3
|
+
import createFixedInputPort from './fixed-transaction.js';
|
|
3
4
|
const owns = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
4
5
|
// Adapt only standard, unvalidated text shadows; never evaluate connected reporters.
|
|
5
|
-
const createBlockEditorHost = ({ ScratchBlocks, workspace, vm, registry, enableDescendants = false, onError = console.error }) => {
|
|
6
|
+
const createBlockEditorHost = ({ ScratchBlocks, workspace, vm, registry, enableDescendants = false, enableFixedInputs = false, onError = console.error }) => {
|
|
6
7
|
let disposed = false;
|
|
7
8
|
let committing = false;
|
|
8
9
|
let revision = 0;
|
|
9
10
|
let identity = 0;
|
|
10
11
|
let generation = 1;
|
|
12
|
+
let externalRevision = 0;
|
|
13
|
+
let fixedTransaction = null;
|
|
11
14
|
let lastTarget = vm.editingTarget;
|
|
12
15
|
const versions = new Map();
|
|
13
16
|
const invalidations = new Set();
|
|
17
|
+
const fixedScopeListeners = new Set();
|
|
18
|
+
const notifyFixedScopes = () => {
|
|
19
|
+
for (const listener of [...fixedScopeListeners]) {
|
|
20
|
+
try {
|
|
21
|
+
listener();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
try {
|
|
25
|
+
onError(error);
|
|
26
|
+
}
|
|
27
|
+
catch ( /* Scope observers cannot interrupt native events. */_a) { /* Scope observers cannot interrupt native events. */ }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
14
31
|
let notifyInvalidations = () => { };
|
|
15
32
|
const identities = new WeakMap();
|
|
16
33
|
const identify = object => {
|
|
@@ -22,6 +39,17 @@ const createBlockEditorHost = ({ ScratchBlocks, workspace, vm, registry, enableD
|
|
|
22
39
|
if (event && event.isUiEvent)
|
|
23
40
|
return;
|
|
24
41
|
revision++;
|
|
42
|
+
if (fixedTransaction && event && fixedTransaction.events.has(event)) {
|
|
43
|
+
fixedTransaction.revisions++;
|
|
44
|
+
if (enableDescendants) {
|
|
45
|
+
for (const id of new Set([event.blockId, ...(event.ids || [])])) {
|
|
46
|
+
if (id)
|
|
47
|
+
fixedTransaction.versions.set(id, (fixedTransaction.versions.get(id) || 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
externalRevision++;
|
|
25
53
|
if (enableDescendants && event) {
|
|
26
54
|
const ids = new Set([event.blockId, ...(event.ids || [])]);
|
|
27
55
|
for (const id of ids)
|
|
@@ -30,6 +58,7 @@ const createBlockEditorHost = ({ ScratchBlocks, workspace, vm, registry, enableD
|
|
|
30
58
|
}
|
|
31
59
|
if (!committing)
|
|
32
60
|
notifyInvalidations();
|
|
61
|
+
notifyFixedScopes();
|
|
33
62
|
};
|
|
34
63
|
const renew = () => {
|
|
35
64
|
generation++;
|
|
@@ -56,6 +85,8 @@ const createBlockEditorHost = ({ ScratchBlocks, workspace, vm, registry, enableD
|
|
|
56
85
|
if (!block || block.isInFlyout || block.isShadow())
|
|
57
86
|
return null;
|
|
58
87
|
const descriptor = registry.resolve(block.type);
|
|
88
|
+
if (typeof registry.resolveFixed === 'function' && registry.resolveFixed(block.type))
|
|
89
|
+
return null;
|
|
59
90
|
const vmBlock = target.blocks.getBlock(block.id);
|
|
60
91
|
if ((!structural && !descriptor) || (!block.isEditable() && (!structural || descriptor)) ||
|
|
61
92
|
!vmBlock || vmBlock.opcode !== block.type)
|
|
@@ -238,10 +269,17 @@ const createBlockEditorHost = ({ ScratchBlocks, workspace, vm, registry, enableD
|
|
|
238
269
|
const host = {
|
|
239
270
|
isCommitting: () => committing,
|
|
240
271
|
readBlock: reference => {
|
|
272
|
+
const block = workspace.getBlockById(reference.blockId);
|
|
273
|
+
const declaration = block && typeof registry.resolveFixed === 'function' && registry.resolveFixed(block.type);
|
|
274
|
+
if (declaration)
|
|
275
|
+
return host.fixedInputs ? host.fixedInputs.readBlock(reference, declaration) : null;
|
|
241
276
|
const result = inspect(reference);
|
|
242
277
|
return result ? result.snapshot : null;
|
|
243
278
|
},
|
|
244
279
|
commit: ({ expected, changes }) => {
|
|
280
|
+
if (typeof registry.resolveFixed === 'function' && registry.resolveFixed(expected.opcode)) {
|
|
281
|
+
throw new TypeError('Fixed input editors require the fixed transaction port.');
|
|
282
|
+
}
|
|
245
283
|
const current = inspect(expected);
|
|
246
284
|
if (!current || JSON.stringify(current.snapshot) !== JSON.stringify(expected)) {
|
|
247
285
|
throw new BlockEditorConflictError();
|
|
@@ -251,6 +289,8 @@ const createBlockEditorHost = ({ ScratchBlocks, workspace, vm, registry, enableD
|
|
|
251
289
|
invalidate: renew,
|
|
252
290
|
dispose: () => {
|
|
253
291
|
disposed = true;
|
|
292
|
+
notifyFixedScopes();
|
|
293
|
+
fixedScopeListeners.clear();
|
|
254
294
|
notifyInvalidations();
|
|
255
295
|
invalidations.clear();
|
|
256
296
|
workspace.removeChangeListener(invalidate);
|
|
@@ -284,6 +324,33 @@ const createBlockEditorHost = ({ ScratchBlocks, workspace, vm, registry, enableD
|
|
|
284
324
|
}
|
|
285
325
|
};
|
|
286
326
|
}
|
|
327
|
+
if (enableFixedInputs === true) {
|
|
328
|
+
host.fixedInputs = createFixedInputPort({ ScratchBlocks, workspace, vm, registry, identify, onError,
|
|
329
|
+
subscribeScope: listener => {
|
|
330
|
+
fixedScopeListeners.add(listener);
|
|
331
|
+
return () => { fixedScopeListeners.delete(listener); };
|
|
332
|
+
},
|
|
333
|
+
getState: () => ({ disposed, revision, generation, externalRevision }),
|
|
334
|
+
begin: () => {
|
|
335
|
+
committing = true;
|
|
336
|
+
fixedTransaction = { events: new WeakSet(), revisions: 0, versions: new Map() };
|
|
337
|
+
},
|
|
338
|
+
markEvent: event => fixedTransaction.events.add(event),
|
|
339
|
+
finish: succeeded => {
|
|
340
|
+
if (!succeeded) {
|
|
341
|
+
revision -= fixedTransaction.revisions;
|
|
342
|
+
for (const [id, delta] of fixedTransaction.versions) {
|
|
343
|
+
const remaining = (versions.get(id) || 0) - delta;
|
|
344
|
+
if (remaining > 0)
|
|
345
|
+
versions.set(id, remaining);
|
|
346
|
+
else
|
|
347
|
+
versions.delete(id);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
fixedTransaction = null;
|
|
351
|
+
committing = false;
|
|
352
|
+
} });
|
|
353
|
+
}
|
|
287
354
|
return host;
|
|
288
355
|
};
|
|
289
356
|
export default createBlockEditorHost;
|
package/dist/turbowarp.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface TurboWarpHostOptions {
|
|
|
15
15
|
readonly vm: object;
|
|
16
16
|
readonly registry: EditorRegistry;
|
|
17
17
|
readonly enableDescendants?: boolean;
|
|
18
|
+
/** Opt in to standard fixed fields; GUI binding remains a separate capability. */
|
|
19
|
+
readonly enableFixedInputs?: boolean;
|
|
18
20
|
readonly onError?: (error: unknown) => void;
|
|
19
21
|
}
|
|
20
22
|
export interface TurboWarpBlockEditorHost extends BlockEditorHost {
|
|
@@ -28,6 +30,8 @@ export interface TurboWarpBlockEditorOptions {
|
|
|
28
30
|
/** Only true attaches listeners or decorates icons. Read feature flags in the host at startup. */
|
|
29
31
|
readonly enabled?: boolean;
|
|
30
32
|
readonly descendants?: boolean;
|
|
33
|
+
/** Opt in to fixed argument editors independently of SourceForms and Descendants. */
|
|
34
|
+
readonly fixedInputs?: boolean;
|
|
31
35
|
readonly ScratchBlocks: object;
|
|
32
36
|
readonly workspace: object;
|
|
33
37
|
readonly vm: object;
|
package/dist/turbowarp.js
CHANGED
|
@@ -8,7 +8,7 @@ export function attachTurboWarpBlockEditors(options) {
|
|
|
8
8
|
var _a;
|
|
9
9
|
if (options.enabled !== true)
|
|
10
10
|
return Object.freeze({ refresh() { }, dispose() { } });
|
|
11
|
-
const bridge = createBridge({ ...options, descendants: options.descendants === true,
|
|
11
|
+
const bridge = createBridge({ ...options, descendants: options.descendants === true, fixedInputs: options.fixedInputs === true,
|
|
12
12
|
onError: (_a = options.onError) !== null && _a !== void 0 ? _a : console.error });
|
|
13
13
|
let disposed = false;
|
|
14
14
|
return Object.freeze({
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ValidationIssue } from './schema-types.js';
|
|
2
2
|
export type { ValidationIssue } from './schema-types.js';
|
|
3
3
|
import type { DescendantEditorHost } from './descendant-types.js';
|
|
4
|
+
import type { FixedInputEditorHost, FixedInputViewDescription } from './fixed-input-types.js';
|
|
4
5
|
export declare const EDITOR_API_VERSION: 1;
|
|
5
6
|
export type LiteralValue = string | number | boolean;
|
|
6
7
|
export type JsonValue = null | LiteralValue | JsonObject | JsonValue[];
|
|
@@ -32,6 +33,8 @@ export interface CommitRequest {
|
|
|
32
33
|
readonly changes: readonly LiteralArgumentChange[];
|
|
33
34
|
}
|
|
34
35
|
export interface BlockEditorHost {
|
|
36
|
+
/** Optional fixed-field capability; legacy hosts do not need this port. */
|
|
37
|
+
readonly fixedInputs?: FixedInputEditorHost;
|
|
35
38
|
/** Optional additional capability; the single-block contract remains unchanged. */
|
|
36
39
|
readonly descendants?: DescendantEditorHost;
|
|
37
40
|
/** Return null for missing, inactive or non-editable blocks. Never evaluate reporters. */
|
|
@@ -52,6 +55,8 @@ export interface SessionState {
|
|
|
52
55
|
}
|
|
53
56
|
export interface EditorViewContext {
|
|
54
57
|
getDraft(): JsonObject;
|
|
58
|
+
/** Fixed views obtain only safe presentation data, never live host objects. */
|
|
59
|
+
getFixedInputView?(): FixedInputViewDescription | undefined;
|
|
55
60
|
/** Updates are rejected after this view unmounts, even when its session stays open. */
|
|
56
61
|
setDraft(draft: JsonObject): void;
|
|
57
62
|
/** Report live UI validation; save() always independently validates again. */
|