Skip to content

Commit fc19f76

Browse files
committed
Apply parent relegated constraints; make generateId monotonic
resolveConstraints: since 060bf2b ("Grid View #10") the parent's relegatedConstraintsToChildren() result was fetched and then ignored (`resolved = resolved.union(resolved)`). Union it into the child's resolved constraints as intended. That commit also dropped the old recursion into resolveConstraints(parent), which is left out on purpose. Layout effect (resolvedConstraints, read by CodelesslyEditor's resize delegate, size-fit panel, fit hit-testing and ListTile position model): - ListTile leading/trailing children now get minWidth = minLeadingWidth. - ListView children are capped at itemExtent on the scroll axis. - GridView children get tight cell-size constraints. - SinglePlaceholder children inherit the placeholder's constraints. CodelesslySDK does not call resolveConstraints directly, but nodes it processes via NodeProcessor pick up the same resolvedConstraints. generateId: lastPushTime and the random suffix were locals, so the same-millisecond increment branch never ran and IDs in one millisecond were not ordered. Move that state (and the Random) to library level. The alphabet is 63 chars (no '-'), so nextInt(63) / max index 62 were already correct; they now derive from _kPushChars.length. Tests: relegated_constraints_test.dart, generate_id_test.dart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VxYXXbiQSk5HVF2LMgQhU
1 parent 38b0f7b commit fc19f76

4 files changed

Lines changed: 114 additions & 14 deletions

File tree

‎lib/src/api/generate_id.dart‎

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ import 'dart:math' as math;
44
const String _kPushChars =
55
'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz';
66

7+
/// Timestamp of the most recently generated ID. Kept at library level so
8+
/// that consecutive calls within the same millisecond can detect the
9+
/// collision and increment [_lastRandomSuffix] instead of re-rolling it.
10+
int _lastPushTime = 0;
11+
12+
/// Random suffix of the most recently generated ID, as indices into
13+
/// [_kPushChars].
14+
final List<int> _lastRandomSuffix = List<int>.filled(12, 0);
15+
16+
final math.Random _random = math.Random.secure();
17+
718
/// Implementation from https://gist.github.com/mikelehen/3596a30bd69384624c11
819
///
920
/// Fancy ID generator that creates 20-character string identifiers with the following properties:
@@ -15,26 +26,30 @@ const String _kPushChars =
1526
/// latter ones will sort after the former ones. We do this by using the previous random bits
1627
/// but "incrementing" them by 1 (only in the case of a timestamp collision).
1728
String generateId() {
18-
int lastPushTime = 0;
19-
final List<int> randomSuffix = List.filled(12, 0);
20-
final math.Random random = math.Random.secure();
21-
2229
final int now = DateTime.now().toUtc().millisecondsSinceEpoch;
2330
String id = _toPushIdBase64(now, 8);
2431

25-
if (now != lastPushTime) {
32+
if (now != _lastPushTime) {
2633
for (int i = 0; i < 12; i += 1) {
27-
randomSuffix[i] = random.nextInt(63);
34+
_lastRandomSuffix[i] = _random.nextInt(_kPushChars.length);
2835
}
2936
} else {
37+
// Same millisecond: increment the previous suffix by one (base-N with
38+
// carry) so this ID sorts strictly after the previous one.
3039
int i;
31-
for (i = 11; i >= 0 && randomSuffix[i] == 62; i--) {
32-
randomSuffix[i] = 0;
40+
for (
41+
i = 11;
42+
i >= 0 && _lastRandomSuffix[i] == _kPushChars.length - 1;
43+
i--
44+
) {
45+
_lastRandomSuffix[i] = 0;
3346
}
34-
randomSuffix[i] += 1;
47+
_lastRandomSuffix[i] += 1;
3548
}
36-
final String suffixStr = randomSuffix.map((int i) => _kPushChars[i]).join();
37-
lastPushTime = now;
49+
final String suffixStr = _lastRandomSuffix
50+
.map((int i) => _kPushChars[i])
51+
.join();
52+
_lastPushTime = now;
3853

3954
return '$id$suffixStr';
4055
}

‎lib/src/api/node_processor.dart‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,9 @@ class NodeProcessor {
172172
/// This function will take the [internalConstraints] and union them to the
173173
/// [constraints] field to get a reinforced set of constraints.
174174
///
175-
/// This function is recursive and may travel up the parent tree from the
176-
/// internal [BaseNode.relegatedConstraintsToChildren] function.
175+
/// The parent's [BaseNode.relegatedConstraintsToChildren] for this node, if
176+
/// any, are also unioned in (e.g. a ListTile's minimum leading width, a
177+
/// ListView's item extent, or a GridView's cell size).
177178
static BoxConstraintsModel resolveConstraints(
178179
BaseNode node, {
179180
SizeFit? horizontalFit,
@@ -189,7 +190,7 @@ class NodeProcessor {
189190
final BoxConstraintsModel? relegatedConstraints = parent
190191
.relegatedConstraintsToChildren(node);
191192
if (relegatedConstraints != null) {
192-
resolved = resolved.union(resolved);
193+
resolved = resolved.union(relegatedConstraints);
193194
}
194195
}
195196

‎test/generate_id_test.dart‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
test('IDs generated in a tight loop are unique and strictly increasing', () {
6+
final List<String> ids = List<String>.generate(10000, (_) => generateId());
7+
8+
expect(ids.toSet(), hasLength(ids.length));
9+
for (int i = 1; i < ids.length; i++) {
10+
expect(
11+
ids[i].compareTo(ids[i - 1]),
12+
greaterThan(0),
13+
reason: '${ids[i]} should sort after ${ids[i - 1]}',
14+
);
15+
}
16+
});
17+
18+
test('IDs are 20 characters', () {
19+
expect(generateId(), hasLength(20));
20+
});
21+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:test/test.dart';
3+
4+
/// [NodeProcessor.resolveConstraints] must union the parent's
5+
/// [BaseNode.relegatedConstraintsToChildren] into the child's resolved
6+
/// constraints. It used to union the child's constraints with themselves,
7+
/// so a ListTile's minimum leading width or a ListView's item extent never
8+
/// reached the child.
9+
void main() {
10+
final Map<String, BaseNode> nodes = <String, BaseNode>{};
11+
12+
setUp(() {
13+
nodes.clear();
14+
NodeProcessor.registerNodeGetter((String id) => nodes[id]!);
15+
});
16+
17+
RectangleNode child(String parentID, {BoxConstraintsModel? constraints}) {
18+
final RectangleNode node = RectangleNode(
19+
id: 'child',
20+
name: 'child',
21+
basicBoxLocal: NodeBox(0, 0, 10, 10),
22+
constraints: constraints ?? const BoxConstraintsModel(),
23+
);
24+
node.parentID = parentID;
25+
nodes[node.id] = node;
26+
return node;
27+
}
28+
29+
test('a ListTile relegates its minLeadingWidth to the leading node', () {
30+
final RectangleNode leading = child('tile');
31+
nodes['tile'] = ListTileNode(
32+
id: 'tile',
33+
name: 'tile',
34+
basicBoxLocal: NodeBox(0, 0, 300, 65),
35+
leading: leading.id,
36+
properties: ListTileProperties(minLeadingWidth: 56),
37+
children: <String>[leading.id],
38+
);
39+
40+
final BoxConstraintsModel resolved = NodeProcessor.resolveConstraints(
41+
leading,
42+
);
43+
expect(resolved.minWidth, 56);
44+
});
45+
46+
test('a ListView caps its children at the item extent', () {
47+
final RectangleNode item = child(
48+
'list',
49+
constraints: const BoxConstraintsModel(maxHeight: 200),
50+
);
51+
nodes['list'] = ListViewNode(
52+
id: 'list',
53+
name: 'list',
54+
basicBoxLocal: NodeBox(0, 0, 300, 400),
55+
properties: ListViewProperties(itemExtent: 80),
56+
children: <String>[item.id],
57+
);
58+
59+
final BoxConstraintsModel resolved = NodeProcessor.resolveConstraints(item);
60+
// The tighter of the child's own max (200) and the relegated one (80).
61+
expect(resolved.maxHeight, 80);
62+
});
63+
}

0 commit comments

Comments
 (0)